Multiple dispatch в C#
Мы уже рассмотрели двестатьи, где функционал C# dynamic мог привести к неожиданному поведению кода.
На этот раз я бы хотел показать позитивную сторону, где динамическая диспетчеризация позволяет упростить код, оставаясь при этом строго-типизированным.
В этом посте мы узнаем:
- возможные варианты реализации шаблона множественная диспетчеризация (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) { }
}
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 (больше об этом здесь).
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);
}
private void CollideWith(Asteroid asteroid)
{
Console.WriteLine("Spaceship collides with Asteroid");
}
private 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.
— Так и где проблема C# dynamic?! — спросите Вы.
В примере с приведением типов мы уже познакомились с примитивной реализацией шаблона мультиметод, где выбор требуемой перегрзуки метода происходит в месте конкретного вызова в отличие от 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();
var myTestExceptionPolicy = new List
{
{
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")
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;
}
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);
}
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 exception) where T : Exception;
}
public interface IExceptionHandler where T : Exception
{
void Handle(T exception);
}
public class FileSystemExceptionHandler : IExceptionHandler,
IExceptionHandler,
IExceptionHandler,
IExceptionHandler
{
public void HandleException(T exception) where T : Exception
{
var handler = this as IExceptionHandler;
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 обработчика исключений вполне возможно.