Comments 48
По моему именно такая реализация и ожидается от ООП модели.
Написать экстеншены — плохой вариант?
Вы либо пишете либу уровня фреймворка, где у вас есть четко определенные цели и задачи и вы их закрываете, либо пишете свой проект, где реализация будет соответствовать требованиям.
Пытаться совместить эти две вещи, усложняя реализацию — плохая идея. И да, визитор в данном случае — бессмысленное усложнение, код стал сложнее, пользы — не заметно.
Это нарушение ocp.
Вы же сами пожертвовали OCP, заметив, что расширения через новые типы фигуры можно и не ждать. А в отличие от абстрактного «нового функционала» это — наиболее вероятный кейс.
Лично я, в данной конкретной задаче, не вижу ни одного преимущества visitor'а в сравнении с добавлением некоторого SRP-интерфейса ISquareCalculator, который может как быть частью самого класса, так и существовать отдельно.
В случае, если мы пишем библиотеку, которую можно расширять, добавляя новые типы, то визитор использовать не получится. А что тогда? Да всё тот же даункастинг, завёрнутый в паттрн-матчинг в C# 7. Или придумать что-нибудь поинтереснее. Если получится – постараюсь написать и об этом.
IoC-контейнер + interface ISquareCalculator<T> where T: IFigure. Получается рафинированный SOLID без единого нарушения.
Только надо ISquareCalculator<in T> where T: IFigure — иначе ISquareCalculator<T> не будет совместим с иначе ISquareCalculator&lFigure>
Вариантность интерфейсов решает вопрос заведомо лучше, чем сам паттерн.
Вы же сами пожертвовали OCP, заметив, что расширения через новые типы фигуры можно и не ждать.
OCP нарушается, но если у вас есть возможность править визитор, то, благодарая этому нарушению, при добавлении новой фигуры вы не забудете написать методы для её обработки. Если же не можем править — то да, решение становится нерасширяемым в направлении новых фигур.
А в отличие от абстрактного «нового функционала» это — наиболее вероятный кейс.
Не факт, зависит от задачи. Если так — то можно модель «перевернуть» — сделать визиторами фигуры )).
IoC-контейнер + interface ISquareCalculator where T: IFigure. Получается рафинированный SOLID без единого нарушения.
Как вы будете резолвить ваш калькулятор, не зная T?
Не факт, зависит от задачи. Если так — то можно модель «перевернуть» — сделать визиторами фигуры )).
Речь идёт о конкретной задаче. Можно, конечно, многое перевернуть с ног на голову, но это только лишний раз доказывает, что пример выбран неудачный)) Преимущества Visitor из него не видны.
Как вы будете резолвить ваш калькулятор, не зная T?
У любого экземпляра фигуры есть метод GetType().
Допустим, у вас реализована эта модель.
Далее понадобилась новая функциональность: считать площадь в определенных единицах измерения на выбор.
Придется добавлять в методы IFiguresVisitor'а новый параметр, который будет абсолютно лишним для WriteToConsoleVisitor и DrawVisitor?
Если мне нужно работать с прямоугольником в калькуляторе, то я напишу класс Rectangle, который будет находится в пространстве имен geom и именно он будет работать с тем самым vo. Если мне нужно нарисовать прямоугольник, то я создам класс Rectangle в пространстве имен shape и этот класс будет производить расчеты с помощью класса rectangle из пространства имен geom. Разве нет? Мне вообще кажется, что применять визитер уместно только в структурной архитектуре, как например, деревья, как например рендер или физичиские движки, да и то в самых низкоуровневых реализациях.
и прямоугольник один, он выражен членами структуры, которые описывают информационную модель конкретной фигуры. два одноименных класса в разных пространствах это модели содержащие логику. одна считает, другая рисует. Поэтому мне не совсем понятно как в виде класса содержащего Rectangle.getArea может быть совместима с интерфейсом Rectangle.draw. Ну а так, если это дерево, то визитер, а если что-то другое, то тоже, что-то другое.
интерфейс это гарантия наличия api, либо выполнения контракта. а абстракция, это абстракция, которая выражается ключевым словом abstract. То есть для меня словосочетание абстрактная ЯФигура, немного дико.
Не очень понял вашу мысль. У нас есть контракт IFigure, который выполняют все фигуры. Абстрактная фигура — другими словами. Что значит «гарантия наличия апи» и зачем тут абстрактный класс — не понятно.
Поэтому мне не совсем понятно как в виде класса содержащего Rectangle.getArea может быть совместима с интерфейсом Rectangle.draw.
Тут тоже не понял, о чем вы.
Впрочем, видение — оно у всех разное. Я лишь описал свои мысли, и не навязывают их. Вы, видимо, по-другому смотрите на эту тему.
Вот представьте что Вам нужно в программе напрямую работать с прямоугольником — производить расчеты сторон, площади. А вместо этого Вам дадут в руки голую модель и щепотку алгоритмов. Это немного странно.
но если Вы создаете что-то крутое, как например физический движок или библиотеку рендера в играх, то это самое то.
А статья классная, кратко, но очень информативно.
Основная идея данного приема — избавиться от 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.
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);
}
public interface OutlookElement {
void accept(OutlookVisitor visitor) throws IOException;
}
public class ItemElement<T extends Item> implements OutlookElement {
private final Method visit;
protected final T item;
public ItemElement(T item) {
this.item = item;
Class<? extends Item> cl = item.getClass();
String methodName = "visit" + cl.getSimpleName();
try {
this.visit = OutlookVisitor.class.getMethod(methodName, ItemElement.class);
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public void accept(OutlookVisitor visitor) throws IOException {
try {
visit.invoke(visitor, this);
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new IOException(e);
}
}
public T getItem() {
return item;
}
}
public class FolderElement implements OutlookElement {
private final Folder folder;
public FolderElement(Folder folder) {
this.folder = folder;
}
@Override
public void accept(OutlookVisitor visitor) throws IOException {
int itemCount = folder.getItemCount();
List<Item> items = folder.getItems(0, itemCount);
for (Item item: items) {
OutlookElement element = new ItemElement<>(item);
element.accept(visitor);
}
List<Folder> folders = folder.getFolders();
for (Folder child: folders) {
FolderElement folderElement = new FolderElement(child);
folderElement.accept(visitor);
}
visitor.visitFolder(this);
}
public Folder getFolder() {
return folder;
}
}
Обход всего, что есть в аутлуке, сверху вниз.
Исходная модель данных ничего о визиторах не знает, поэтому вокруг ее объектов сделаны обертки с методами accept.
Чтобы не писать много однотипных методов, при помощи reflection сделан один универсальный метод accept.
Visitor работает с отдельными элементами, содержимое каталога извлекает FolderElement.
Мне всегда казалось, что большой switch надо разруливать паттерном Commander
Visitor — хороший паттерн, но проблемы начинаются, когда нужно расширять количество обходимых классов в другой сборке, отличной от сборки, в которой определен сам Visitor.
Спасибо, я как раз про это написал в разделе «недостатки», в конце.
В 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
+ }
Этого интерфейса вообще не будет. Вместо него будет использоваться
IVisitorItem<IFiguresVisitor> item;
Общий посетитель собирается из базовых:
public interface IFiguresVisitor
: IVisitorBase<Rectangle>
, IVisitorBase<Circle>
{}
Прямоугольник — цель посетителя прямоугольника.
За счет контрвариации: прямоугольник — цель посетителя фигуры
HashMap<Class, Function> dispatch;
Вот вам и весь визитор.
Спасибо за статью.
Хоть выше уже многие и написали, скажу ещё раз:
1) Ваши примеры не совсем подходят к патерну visitor, так как последний чаще используют для обхода структур, имеющие вложенные структуры, те в свою очередь содержат ещё структуры и т.д.
2) Конкретно в Вашем случае, уместны 2 решения — (1) или фигуры содержат методы для их отрисовки, вывода на консоль и расчёт площади в виде реализаций т.е. по 3 метода в каждой из фигур, что иногда позволяет сокрыть данные внутри фигуры не давая к ним доступ даже на чтение (2) или добавить тип фигуры а по этому типу делать связывание с нужным действием, причём замечу, что реализации действий могут быть сделаны по одному экземпляру, быть потокобезопасными и определять функциональные слои (слой отрисовки данных, слой описания данных, слой расчета площади и иных метрик фигур) Вашего фреймворка/библиотеки. И (1) и (2) делают Ваш Фреймворк расширяемым, гибким и предсказуемым, 2ой вариант ещё легко позволит определять действие по умолчанию для конкретного слоя — что значит не обязательность в реализации всех функциональных слоёв для новой фигуры, хотя этот факт может быть и минусом.
1. По-моему, здесь нет связи: обход структуры — это одна задача, обработка элемента — другая. Визитор — это выбор обработки конкретного типа элемента.
2. Первый вариант мне не нравится, т.к. фигуры получаются жирными и не переиспользуемыми в других моделях. Но этот вариант имеет полное право на жизнь и может быть вполне уместен.
Второй вариант — тип у фигуры и так есть, его можно получить методом GetType :). Т.е. это всё тот же даункастинг. Главный недостаток даункастинга, а также словарей, рефлексии, о которых здесь многие пишут — это то, что мы выбираем метод «руками» в рантайме, что чревато ошибками. К этому следует прибегать, только когда по-другому нельзя. Визитор — это статика и проверка при компиляции.
А почему бы вместо Visitor не сделать отдельный интерфейс DrawableShape и классы, его реализующие. Например так:
public interface DrawableShape
{
public void draw();
}
public class DrawableRectangle : DrawableShape
{
public DrawableRectangle(Rectangle rectangle)
{
this.rectangle = rectangle;
}
public void draw()
{
// тут как-то рисуем
}
}
Представим таблицу, строки которой соответствуют конкретным классам, реализующим IFigure, а столбцы — различным действиям, которые коду нужно выполнять с этими объектами.
Если столбцов мало, и они редко добавляются / меняются, а строк много, и они чаще добавляются / меняются, в линейном программном коде удобно группировать строки (реализовывать «естественный» подход, описанный выше). Полное описание строки — класса, реализующего IFigure, расположено в одном файле, где рядом сидит и рисование данной фигуры, и вывод на консоль, и вычисление площади… Немного разношёрстно, но зато — вот всё про эту фигуру, в одном месте! Правда, в случае с деревьями вложенных объектов логика обхода дерева объектов может оказаться многократно продублированной (для разных методов IFigure), но это не страшно, пока IFigure остаётся достаточно компактным и стабильным интерфейсом (т.е., повторяясь, когда в воображаемой таблице набор столбцов не слишком велик и достаточно стабилен).
Однако, если столбцов становится много, и они добавляются / меняются часто в сравнении с устоявшимся набором строк, группировка кода по столбцам становится более рациональным выбором. Визитор — хорошо известная и широко применяемая модель реализации этого выбора. Я встречаю успешное применение этой модели в работе, например, с синтаксическими деревьями или с деревьями вложенных объектов в графических диаграммах. Я встречал также неоднократно ситуации с безобразно разросшимися базовыми интерфейсами в отсутствии решения применить визитор.
То, что при первой встрече паттерн Визитор выглядит переусложнённым, неудивительно — структура классических ООП языков подталкивает к группировке по строкам, она как бы встраивается в арсенал программиста с первых шагов изучения ООП. Паттерны же вроде Визитора были изобретены и пере-изобретены для улучшения структуры кода в ситуациях, когда решения в лоб приводят к накапливающимся проблемам («разрастается в такое, что поддерживать становится невозможно»). Да, и, кстати, группировку по столбцам можно организовать и по-другому, в конкретной ситуации применение Визитора может быть, действительно, не самой удачной идеей. Тем не менее, последняя фраза вашего комментария («Отпинываться от ООП ради спорных идиологий и терять ресурсы на поддержке, это всё же пахнет глупым фанатизмом.») мне не кажется обоснованной без рассмотрения конкретных ситуаций. Повторюсь, имел дело с двумя широкими областями (синтаксический разбор, деловая графика) где использование Визитора вполне уместно и де-факто является стандартной для этих областей практикой.
Предположу, что под «отпиныванием от ООП» вы имели в виду сам факт, что описываемый паттерн предлагает альтернативу пополнению базового интерфейса IFigure функциями на все случаи жизни (что более «естественно» и, действительно, в ряде ситуаций является лучшей альтернативой).
Да, я тоже полагаю, что речь об этом. Но визитор никак не выходит за рамки ООП, поэтому никакого «отпинывания» тут нет.
, в конкретной ситуации применение Визитора может быть, действительно, не самой удачной идеей.
Мой пример про 3 фигуры — упрощённый. Я не призываю в первой же лабораторке по полиморфизму прикручиваться визитор ))
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-зависимый код можно было бы иметь чистую картину, без кудрявостей. Мне кажется, такой подход может работать в реальных проектах, хотя он и «сложный».
2. Мы получим метод Visit(IOtherFigure otherFigure). И эту самую otherFigure придётся кастить руками к чему-то. Так ведь?
В некоторых случаях применение паттерн матчинга целесообразней/удобней применения визитора. (кто юзал хаскель или раст давно об этом знают)
Некоторые мысли о паттерне Visitor