Мы уже рассмотрели две статьи, где функционал C# dynamic мог привести к неожиданному поведению кода.
На этот раз я бы хотел показать позитивную сторону, где динамическая диспетчеризация позволяет упростить код, оставаясь при этом строго-типизированным.
В этом посте мы узнаем:
Иногда мы можем столкнуться с проблемой выбора перегрузки методов. Например:
Как мы видим, будет выбран метод только
Начнем с простого:
Выглядит не очень «красиво».
Ну что ж, мы объявили перечисление NodeType, ввели одноименное абстрактное свойство в класс Node. Задача решена.Спасибо за внимание.
Такой шаблон помогает в тех случаях, когда необходимо иметь межплатформенную переносимость; будь то язык программирования, либо среда исполнения. По такому пути пошел стандарт W3C DOM, например.
Мно́жественная диспетчериза́ция или мультиметод (multiple dispatch) является вариацией концепции в ООП для выбора вызываемого метода во время исполнения, а не компиляции.
Чтобы проникнуться идеей, начнем с простого: double dispatch (больше об этом здесь).
Суть double dispatch заключается в том, что привязка метода производится наследником в иерархии классов, а не в месте конкретного вызова. К минусам стоит отнести также и проблему расширяемости: при увеличении элементов в системе, придется заниматься copy-paste.
— Так и гдепроблема C# dynamic?! – спросите Вы.
В примере с приведением типов мы уже познакомились с примитивной реализацией шаблона мультиметод, где выбор требуемой перегрузки метода происходит в месте конкретного вызова в отличие от double dispatch.
Но постоянно писать кучу if'овне по фен-шую — плохо!
Не всегда, конечно. Просто примеры выше — синтетические. Поэтому рассмотрим более реалистичные.
Прежде чем двигаться дальше, давайте вспомним, что такое Enterprise Library.
Enterprise Library — это набор переиспользуемых компонентов/блоков (логирование, валидация, доступ к данным, обработка исключений и т.п.) для построения при��ожений. Существует отдельная книга, где рассмотрены все подробности работы.
Каждый из блоков можно конфигурировать как в XML, так и в самом коде.
Блок по обработке ошибок сегодня мы и рассмотрим.
Если Вы разрабатываете приложение, в котором используется pipeline паттерн а-ля ASP.NET, тогда Exception Handling Block (далее просто «EHB») может сильно упростить жизнь. Ведь краеугольным местом всегда является модель обработки ошибок в языке/фрейворке и т.п.
Пусть у нас есть участок кода, где мы заменили императивный код на более ООП-шный с шаблоном policy (вариации шаблона стратегия).
Было:
Стало (с использованием EHB):
Что ж, выглядит более «энтерпрайзно». Но можно ли избежать массивных зависимостей и ограничится возможностями самого языка C#?
— Императивный подход и есть сами возможности языка, — можно возразить.
Однако не только.
Попробуем написать свой Exception Handling Block, но только проще.
Для этого рассмотрим реализацию раскрутки обработчиков исключений в самом EHB.
Итак, исходный код еще раз:
Цепочка вызовов, начиная с
И это только вершина айсберга.
Ключевым интерфейсом является IExceptionHandler:
Возьмем его за основу и ничего более.
Объявим два интерфейса (зачем это нужно — увидим чуть позже):
Применим:
Все сработало! Но как? Ведь мы не написали ни строчки кода для разрешения типов исключений и т.п.
Так, если у нас есть соответствующая реализация IExceptionHandler, тогда используем ее.
Если нет — multiple dispatch через dynamic.
Так, пример №1 можно решить лишь одной строчкой кода:
На первый взгляд, весьма неочевидно, что целый паттерн может поместится лишь в одной языковой конструкции, но это так.
При детальном рассмотрении мы увидели, что построение простого policy-based обработчика исключений вполне возможно.
На этот раз я бы хотел показать позитивную сторону, где динамическая диспетчеризация позволяет упростить код, оставаясь при этом строго-типизированным.
В этом посте мы узнаем:
- возможные варианты реализации шаблона множественная диспетчеризация (multiple/double dispatch & co.)
- как
избавиться отреализовать Exception Handling Block из Enterprise Library за пару минут. И, конечно же, упростить policy-based модель обработки ошибок - dynamic – эффективнее Вашего кода
А оно нам надо?
Иногда мы можем столкнуться с проблемой выбора перегрузки методов. Например:
public static void Sanitize(Node node) { Node node = new Document(); new Sanitizer().Cleanup(node); // void Cleanup(Node node) } class Sanitizer { public void Cleanup(Node node) { } public void Cleanup(Element element) { } public void Cleanup(Attribute attribute) { } public void Cleanup(Document document) { } }
[иерархия классов]
class Node { } class Attribute : Node { } class Document : Node { } class Element : Node { } class Text : Node { } class HtmlElement : Element { } class HtmlDocument : Document { }
Как мы видим, будет выбран метод только
void Cleanup(Node node). Данную проблему можно решить ООП-подходом, либо использовать приведение типов.Начнем с простого:
[приведение типов]
public static void Sanitize(Node node) { var sanitizer = new Sanitizer(); var document = node as Document; if (document != null) { sanitizer.Cleanup(document); } var element = node as Element; if (element != null) { sanitizer.Cleanup(element); } /* * остальные проверки на типы */ { // действие по-умолчанию sanitizer.Cleanup(node); } }
Выглядит не очень «красиво».
Поэтому применим ООП:
public static void Sanitize(Node node) { var sanitizer = new Sanitizer(); switch (node.NodeType) { case NodeType.Node: sanitizer.Cleanup(node); break; case NodeType.Element: sanitizer.Cleanup((Element)node); break; case NodeType.Document: sanitizer.Cleanup((Document)node); break; case NodeType.Text: sanitizer.Cleanup((Text)node); break; case NodeType.Attribute: sanitizer.Cleanup((Attribute)node); break; default: throw new ArgumentOutOfRangeException(); } } enum NodeType { Node, Element, Document, Text, Attribute } abstract class Node { public abstract NodeType NodeType { get; } } class Attribute : Node { public override NodeType NodeType { get { return NodeType.Attribute; } } } class Document : Node { public override NodeType NodeType { get { return NodeType.Document; } } } class Element : Node { public override NodeType NodeType { get { return NodeType.Element; } } } class Text : Node { public override NodeType NodeType { get { return NodeType.Text; } } }
Ну что ж, мы объявили перечисление NodeType, ввели одноименное абстрактное свойство в класс Node. Задача решена.
Такой шаблон помогает в тех случаях, когда необходимо иметь межплатформенную переносимость; будь то язык программирования, либо среда исполнения. По такому пути пошел стандарт W3C DOM, например.
Multiple dispatch pattern
Мно́жественная диспетчериза́ция или мультиметод (multiple dispatch) является вариацией концепции в ООП для выбора вызываемого метода во время исполнения, а не компиляции.
Чтобы проникнуться идеей, начнем с простого: double dispatch (больше об этом здесь).
Double dispatch
class Program { interface ICollidable { void CollideWith(ICollidable other); } class Asteroid : ICollidable { public void CollideWith(Asteroid other) { Console.WriteLine("Asteroid collides with Asteroid"); } public void CollideWith(Spaceship spaceship) { Console.WriteLine("Asteroid collides with Spaceship"); } public void CollideWith(ICollidable other) { other.CollideWith(this); } } class Spaceship : ICollidable { public void CollideWith(ICollidable other) { other.CollideWith(this); } public void CollideWith(Asteroid asteroid) { Console.WriteLine("Spaceship collides with Asteroid"); } public void CollideWith(Spaceship spaceship) { Console.WriteLine("Spaceship collides with Spaceship"); } } static void Main(string[] args) { var asteroid = new Asteroid(); var spaceship = new Spaceship(); asteroid.CollideWith(spaceship); asteroid.CollideWith(asteroid); } }
Суть double dispatch заключается в том, что привязка метода производится наследником в иерархии классов, а не в месте конкретного вызова. К минусам стоит отнести также и проблему расширяемости: при увеличении элементов в системе, придется заниматься copy-paste.
— Так и где
В примере с приведением типов мы уже познакомились с примитивной реализацией шаблона мультиметод, где выбор требуемой перегрузки метода происходит в месте конкретного вызова в отличие от double dispatch.
Но постоянно писать кучу if'ов
Не всегда, конечно. Просто примеры выше — синтетические. Поэтому рассмотрим более реалистичные.
I'll take two
Прежде чем двигаться дальше, давайте вспомним, что такое Enterprise Library.
Enterprise Library — это набор переиспользуемых компонентов/блоков (логирование, валидация, доступ к данным, обработка исключений и т.п.) для построения при��ожений. Существует отдельная книга, где рассмотрены все подробности работы.
Каждый из блоков можно конфигурировать как в XML, так и в самом коде.
Блок по обработке ошибок сегодня мы и рассмотрим.
Если Вы разрабатываете приложение, в котором используется pipeline паттерн а-ля ASP.NET, тогда Exception Handling Block (далее просто «EHB») может сильно упростить жизнь. Ведь краеугольным местом всегда является модель обработки ошибок в языке/фрейворке и т.п.
Пусть у нас есть участок кода, где мы заменили императивный код на более ООП-шный с шаблоном policy (вариации шаблона стратегия).
Было:
try { // code to throw exception } catch (InvalidCastException invalidCastException) { // log ex // rethrow if needed } catch (Exception e) { // throw new Exception with inner }
Стало (с использованием EHB):
var policies = new List<ExceptionPolicyDefinition>(); var myTestExceptionPolicy = new List<ExceptionPolicyEntry> { { new ExceptionPolicyEntry(typeof (InvalidCastException), PostHandlingAction.NotifyRethrow, new IExceptionHandler[] {new LoggingExceptionHandler(...),}) }, { new ExceptionPolicyEntry(typeof (Exception), PostHandlingAction.NotifyRethrow, new IExceptionHandler[] {new ReplaceHandler(...)}) } }; policies.Add(new ExceptionPolicyDefinition("MyTestExceptionPolicy", myTestExceptionPolicy)); ExceptionManager manager = new ExceptionManager(policies); try { // code to throw exception } catch (Exception e) { manager.HandleException(e, "Exception Policy Name"); }
Что ж, выглядит более «энтерпрайзно». Но можно ли избежать массивных зависимостей и ограничится возможностями самого языка C#?
— Императивный подход и есть сами возможности языка, — можно возразить.
Однако не только.
Попробуем написать свой Exception Handling Block, но только проще.
Для этого рассмотрим реализацию раскрутки обработчиков исключений в самом EHB.
Итак, исходный код еще раз:
ExceptionManager manager = new ExceptionManager(policies); try { // code to throw exception } catch (Exception e) { manager.HandleException(e, "Exception Policy Name"); }
Цепочка вызовов, начиная с
manager.HandleException(e, "Exception Policy Name")ExceptionPolicyDefinition.FindExceptionPolicyEntry
private ExceptionPolicyEntry FindExceptionPolicyEntry(Type exceptionType) { ExceptionPolicyEntry policyEntry = null; while (exceptionType != typeof(object)) { policyEntry = this.GetPolicyEntry(exceptionType); if (policyEntry != null) { return policyEntry; } exceptionType = exceptionType.BaseType; } return policyEntry; }
ExceptionPolicyEntry.Handle
public bool Handle(Exception exceptionToHandle) { if (exceptionToHandle == null) { throw new ArgumentNullException("exceptionToHandle"); } Guid handlingInstanceID = Guid.NewGuid(); Exception chainException = this.ExecuteHandlerChain(exceptionToHandle, handlingInstanceID); return this.RethrowRecommended(chainException, exceptionToHandle); }
ExceptionPolicyEntry.ExecuteHandlerChain
private Exception ExecuteHandlerChain(Exception ex, Guid handlingInstanceID) { string name = string.Empty; try { foreach (IExceptionHandler handler in this.handlers) { name = handler.GetType().Name; ex = handler.HandleException(ex, handlingInstanceID); } } catch (Exception exception) { // rest of implementation } return ex; }
И это только вершина айсберга.
Ключевым интерфейсом является IExceptionHandler:
namespace Microsoft.Practices.EnterpriseLibrary.ExceptionHandling { public interface IExceptionHandler { Exception HandleException(Exception ex, Guid handlingInstanceID); } }
Возьмем его за основу и ничего более.
Объявим два интерфейса (зачем это нужно — увидим чуть позже):
public interface IExceptionHandler { void HandleException<T>(T exception) where T : Exception; } public interface IExceptionHandler<T> where T : Exception { void Handle(T exception); }
А также обработчик для исключений ввода-вывода (I/O):
public class FileSystemExceptionHandler : IExceptionHandler, IExceptionHandler<Exception>, IExceptionHandler<IOException>, IExceptionHandler<FileNotFoundException> { public void HandleException<T>(T exception) where T : Exception { var handler = this as IExceptionHandler<T>; if (handler != null) handler.Handle(exception); else this.Handle((dynamic) exception); } public void Handle(Exception exception) { OnFallback(exception); } protected virtual void OnFallback(Exception exception) { // rest of implementation Console.WriteLine("Fallback: {0}", exception.GetType().Name); } public void Handle(IOException exception) { // rest of implementation Console.WriteLine("IO spec"); } public void Handle(FileNotFoundException exception) { // rest of implementation Console.WriteLine("FileNotFoundException spec"); } }
Применим:
IExceptionHandler defaultHandler = new FileSystemExceptionHandler(); defaultHandler.HandleException(new IOException()); // Handle(IOException) overload defaultHandler.HandleException(new DirectoryNotFoundException()); // Handle(IOException) overload defaultHandler.HandleException(new FileNotFoundException()); // Handle(FileNotFoundException) overload defaultHandler.HandleException(new FormatException()); // Handle(Exception) => OnFallback
Все сработало! Но как? Ведь мы не написали ни строчки кода для разрешения типов исключений и т.п.
Рассмотрим схему

Так, если у нас есть соответствующая реализация IExceptionHandler, тогда используем ее.
Если нет — multiple dispatch через dynamic.
Так, пример №1 можно решить лишь одной строчкой кода:
public static void Sanitize(Node node) { new Sanitizer().Cleanup((dynamic)node); }
Подводя итоги
На первый взгляд, весьма неочевидно, что целый паттерн может поместится лишь в одной языковой конструкции, но это так.
При детальном рассмотрении мы увидели, что построение простого policy-based обработчика исключений вполне возможно.
