В последнее время очень мне часто приходится использовать всем известный паттерн «Visitor» (он же Посетитель, далее — визитор). Раньше же я им пренебрегал, считал костылём, лишним усложнением кода. В данной статье я поделюсь своими мыслями о том, что в этом паттерне, на мой взгляд, хорошо, что плохо, какие задачи он помогает решить и как упростить его использование. Код будет на C#. Если интересно – прошу под кат.



Что это такое ?


Для начала немного вспомним, что же это за паттерн такой и для чего его используют. Тем, кто хорошо знаком с ним, могут просмотреть по диагонали. Допустим, у нас есть некоторая библиотека с иерархией геометрических фигур.



Теперь нам нужно научиться научиться вычислять их площади. Как? Да нет проблем. Добавляем метод к IFigure и реализуем. Всё здорово, разве что теперь наша библиотека зависит от библиотеки алгоритмов.

Потом нам понадобилось выводить описание каждой фигуры в консоль. А затем рисовать фигуры. Добавляя соответствующие методы, мы раздуваем нашу библиотеку, попутно жёстко нарушая SRP и OCP.

Что же делать? Конечно, в отдельных библиотеках создать классы, решающие нужные нам задачи. Как они узнают, какую конкретно фигуру им передали? Приведение типов!

public void Draw(IFigure figure)
        {
            if (figure is Rectangle)
            {
                ///////
                return;
            }
            if (figure is Triangle)
            {
                ///////
                return;
            }
            if (figure is Triangle)
            {
                ///////
                return;
            }
        }

Увидели ошибку? А я заметил её только в рантайме. Даункастинг — это всеми признаный дурной тон, путь к нарушению LSP и т. д. и т.п… Есть языки, система типов которых решает нашу задачу «из коробки» (см. мультиметоды), но C# к ним не относится.

Вот тут и приходит на помощь Визитор ака Посетитель. Суть вот в чём: есть класс – визитор, который содержит методы для работы с каждой из конкретных реализаций нашей абстракции. А каждая конкретная реализация содержит метод, который делает одну единственную вещь — передаёт себя соответствующему методу визитора.



Немного запутанно, не так ли? Вообще, один из главных недостатков визитора — то, что в него не все сходу въезжают (сужу по себе). Т.е. его использование несколько повышает порог сложности вашей системы.

Что же получилось? Как видите, вся логика находится вне наших геометрических фигур, а в визиторах. Никакого приведения типов в рантайме — выбор метода для каждой фигуры определяется при компиляции. Проблемы, с которыми мы столкнулись чуть ранее, удалось обойти. Казалось бы, всё замечательно? Конечно же нет. Недостатки имеются, но о них — в самом конце.

Варианты приготовления


Значение какого типа должны возвращать методы Visit и AcceptVisitor? В классическом варианте они void. Как быть в случае расчёта площади? Можно завести в визиторе свойство и присваивать ему значение, а после вызова Visit его читать. Но гораздо удобнее, чтобы метод AcceptVisitor сразу возвращал результат. В нашем случае тип результата – double, но очевидно что это не всегда так. Сделаем визитор и метод AcceptVisitor дженериками.

public interface IFigure
    {
        T AcceptVisitor<T>(IFiguresVisitor<T> visitor);
    }

public interface IFiguresVisitor<out T>
    {
        T Visit(Rectangle rectangle);
        T Visit(Triangle triangle);
        T Visit(Circle circle);
    }

Такой интерфейс можно использовать во всех кейсах. Для асинхронных операций типом результата будет Task. Если ничего не нужно возвращать, то возвращаемым типом может быть тип-пустышка, известный в функциональных языках как Unit. В C# он тоже определён в некоторых библиотеках, например, в Reactive Extensions.

Бывают ситуации, когда, в зависимости от типа объекта нам нужно выполнить какое-то тривиальное действие, да всего в одном месте программы. Например, на практике выводить название фигуры вряд ли нам где-то понадобится, кроме как в тестовом примере. Или в каком-нибудь юнит-тесте надо определить, что фигура – окружность либо прямоугольник. Что же, для каждого такого примитивного случая создавать новую сущность – специализированный визитор? Можно поступить по-другому:

public class FiguresVisitor<T> : IFiguresVisitor<T>
    {
        private readonly Func<Circle, T> _ifCircle;
        private readonly Func<Rectangle, T> _ifRectangle;
        private readonly Func<Triangle, T> _ifTriangle;

        public FiguresVisitor(Func<Rectangle, T> ifRectangle, Func<Triangle, T> ifTrian-gle, Func<Circle, T> ifCircle)
        {
            _ifRectangle = ifRectangle;
            _ifTriangle = ifTriangle;
            _ifCircle = ifCircle;
        }

        public T Visit(Rectangle rectangle) => _ifRectangle(rectangle);

        public T Visit(Triangle triangle) => _ifTriangle(triangle);

        public T Visit(Circle circle) => _ifCircle(circle);
    } 

public double CalcArea(IFigure figure)
        {
            var visitor = new FiguresVisitor<double>(
                r => r.Height * r.Width,
                t =>
                {
                    var p = (t.A + t.B + t.C) / 2;
                    return Math.Sqrt(p * (p - t.A) * (p - t.B) * (p - t.C));
                },
                c => Math.PI * c.Radius * c.Radius);

            return figure.AcceptVisitor(visitor);
        }

Как видите, получилось нечто, напоминающее паттерн-матчинг. Не тот, который добавили в C# 7 и который, по сути, лишь припудренный даункастинг, а типизированный и контролируемый компилятором.

А что, если у нас с десяток фигур, и нам нужно лишь для одной или двух выполнить нечто-особенное, а для остальных – какое-то действие «по умолчанию»? Копипастить в конструктор десяток одинаковых выражений – лениво и некрасиво. Как насчёт такого синтаксиса?

string description = figure
                .IfRectangle(r => $"Rectangle with area={r.Height * r.Width}")
                .Else(() => "Not rectangle");

bool isCircle = figure
                .IfCircle(_=>true)
                .Else(() => false);

В последнем примере получился настоящий аналог оператора «is»! Реализация данной фабрики для нашего набора фигур, как все остальные исходники — на гитхабе. Напрашивается вопрос – что же, для каждого случая писать этот бойлерплейт? Да. Или можно, вооружившись T4 и Roslyn, написать кодогенератор. Признаться, к моменту публикации статьи я планировал это сделать, но времени в обрез — не успел.

Недостатки


Конечно, визитор имеет достаточно недостатков и ограничений в применении. Взять хотя бы метод AcceptVisitor у IFifgure. Какое отношение он имеет к геометрии? Да никакого. Так что опять имеем нарушение SRP.

Далее, взглянем на схему ещё раз.



Мы видим замкнутую систему, где все знают обо всех. Каждый тип иерархии знает о визиторе – визитор знает обо всех типах – следовательно, каждый тип транзитивно знает обо всех других! Добавление нового типа ( фигуры в нашем примере ) фактически затрагивает всех. А это – снова прямое нарушение ранее упомянутого Open Close Principle. Если мы имеем возможность менять код, то в этом даже есть существенный плюс – если мы добавим новую фигуру, компилятор заставит нас добавить соответствующий метод в интерфейс визитора и его реализации – мы ничего не забудем. Но как быть, если мы только пользователи библиотеки, а не авторы, и не можем менять иерархию? Никак. Расширить чужую структуру с визитором мы никак не можем. Не зря во всех определениях паттерна пишут, что его применяют при наличии устоявшейся иерархии. Таким образом, если мы проектируем расширяемую библиотеку геометрических фигур, использовать визитор мы никак не можем.

Итого


Паттерн «Visitor» очень удобен, когда мы имеем возможность вносить изменения в его код. Он позволяет уйти от даункастинга, его «нерасширяемость» позволяет компилятору следить, чтобы вы добавили все обработчики для всех свежедобавленных типов.

В случае, если мы пишем библиотеку, которую можно расширять, добавляя новые типы, то визитор использовать не получится. А что тогда? Да всё тот же даункастинг, завёрнутый в паттрн-матчинг в C# 7. Или придумать что-нибудь поинтереснее. Если получится – постараюсь написать и об этом.

И, конечно, буду рад почитать мнения и идеи в камментах.
Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (46)


  1. MonkAlex
    30.06.2017 18:21
    +2

    Я может чего то не понял, но чем плох самый первый предложенный вариант — когда объект сам решает, как ему считать свою площадь, рисоваться и прочая прочая?
    По моему именно такая реализация и ожидается от ООП модели.


    1. IL_Agent
      30.06.2017 19:10
      -4

      И каждый раз, когда понадобится новый функционал, добавлять методы? Это нарушение ocp. А если потом нам фигуры понадобятся в проекте, где не нужны добавленные нами возможности?


      1. questor
        30.06.2017 19:34

        Написать экстеншены — плохой вариант?


        1. IL_Agent
          30.06.2017 19:49

          Экстеншны к чему? У нас на входе — IFigure, конкретный тип неизвестен. Можно только написать экстеншн к IFigure, но как внутри него мы определим, какая именно фигура у нас?


      1. MonkAlex
        30.06.2017 23:08
        +5

        А потом понадобятся в другом проекте — глупая отмазка.
        Вы либо пишете либу уровня фреймворка, где у вас есть четко определенные цели и задачи и вы их закрываете, либо пишете свой проект, где реализация будет соответствовать требованиям.

        Пытаться совместить эти две вещи, усложняя реализацию — плохая идея. И да, визитор в данном случае — бессмысленное усложнение, код стал сложнее, пользы — не заметно.


        1. wlbm_onizuka
          30.06.2017 23:14
          +1

          Никто не говорит о преждевременной оптимизации. Здесь на нарочно упрощенном примере показано как сделать расширение. Предполагается что в реальной жизни вам это уже понадобилось, и теперь вы знаете как это сделать.


        1. IL_Agent
          30.06.2017 23:22
          -1

          Ок, как скажите. Я же продолжаю считать, что надо всегда стремиться писать переиспользуемый код, и что solid — это полезные рекомендации.


      1. Guderian
        01.07.2017 12:41
        +1

        Это нарушение ocp.

        Вы же сами пожертвовали OCP, заметив, что расширения через новые типы фигуры можно и не ждать. А в отличие от абстрактного «нового функционала» это — наиболее вероятный кейс.

        Лично я, в данной конкретной задаче, не вижу ни одного преимущества visitor'а в сравнении с добавлением некоторого SRP-интерфейса ISquareCalculator, который может как быть частью самого класса, так и существовать отдельно.

        В случае, если мы пишем библиотеку, которую можно расширять, добавляя новые типы, то визитор использовать не получится. А что тогда? Да всё тот же даункастинг, завёрнутый в паттрн-матчинг в C# 7. Или придумать что-нибудь поинтереснее. Если получится – постараюсь написать и об этом.

        IoC-контейнер + interface ISquareCalculator<T> where T: IFigure. Получается рафинированный SOLID без единого нарушения.


        1. Bonart
          01.07.2017 14:20

          Только надо ISquareCalculator<in T> where T: IFigure — иначе ISquareCalculator<T> не будет совместим с иначе ISquareCalculator&lFigure>
          Вариантность интерфейсов решает вопрос заведомо лучше, чем сам паттерн.


        1. IL_Agent
          01.07.2017 18:14

          Вы же сами пожертвовали OCP, заметив, что расширения через новые типы фигуры можно и не ждать.

          OCP нарушается, но если у вас есть возможность править визитор, то, благодарая этому нарушению, при добавлении новой фигуры вы не забудете написать методы для её обработки. Если же не можем править — то да, решение становится нерасширяемым в направлении новых фигур.
          А в отличие от абстрактного «нового функционала» это — наиболее вероятный кейс.

          Не факт, зависит от задачи. Если так — то можно модель «перевернуть» — сделать визиторами фигуры )).
          IoC-контейнер + interface ISquareCalculator where T: IFigure. Получается рафинированный SOLID без единого нарушения.

          Как вы будете резолвить ваш калькулятор, не зная T?


          1. Guderian
            02.07.2017 08:58

            Не факт, зависит от задачи. Если так — то можно модель «перевернуть» — сделать визиторами фигуры )).

            Речь идёт о конкретной задаче. Можно, конечно, многое перевернуть с ног на голову, но это только лишний раз доказывает, что пример выбран неудачный)) Преимущества Visitor из него не видны.

            Как вы будете резолвить ваш калькулятор, не зная T?

            У любого экземпляра фигуры есть метод GetType().


  1. dimaaan
    30.06.2017 18:28

    Допустим, у вас реализована эта модель.
    Далее понадобилась новая функциональность: считать площадь в определенных единицах измерения на выбор.
    Придется добавлять в методы IFiguresVisitor'а новый параметр, который будет абсолютно лишним для WriteToConsoleVisitor и DrawVisitor?


    1. IL_Agent
      30.06.2017 19:00
      +1

      Нет конечно. Как вариант, в конструктор визитора передавать единицы измерения. Или на кажый вариант — свой визитор.


  1. copal
    30.06.2017 20:11

    В программе, сама геометрическая фигура, будет выражена исключительно value object.
    Если мне нужно работать с прямоугольником в калькуляторе, то я напишу класс Rectangle, который будет находится в пространстве имен geom и именно он будет работать с тем самым vo. Если мне нужно нарисовать прямоугольник, то я создам класс Rectangle в пространстве имен shape и этот класс будет производить расчеты с помощью класса rectangle из пространства имен geom. Разве нет? Мне вообще кажется, что применять визитер уместно только в структурной архитектуре, как например, деревья, как например рендер или физичиские движки, да и то в самых низкоуровневых реализациях.


    1. IL_Agent
      30.06.2017 20:26

      Лично мне не очень понятно, зачем создавать разные прямоугольники с разными возможностями, когда можно описать отдельно прямоугольник и отдельно — возможности. Но даже если и так — как вы узнаете, что надо создать именно прямоугольник из geom или shape, если на входе — абстрактная IFigure?


      1. copal
        30.06.2017 20:47
        +1

        интерфейс это гарантия наличия api, либо выполнения контракта. а абстракция, это абстракция, которая выражается ключевым словом abstract. То есть для меня словосочетание абстрактная ЯФигура, немного дико.
        и прямоугольник один, он выражен членами структуры, которые описывают информационную модель конкретной фигуры. два одноименных класса в разных пространствах это модели содержащие логику. одна считает, другая рисует. Поэтому мне не совсем понятно как в виде класса содержащего Rectangle.getArea может быть совместима с интерфейсом Rectangle.draw. Ну а так, если это дерево, то визитер, а если что-то другое, то тоже, что-то другое.


        1. IL_Agent
          30.06.2017 22:52
          -1

          интерфейс это гарантия наличия api, либо выполнения контракта. а абстракция, это абстракция, которая выражается ключевым словом abstract. То есть для меня словосочетание абстрактная ЯФигура, немного дико.

          Не очень понял вашу мысль. У нас есть контракт IFigure, который выполняют все фигуры. Абстрактная фигура — другими словами. Что значит «гарантия наличия апи» и зачем тут абстрактный класс — не понятно.

          Поэтому мне не совсем понятно как в виде класса содержащего Rectangle.getArea может быть совместима с интерфейсом Rectangle.draw.

          Тут тоже не понял, о чем вы.
          Впрочем, видение — оно у всех разное. Я лишь описал свои мысли, и не навязывают их. Вы, видимо, по-другому смотрите на эту тему.


      1. copal
        30.06.2017 21:19

        Но все дело в том, что у нас в голове находится в данный момент. Вы объясняете на примере рисования фигур, каким замечательным является паттерн визитер. Я говорю что он замечательный, когда работает с деревом.
        Вот представьте что Вам нужно в программе напрямую работать с прямоугольником — производить расчеты сторон, площади. А вместо этого Вам дадут в руки голую модель и щепотку алгоритмов. Это немного странно.
        но если Вы создаете что-то крутое, как например физический движок или библиотеку рендера в играх, то это самое то.

        А статья классная, кратко, но очень информативно.


        1. IL_Agent
          30.06.2017 22:56

          Конечно, модель должна зависеть от задачи и от выбранного подхода к её решению.

          Спасибо, приятно!


  1. sergey-b
    30.06.2017 22:49
    +2

    Паттерн Visitor позволяет сделать универсальный обход структуры. Он становится полезен, когда мы имеем не один объект из семейства, а структуру из множества объектов. Например, внутри треугольника могут лежать квадраты, в которых есть другие треугольники. А у вас Visitor знает только то, как работать с одним треугольником и с одним квадратом. То, как они друг в друга входят, посетитель не знает. Как только Visitor обработал одну фигуру, управление передается в метод accept, а там уже наш Visitor получит для обработки следующую фигуру.

    Основная идея данного приема — избавиться от god-метода со свитчом:

    switch (figure.type) {
        case Triangle:
            visitTriangle(figure);
            break;
        case Circle:
            visitCircle(figure);
            break;
        case Rectangle(figure);
            visitRectangle(figure);
            break;
    }
    


    Если figure — простая фигура, то этот switch можно расширять до бесконечности и особых проблем не заметить. Но если figure состоит из других фигур, то внутри методов visitRectangle, visitCircle, visitTriangle придется сделать отдельные свитчи, и вот тут-то уже возникают реальные сложности, когда можно в 3 объектах запутаться. И тогда на помощь приходит Visitor.


    1. sergey-b
      30.06.2017 23:40
      +1

      Пример из жизни

      OutlookVisitor
      public interface OutlookVisitor {
      
          void visitFolder(FolderElement folder);
      
          void visitAppointment(ItemElement<Appointment> appointment);
      
          void visitCalendarMessage(ItemElement<CalendarMessage> calendarMessage);
      
          void visitReportMessage(ItemElement<ReportMessage> reportMessage);
      
          void visitTaskRequest(ItemElement<TaskRequest> taskRequest);
      
          void visitMessage(ItemElement<Message> message);
      
          void visitItem(ItemElement<Item> item);
      
          void visitTask(ItemElement<Task> task);
      
          void visitPost(ItemElement<Post> post);
      
          void visitNote(ItemElement<Note> note);
      
          void visitContact(ItemElement<Contact> contact);
      
          void visitDistributionList(ItemElement<DistributionList> distributionList);
      
          void visitDocument(ItemElement<Document> document);
      
          void visitJournal(ItemElement<Journal> journal);
      
      }
      


    1. TSR2013
      01.07.2017 00:31

      Мне всегда казалось, что большой switch надо разруливать паттерном Commander


  1. timramone
    30.06.2017 23:04
    +1

    Visitor — хороший паттерн, но проблемы начинаются, когда нужно расширять количество обходимых классов в другой сборке, отличной от сборки, в которой определен сам Visitor. Тогда в этих наследниках приходится ожидать один тип Visitor'а и делать cast к наследнику, определенному в этой сборке, что не очень приятно. В такой ситуации уже удобнее бывает просто сделать Dictioary<Type, Action>. С таким подходом потеряем в производительности и возможно в читабельности (хотя и не факт, если такой подход используется часто в кодовой базе), зато получим больше гибкости.


    1. IL_Agent
      30.06.2017 23:09

      Visitor — хороший паттерн, но проблемы начинаются, когда нужно расширять количество обходимых классов в другой сборке, отличной от сборки, в которой определен сам Visitor.

      Спасибо, я как раз про это написал в разделе «недостатки», в конце.


      1. Deosis
        03.07.2017 08:36

        В C# реально сделать визитор для классов из разных сборок.


        +    public interface IVisitorItem<in TV>
        +    {
        +        void Accept(TV visitor);
        +    }
        +
        +    public interface IVisitorBase<in TI>
        +    {
        +        void Visit(TI item);
        +    }
        
        public class Rectangle : IVisitorItem<IVisitorBase<Rectangle>>
        {
            public void Accept(IVisitorBase<Rectangle> visitor) => visitor.Visit(this);
        }
        
        public class ConsoleWriter
            : IVisitorBase<Rectangle>
            , IVisitorBase<Circle>
        {
            public void Visit(Rectangle item) => Console.WriteLine("Rectangle");
            public void Visit(Circle item) => Console.WriteLine("Circle");
        }
        public void Test(List<IVisitorItem<ConsoleWriter>> list)
            => list.ForEach(_ => _.Accept(ConsoleWriter.Instance));

        Классы круга и прямоугольника могут не знать друг о друге и о конкретном визиторе.
        Работает за счет внутренних механизмов CLR
        Можно написать обертку:


        +    public class VisitorTargret<T>
        +        : IVisitorItem<IVisitorBase<T>>
        +    {
        +        private readonly T data;
        +
        +        public VisitorTargret(T data)
        +        {
        +            this.data = data;
        +        }
        +
        +        #region Implementation of IVisitorItem<in IVisitorBase<T>>
        +
        +        public void Accept(IVisitorBase<T> visitor)
        +        {
        +            visitor.Visit(data);
        +        }
        +
        +        #endregion
        +    }


        1. IL_Agent
          03.07.2017 13:31

          Как будет выглядеть интерфейс IFigure?


          1. Deosis
            04.07.2017 08:31

            Этого интерфейса вообще не будет. Вместо него будет использоваться


            IVisitorItem<IFiguresVisitor> item;

            Общий посетитель собирается из базовых:


            public interface IFiguresVisitor
                : IVisitorBase<Rectangle>
                , IVisitorBase<Circle>
            {}

            Прямоугольник — цель посетителя прямоугольника.
            За счет контрвариации: прямоугольник — цель посетителя фигуры


  1. Trixon
    01.07.2017 09:44

    Не вижу проблем переориентировать код на использование статического листа или словаря с возможностью добавлять в него поддержку сторонних типов и визиторов.


  1. Nakosika
    01.07.2017 09:55

    HashMap<Class, Function> dispatch;


    Вот вам и весь визитор.


  1. reforms
    01.07.2017 13:50
    +1

    Спасибо за статью.
    Хоть выше уже многие и написали, скажу ещё раз:
    1) Ваши примеры не совсем подходят к патерну visitor, так как последний чаще используют для обхода структур, имеющие вложенные структуры, те в свою очередь содержат ещё структуры и т.д.
    2) Конкретно в Вашем случае, уместны 2 решения — (1) или фигуры содержат методы для их отрисовки, вывода на консоль и расчёт площади в виде реализаций т.е. по 3 метода в каждой из фигур, что иногда позволяет сокрыть данные внутри фигуры не давая к ним доступ даже на чтение (2) или добавить тип фигуры а по этому типу делать связывание с нужным действием, причём замечу, что реализации действий могут быть сделаны по одному экземпляру, быть потокобезопасными и определять функциональные слои (слой отрисовки данных, слой описания данных, слой расчета площади и иных метрик фигур) Вашего фреймворка/библиотеки. И (1) и (2) делают Ваш Фреймворк расширяемым, гибким и предсказуемым, 2ой вариант ещё легко позволит определять действие по умолчанию для конкретного слоя — что значит не обязательность в реализации всех функциональных слоёв для новой фигуры, хотя этот факт может быть и минусом.


    1. IL_Agent
      01.07.2017 18:30

      Спасибо.
      1. По-моему, здесь нет связи: обход структуры — это одна задача, обработка элемента — другая. Визитор — это выбор обработки конкретного типа элемента.
      2. Первый вариант мне не нравится, т.к. фигуры получаются жирными и не переиспользуемыми в других моделях. Но этот вариант имеет полное право на жизнь и может быть вполне уместен.
      Второй вариант — тип у фигуры и так есть, его можно получить методом GetType :). Т.е. это всё тот же даункастинг. Главный недостаток даункастинга, а также словарей, рефлексии, о которых здесь многие пишут — это то, что мы выбираем метод «руками» в рантайме, что чревато ошибками. К этому следует прибегать, только когда по-другому нельзя. Визитор — это статика и проверка при компиляции.


  1. franzose
    01.07.2017 15:13

    А почему бы вместо Visitor не сделать отдельный интерфейс DrawableShape и классы, его реализующие. Например так:


    public interface DrawableShape
    {
        public void draw();
    }
    
    public class DrawableRectangle : DrawableShape
    {
        public DrawableRectangle(Rectangle rectangle)
        {
            this.rectangle = rectangle;
        }
    
        public void draw()
        {
             // тут как-то рисуем
        }
    }


    1. ATOMOHOD
      01.07.2017 15:52

      "предпочитайте композицию наследованию" :)


      1. franzose
        02.07.2017 14:53

        Так это и есть композиция)


    1. Nerlin
      01.07.2017 17:59
      +1

      В таком случае у Вас все равно придется написать код, который будет сопоставлять Вашу вторую иерархию отрисовки с основной иерархией, что приводит к той же проблеме.


  1. AxisPod
    01.07.2017 18:12

    А со временем это разрастается в такое, что поддерживать становится невозможно. Особенно учитывая кучи разработчиков, что засунут свои грязные ручки в данный код. Код будет раскидан по куче мест. Отпинываться от ООП ради спорных идиологий и терять ресурсы на поддержке, это всё же пахнет глупым фанатизмом.


    1. IL_Agent
      01.07.2017 18:32

      Где же вы увидели отпинывание от ООП? И какую модель, на ваш взгляд, проще поддерживать?


    1. sashagil
      01.07.2017 22:41
      +2

      Предположу, что под «отпиныванием от ООП» вы имели в виду сам факт, что описываемый паттерн предлагает альтернативу пополнению базового интерфейса IFigure функциями на все случаи жизни (что более «естественно» и, действительно, в ряде ситуаций является лучшей альтернативой).

      Представим таблицу, строки которой соответствуют конкретным классам, реализующим IFigure, а столбцы — различным действиям, которые коду нужно выполнять с этими объектами.

      Если столбцов мало, и они редко добавляются / меняются, а строк много, и они чаще добавляются / меняются, в линейном программном коде удобно группировать строки (реализовывать «естественный» подход, описанный выше). Полное описание строки — класса, реализующего IFigure, расположено в одном файле, где рядом сидит и рисование данной фигуры, и вывод на консоль, и вычисление площади… Немного разношёрстно, но зато — вот всё про эту фигуру, в одном месте! Правда, в случае с деревьями вложенных объектов логика обхода дерева объектов может оказаться многократно продублированной (для разных методов IFigure), но это не страшно, пока IFigure остаётся достаточно компактным и стабильным интерфейсом (т.е., повторяясь, когда в воображаемой таблице набор столбцов не слишком велик и достаточно стабилен).

      Однако, если столбцов становится много, и они добавляются / меняются часто в сравнении с устоявшимся набором строк, группировка кода по столбцам становится более рациональным выбором. Визитор — хорошо известная и широко применяемая модель реализации этого выбора. Я встречаю успешное применение этой модели в работе, например, с синтаксическими деревьями или с деревьями вложенных объектов в графических диаграммах. Я встречал также неоднократно ситуации с безобразно разросшимися базовыми интерфейсами в отсутствии решения применить визитор.

      То, что при первой встрече паттерн Визитор выглядит переусложнённым, неудивительно — структура классических ООП языков подталкивает к группировке по строкам, она как бы встраивается в арсенал программиста с первых шагов изучения ООП. Паттерны же вроде Визитора были изобретены и пере-изобретены для улучшения структуры кода в ситуациях, когда решения в лоб приводят к накапливающимся проблемам («разрастается в такое, что поддерживать становится невозможно»). Да, и, кстати, группировку по столбцам можно организовать и по-другому, в конкретной ситуации применение Визитора может быть, действительно, не самой удачной идеей. Тем не менее, последняя фраза вашего комментария («Отпинываться от ООП ради спорных идиологий и терять ресурсы на поддержке, это всё же пахнет глупым фанатизмом.») мне не кажется обоснованной без рассмотрения конкретных ситуаций. Повторюсь, имел дело с двумя широкими областями (синтаксический разбор, деловая графика) где использование Визитора вполне уместно и де-факто является стандартной для этих областей практикой.


      1. IL_Agent
        02.07.2017 19:37

        Спасибо за развёрнутый комментарий.

        Предположу, что под «отпиныванием от ООП» вы имели в виду сам факт, что описываемый паттерн предлагает альтернативу пополнению базового интерфейса IFigure функциями на все случаи жизни (что более «естественно» и, действительно, в ряде ситуаций является лучшей альтернативой).

        Да, я тоже полагаю, что речь об этом. Но визитор никак не выходит за рамки ООП, поэтому никакого «отпинывания» тут нет.
        , в конкретной ситуации применение Визитора может быть, действительно, не самой удачной идеей.

        Мой пример про 3 фигуры — упрощённый. Я не призываю в первой же лабораторке по полиморфизму прикручиваться визитор ))


        1. sashagil
          02.07.2017 22:38

          Спасибо за статью! У меня ещё два пункта для обсуждения, пользуюсь случаем:

          1. Вы написали, что для генерации обвязки думали взять Roslyn — но не найдётся ли способов проще, например, основанных на атрибутах, использовании рефлексии, code rewrite? [ Я — C++ программист, c C# знаком неплохо, но не на экспертном уровне. Думаю полистать книгу «Metaprogramming in .Net» (2013). Использую в работе генерацию кода в небольшом DSL-фреймворке, созданном коллегами, там для фронтенда (построения AST) используется Antlr, для шаблонов генерации — T4; DSL-языки похожи синтаксисом на C#. Я думаю об освоении других фронтенд-технологий, и использование структуры Roslyn-а — один из вариантов (им пользуется, например, разработчик HlslTools для DSL «High Level Shading Language», он перешёл на такую структуру с Antlr-а, чтобы улучшить скорость разбора и восстановление при синтаксических ошибках). Есть ещё работоспособные альтернативы для фронтенда, но более экзотические (у меня на примете их примерно 3-4). Короче, меня эта тема довольно-таки занимает, пользуюсь случаем попробовать обменяться мнениями. ]

          2. В статье и комментариях затрагивается проблема с расширяемостью системы при невозможности обновления исходной сборки библиотеки, в которой определены конкретные классы (или, скорее, специализирующие производные интерфейсы), наследующие IFigure. Мне кажется, если эту проблему предвидеть в дизайне библиотеки, она может решаться, хотя бы частично; детально я идею в этом направлении не обдумал ещё, правда. Автор библиотеки для того, чтобы допустить возможность расширения, оставляет один интерфейс-наследник IFigure (специально для этого предназначенный и названный, скажем, IOtherFugure) незапечатанным. Для пользователей библиотеки, или авторов новых версии библиотеки, исходный (v1) набор фигур не меняется, обслуживающие их визиторы стабильны и не нуждаются в изменениях при переходе на v2 с новыми фигурами — но нуждаются в подвеске дополнительных визиторов для новых фигур, представленных интерфейсами, наследующими от IOtherFugure. Для этого семейства в v2 создаётся новый интерфейс визиторов. Результирующая картина смеси v1 и v2 выглядит причудливо, но даёт возможность сохранить код, написанный для v1, без модификаций. Параллельно v2 предоставляет новый свежий «100% чистый v2» API без разделения на фигуры v1 / новые фигуры v2, чтобы при свежем старте на v2 или возможности переписать v1-зависимый код можно было бы иметь чистую картину, без кудрявостей. Мне кажется, такой подход может работать в реальных проектах, хотя он и «сложный».


          1. IL_Agent
            03.07.2017 18:59

            1. Мне особо и не приходилось сталкиваться с кодогенерацией прежде, поэтому ничего интересного рассказать не могу. Roslyn — первое, что приходит в голову, т.к. очень популярен сейчас. Нашёл вот такой пример https://daveaglick.com/posts/compiler-platform-in-t4, из него можно идею стянуть.

            2. Мы получим метод Visit(IOtherFigure otherFigure). И эту самую otherFigure придётся кастить руками к чему-то. Так ведь?


            1. sashagil
              03.07.2017 23:58

              1. Спасибо за ссылку, посмотрел и буду иметь в виду.
              2. Да, нужно будет кастить. Я покрутил в голове, и решил эту тему бросить — не стоит усилий в данный момент.

              Спасибо за обсуждение!


  1. Legion21
    01.07.2017 20:40

    Много кода как-то, а что если у вас будет до 1000 разных геометрических объектов? Ад же…


  1. Ryppka
    02.07.2017 14:31

    Если программисту доступен multiple dispatch, то зачем ему визитор? А если программист не понимает, что multiple dispatch иногда надобится — зачем такой программист? )))


  1. Dethrone
    02.07.2017 19:22

    tl;dr
    В некоторых случаях применение паттерн матчинга целесообразней/удобней применения визитора. (кто юзал хаскель или раст давно об этом знают)


    1. 0xd34df00d
      05.07.2017 23:25

      Так визитор и есть костыль эмуляция ADT и паттерн-матчинга для тех языков, где этого нет.