Pull to refresh

Multiple dispatch в C#

Reading time 7 min
Views 29K
Мы уже рассмотрели две статьи, где функционал 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) { }

    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.

Так и где проблема 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<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 обработчика исключений вполне возможно.
Tags:
Hubs:
+19
Comments 17
Comments Comments 17

Articles