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) { }
    
        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 обработчика исключений вполне возможно.
    Share post

    Similar posts

    Comments 17

      +3
      а Visitor почему не отнесли к «ооп» подходу?
        +1
        верно подмечено! т.к. сам visitor относится к семейству шаблонов динамической диспетчеризации так же, как и double dispatch, то хотелось «разбавить» статью. visitor подошел бы тоже
          0
          Deleted
          0
          А проблема N1 не слишком надуманна? В первом примере кода всё красиво. Выбирается метод с типом Node. Уже в методе можно разрулить какой именно тип в иерархии. Уже в самой Node потом написать внутренний virtual Cleanup(), чтобы каждый сам себя чистил как нужно.
            0
            ok. как быть, если код сторонний?
              +3

              На самом деле неудобства наступают тогда, когда у вас объекты-данные, особенно если внешние или сгенерированные — добавление объекта вызывает изменения в нескольких местах. Когда количество таких объектов переваливает за первый десяток ( геометрия, например, или любая messaging-based система) — все это начинает вызывать ощутимый дискомфорт даже при чтении, не говоря уже про сопровождение.


              Мы тоже используем подобный паттерн для обработки "сообщений" к актерам в Orleans например. И тоже перед этим прошли через стадии var x = input as Foo, и enum внутри объекта.


              Я бы сказал что тут пример немного неудачный — паттерн хорошо работает когда у вас event-sourcing система, которая отправляет большое количество разных типов сообщений и бывает нужно получать сообщения не своего базового типа или даже просто иметь возможность очень быстро прикрутить новую обработку без изменения кода в 10 разных местах. Вот тогда этот паттерн очень помогает — просто заводите тип и пишете новый обработчик типа, остальное уже трогать не надо. А еще можно вкрутить централизованный логгинг, пре и пост-хуки и все это буквально в 1-2 линии кода
              Например


              public async Task ReceiveMsg(MessageEnvelopeBase item)
              {
                await validate(item as dynamic);
              
                await preProcessing(item as dynamic);
                await handle(item as dynamic);
                await postProcessing(item as dynamic);
              
                await logging(item as dynamic);
              }
              // тут определяем осмысленные методы для обработки базового класса MessageEnvelopeBase
              // А потом по необходимости пишем обработчики валидаторы и хуки для конкретных сообщений, 
              // уже больше не трогая центральную точку входа ReceiveMsg(MessageEnvelopeBase item)

              Не то что бы это какой-то rocket-science но код выглядит намного элегантнее. Да и разделение логики и данных хорошее — легко тестировать при помощи разных генераторов данных...

              0
              Почему не использовать Reflection?

              class Sanitize
              {
                  ...
              
                  public void Cleanup(object x)
                  {
                      GetType().GetMethod(nameof(Cleanup), new[] { x.GetType() })?.Invoke(this, x);
                  }
              }
              

                +3
                вопрос же не в вызове гипотетического метода у объекта.
                нужно: используя ссылку базового типа — выбрать правильную перегрузку.

                reflection — не то что план B, а самый H :)
                0
                Приведение типов и то, что вы назвали ООП подход, по сути одно и тоже, просто if заменили на switch.
                В таких случаях я всегда применяю Visitor. Не нужно в этом месте придумывать велосипед.
                  0
                  >просто if заменили на switch

                  спасибо кэп) только вот не надо путать возможности системы типов и ООП. есть системы, где этого нет.

                  >В таких случаях я всегда применяю Visitor
                  круто! принцип timtowtdi должен быть всегда, но по теме?
                  причинно-следственную связь между параграфами держать не обязательно?
                  т.е. надо писать статью в виде твита: не используешь visitor, тогда мы идем к вам, да? изобиловать выражениями вида «ну вы поняли».

                  >Не нужно в этом месте придумывать велосипед.
                  т.е. Вы назвали целый блок паттернов велосипедом. угу…
                    0
                    спасибо кэп) только вот не надо путать возможности системы типов и ООП. есть системы, где этого нет.

                    Вы же про C# пишите.
                    Суть в том, используете вы switch или if, не имеет значения. Т.к. у вас есть метод который знает о всех типах. Т.е. вы получили нерасширяемый дизайн (нарушили Open/closed principle)

                    т.е. Вы назвали целый блок паттернов велосипедом. угу…

                    Я имел ввиду, что в C# это делается визитором очень просто.
                    И в C# нет Multiple dispatch, поэтому используется Visitor (как бы вы его не называли, т.к. double dispatch в C# делается через Visitor)
                      0
                      >И в C# нет Multiple dispatch, поэтому используется Visitor (как бы вы его не называли, т.к. double dispatch в C# делается через Visitor)

                      предлагаю на этом закончить дискуссию :)
                        0
                        Если вы считаете, что с появлением dynamicв в C# появился Multiple dispatch, то да, думаю стоит закончить.
                          0
                          нет, я не считаю, что с появлением dynamic в C# появился multiple dispatch.

                          вышеназванный шаблон прекрасно эмулируется через if, switch, visitor pattern в C, Java, C++ и т.д. даже reflection подойдет как крайний случай.
                          но обычно предполагается, что visitor является примером решения/частным случаем самого double dispatch и обычно используется для отделения данных от алгоритмов.
                          Типичным примером для visitor'a является (как Вы сами прекрасно знаете) обход какого-либо набора данных, скажем AST.
                          как и в случае с примером из статьи, вместо CollideWith — Visit/Accept. плюсом является использование принципа открытости/закрытости.

                          НО, visitor — это поведенческий шаблон, в то время как double/multiple dispatch являются такими же атрибутами полиморфизма как и параметрический полиморфизм (aka обобщения aka generics).
                          Ведь оба вводят понятие динамической диспетчеризации в пику single dispatch.

                          Почему double dispatch — частный случай multiple dispatch?
                          Потому что он (т.е. double dispatch) эмулирует последний (multiple dispatch), используя single dispatch. выбор перегрузки осуществляется на этапе компиляции, не так ли?

                          т.о., multiple dispatch является понятием более широким, чем double dispatch.
                          visitor, в свою очередь, является лишь примером решения/реализации double dispatch. в ситуации, когда сущности и их поведение сильно сплетены, то, увы, даже большой с натяжкой будет трудно это назвать visitor'ом. называть это можно будет как угодно, но double dispatch будет.

                          p.s.
                          надеюсь, что мы не потеряли еще одну важную деталь: double dispatch оперирует с 2-мя аргументами (где 1-й сам экземпляр, например), а multiple dispatch — со всеми аргументами метода, что только подтверждает умозаключения в статье.
                            0
                            Вы все правильно пишите.

                            Мой первый комментарий
                            Приведение типов и то, что вы назвали ООП подход, по сути одно и тоже, просто if заменили на switch.

                            был о том, что то, что вы назвали «приведене типов» и «ООП подход» — одно и тоже, т.к. даже в разделе «ООП» подход вы пользуетесь приведением типов.
                            Например вот:
                            sanitizer.Cleanup((Element)node);


                            И еще раз, я всего лишь хочу указать на то, что применяя приведение типов, или используя dynamic мы лишаемся главного преимущества C# — строгой типизации.

                            И, лично для меня, «элегантность кода» важна меньше, чем строгая типизация.

                            Я считаю использование dynamic плохой практикой, т.к. ни разу не видел хорошего его применения (кроме работы с Word/Excel), а вот как dynamic используют для workaround видел и не однажды.
                              0
                              ага! поинт понятен теперь. ну да, от перестановки слагаемых сумма не меняется ;)

                              p.s.
                              наверное, имели в виду статическую типизацию, ибо тот же Python — язык динамический, но со строгой типизацией.
                                0
                                наверное, имели в виду статическую типизацию

                                Да, именно статическую типизацию, спасибо.

                Only users with full accounts can post comments. Log in, please.