Pull to refresh

Comments 45

Где ссылка на игру? :)

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

Ссылка на игру будет в следующей статье :)
Ну, все что я увидел — это здоровая практика, убрал копи-паст, вынес его в отдельный класс. Если раньше вы писали одно и тоже в классе Creator и Destroyer, то теперь это все в UpcastVisitor, который наследуют Creator и Destroyer. Это как-то на новизну, не тянет, мне кажется это сделал бы любой здравомыслящий программист. Или я не прав?
В UpcastVisitor'е нет ничего из классов Creator и Destroyer, он никак не взаимодействует с библиотекой Box2D, там просто происходит преобразование типов. UpcastVisitor позволяет в Destroyer'e избежать перегрузки четырех методов. Вместо этого перегружается только один — для типа Box2DObject. Любой здравомыслящий программист остановился бы на том, что все эти четыре перегрузки вызывали бы один и тот же метод. Я же пошел дальше и превратил четыре перегрузки в одну.
Автор, спасибо за статью.
Все было прекрасно до момента использования UpcastVisitor. Код сильно не сокращает, но для понимания — усложняет.
Мне кажется, или Вы, убрали все методы кроме одного из Visitor'a (а их там было по количеству листьев) наплодили в upcastVisitor'e методов еще больше (метод есть для каждого класса иерархии), т.е. код Вы только увеличили (ну зато он не дублируется, да=) ). Польза будет сомнительна даже если в Visitor'e (ну Destroyer'e в данном случае), будут одинаковые методы из большого числа строк, конечно, возможно код уменьшится, но все равно для каждого класса придется писать метод в UpcastVisitor'e, а трудозатраты на копипаст одинаковы (хотя его можно было бы и автоматизировать).

Поправьте меня, если я не прав, но мне показалось, что все обстоит именно так.
Если у вас в программе большое количество visitor'ов (например 10), и половина из них будет пользоваться UpcastVisitor'ом, то кода будет меньше. Просто я пытался привести простой пример. В UpcastVisitor'e каждый метод всегда состоит всего из одной строчки, его не сложно добавить при создании нового класса.
Да, согласен… фактически он будет аккумулировать дублирующийся код.
Поздравляю вас, вы написали код с повышенной двусторонней связностью (каждый объект знает о базовом визиторе, визитор знает обо _всех_ объектах). Добавили новый объект — добро пожаловать в каждый визитор обновлять методы Visit. А главное — зачем?

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

Причем с точки зрения поддержки наиболее выгодным оказывается писать визиторы, которые содержат внутри себя минимум логики (в особенности объектоспецифичной), а саму эту логику переносить обратно в посещаемый объект.
Базовый Visitor всегда знает о всех классах иерархии. По крайней мере о всех листьях. Такой уж это паттерн. Традиционное применение — разделение объектов и алгоритмов для работы с ними. Переносить логику обратно в посещаемый объект противоречит самой идее паттерна.
Обход своих детей — это дополнительная фича, которой, наверное, часто пользуются. Но эта фича никак не противоречит и не мешает работе UpcastVisitor'а. Например, если бы дерево хранило все ветки:
class Tree
{
public:
    virtual void accept(Visitor& visitor)
    {
        for (int i = 0; i < m_branches.size(); ++i)
        {
            m_branches[i].accept(visitor);
        }
        visitor.visit(*this);
    }
    //...
};

Все прекрасно работает, UpcastVisitor посещает все объекты, как и планировалось.
«Базовый Visitor всегда знает о всех классах иерархии. По крайней мере о всех листьях.»
Вот это и создает геморрой при появлении новых классов иерархии. Редкостный.

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

Учитывая редкостную непрозрачность паттерна, я без самой крайней необходимости его не использую и всячески избегаю.
При появлении новых классов нужно добавить один метод в самый базовый Visitor, с пустой реализацией. Разве это сложно?

Обычно при наличии visitor'ов из классов иерархии выносится большая часть алгоритмов, в этом нет ничего необычного, затем он и нужен, этот паттерн. Да, это вносит некоторую сложность, зато с лихвой окупается гибкостью.
«Разве это сложно?»
Это просто. Только после этого надо _не забыть_ — и это никак не лечится компилятором — добавить оверрайды там, где это нужно. Собственно, отсутствие поддержки компилятором — тоже недостаток этого паттерна.

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

«Да, это вносит некоторую сложность, зато с лихвой окупается гибкостью. „
“Don't write smart code». Визитор — классический пример смарткода. Я сторонник простых решений, пусть даже для этого придется пожертвовать какой-то (чаще всего оказывающейся ненужной) гибкостью, потому что поддерживать это потом обычным людям. Паттерн «визитер» в поддержке очень дорог, это проверено личным опытом.

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

В вашем случае визитор вообще не нужен по одной простой причине: для того, чтобы нарисовать все объекты в игровом мире, вам достаточно последовательно вызывать painter.visit(*it). В коде _вообще_ ничего не изменится, а вот читаемость мгновенно вырастет.
Если вы так напишете, то все перестанет работать, потому что именно из метода accept визитор получает информацию о типе объекта. А если написать painter.visit(*it), то будет вызываться заглушка из базового класса Visitor::visit(Object& object) и ничего не нарисуется.
А вот это свойство не паттерна, а конкретного языка, где один указатель разыменовывается одним образом, а другой — другим. В других языках это будет работать без костылей, и получится, что ваш паттерн не нужен. Учитывая, что паттерн всегда выше языка, это означает, что паттерн применен не по делу (а для «умного» обхода ограничений языка).

Повторюсь, существенная часть паттерна visitor (и единственная, оправдывающая его существование) — это операции с объектными структурами (а не просто выбор нужного оверлоада по типу), где только конкретный объект знает свою структуру.
В С++ указатели всегда разыменовываются одним образом. В Java и C# нет указателей, но ситуация там аналогичная и паттерн visitor применяется точно так же. О каких костылях и каких языках идет речь?

Я с вами не согласен, в первую очередь паттерн visitor используется для отделения структуры объектов от алгоритмов, работающих с ней, позволяя добавлять новые методы для работы с классами без модификации этих классов, а так же изменять существующие алгоритмы в одном месте, а не в каждом отдельном классе.
«В Java и C# нет указателей, но ситуация там аналогичная и паттерн visitor применяется точно так же. „
В java и c# есть рефлексия, и там можно обойтись без double dispatch.

“в первую очередь паттерн visitor используется для отделения структуры объектов от алгоритмов, работающих с ней»
Слово «структура» в этом предложении вам ни о чем не говорит?
Хотел уточнить у вас какой подход вы практикуете для работы с объектами разных классов с разными интерфейсами?
Например, у нас есть сервис, который получает команды от пользователя и отдает ему ответ. Притом сами запросы и ответы статичны и определены в доменной модели, меняется всего лишь транспорт доставки ответа клиенту. Хотелось бы чтобы доменная модель при возврате ответа играла роль Producer, а конкретный транспорт Consumer. И хотелось бы чтобы Producer генерировал «рабочие элементы» в каком-то обобщенном виде. В таком случае Consumer видит только некий базовый класс/интерфейс ClientResponseBase. И хотелось бы не смешивать иерархию ClientResponseBase с конкретными транспортами. И Vizitor имеет свои плюсы, есть возможность запихнуть всю логику упаковки всех вариантов ответа для одного конкретного транспорта в один класс и она больше не всплывет нигде.
Если не сложно поделитесь опытом решения схожих задач.
Притом сами запросы и ответы статичны и определены в доменной модели, меняется всего лишь транспорт доставки ответа клиенту. Хотелось бы чтобы доменная модель при возврате ответа играла роль Producer, а конкретный транспорт Consumer. И хотелось бы чтобы Producer генерировал «рабочие элементы» в каком-то обобщенном виде.

Все придумано до нас в WCF и/или WebAPI (и даже еще раньше, когда придумывали XmlSerializer и ISerializable). Такие задачи традиционно решаются с помощью конвенций по форматированию DTO (не доменной модели, как у вас, использовать доменную модель в качестве DTO опасно) + разметки атрибутами, если конвенций не хватает. Дальше отдельный уровень (форматер), опираясь на эти конвенции, преобразует DTO в транспортный слой и обратно.
Может быть вы меня не так поняли, я имел ввиду что когда мы пытаемся упаковать в конкретный транспорт разные типы сообщений(которые приходят в виде обобщенного класса/интерфейса) я не вижу способа избежать typeof. А если пытаться впихнуть детали транспорта пусть даже в DTO, то во-первых, специфичный код транспорта начинает размазываться, а во вторых с увеличение числа различных транспортов эти DTO начинают превращаться в свалку.
Мне как-то не очень нравится вариант с внесением специфичной для транспорта логики в DTO и для меня остается вариант с typeof, что мне очень не нравится.
Double dispatch в лице Visitor позволяет решить проблему typeof и загрязнения DTO деталями транспорта. Вся транспорто-специфичная логика уходит в Visitor.
То, что вы описываете — это совершенно типовая задача, многократно решенная во всех системах удаленного взаимодействия.

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

В подавляющем большинстве случаев сериализация сводится к тому, чтобы получить коллекцию туплов имя-тип-значение, которая потом уже приводится форматтером к конкретному виду, нужному для передачи. Для того, чтобы получить такую коллекцию, визитор избыточен — достаточно метаданных объекта (если платформа их поддерживает) или простейшего интерфеса (если метаданных нет или недостаточно). Как пример, в .net все это реализовано как минимум трижды (в Serializable, DataContract и XmlSerializer), и никакого визитора не требует.

Паттерн интересный. Но при большой иерархии классов можно будет элементарно запутаться, особенно если к программе придется вернуться через какое-то время.
И UpcastVisitor нам жизнь не облегчил — код-то все равно дублируется.
Извините, не совсем вас понял. Где дублируется код?
К слову, это один из примеров static_cast'а не к месту, было бы достаточно implicit cast в духе
visit(Box2DObject& object) {
Object& o = object;
visit(o);
}

Естественно, при желании оно заворачивается в темплейт.
1. Помоему любой cast — это «запах», говорящий о том что введен лишний уровень абстракции: мы как бы говорим, что яблоки и автомобильные шины «родственны», но при работе с ними все равно применяем конкретно яблоки и конкретно шины. В чем тогда «родственность»?

2. Я возможно заблуждаюсь (потому, что не вижу весь код целиком и не доконца представляю его окончательный вид), но помоему Object и его потомки «бедноваты». Они являются носителями данных и только: accept выполняющий передачу управления переданному в параметре визитору выглядит странно. С таким же успехом это мог бы выполнить клиент этих классов.
Мне кажется, что пример для иллюстрации не самый удачный, поскольку посетитель уже знает обо всех конечных классах и эта зависимость прописана в его интерфейсе. Кроме того в реальном приложении типов объектов будет с несколько десятков как минимум, что приведет к разбуханию классов-посетителей.
В данном конкретном примере напрашивается вынесение логики отрисовки и удаления в отдельный класс для каждого типа объектов, то есть создание интерфейса типа
class IDrawingStrategy
{
public:
virtual void Draw(Object *obj) = null;
...
}

И в дальнейшем каждому объекту надо присвоить соответствующую стратегию. При этом единственное место, где потребуется информация о всех классах иерархии — это фабричный метод получения стратегии по типу объекта.
Разбухать будет только базовый класс Visitor и UpcastVisitor, все остальные визиторы будут реализовывать только то, что им нужно. Это абсолютно нормально.
Если я все правильно понял, вы хотите хранить в каждом объекте как минимум три стратегии — для отрисовки, создания и удаления. С таким подходом в реальном приложении при большом количестве объектов занимаемая память может увеличиться в несколько раз. Паттерн visitor лишен этого недостатка, т.к. в объекте ничего не хранится.
Также при добавлении нового типа стратегии вам нужно будет модифицировать все классы иерархии, добавляя в них вызовы метода этой стратегии. В случае использования паттерна visitor вам просто нужно будет создать новый тип visitor'а, что намного проще.
>Разбухать будет только базовый класс Visitor и UpcastVisitor, все остальные визиторы будут реализовывать только то, что им нужно
Не очень хорошо, когда базовый класс знает о конечных компонентах.

>Если я все правильно понял, вы хотите хранить в каждом объекте как минимум три стратегии
Теоретически их можно объединить. Один указатель — все-таки не очень большой перерасход памяти. Хотя, не спорю, в некоторых случаях и он может быть критичным.

>Также при добавлении нового типа стратегии вам нужно будет модифицировать все классы иерархии
В принципе сигнатура и назначение стратегии должны быть максимально продуманы еще на стадии проектирования, но если понадобиться новая стратегия — то надо менять имплементацию классов.

> В случае использования паттерна visitor вам просто нужно будет создать новый тип visitor'а, что намного проще.
Не совсем так, если нужна новый тип (не имплементация) стратегии — то на языке посетителей это означает, что надо добавить новый метод в базовый класс посетителя и поменять методы объектов, чтобы они новый функционал делали через посетителя.
>надо добавить новый метод в базовый класс посетителя и поменять методы объектов, чтобы они новый функционал делали через посетителя

Нет, не нужно ничего добавлять в базовый класс посетителя и менять методы объектов тоже не нужно. В объектах для поддержки паттерна visitor должен быть только один метод accept.
Простой пример — вы пользуетесь паттерном visitor только для рисования, у вас уже есть класс Painter. Захотели добавить сохранение состояния программы — добавляете новый класс Saver, который унаследован от Visitor. В нем находится примерно следующее:
void Saver::visit(Tree& tree)
{
    m_document.addNode("tree");
}

void Saver::visit(Apple& apple)
{
    m_document.addNode("apple");
}

При этом вносить изменения в существующие классы и базовый Visitor не нужно.
Пользоваться Saver'ом нужно следующим образом:
void MainGameClass::saveAllObjects(std::vector<Object*> objects)
{
    for (int i = 0; i < objects.size(); ++i)
    {
        objects[i]->accept(m_saver);
    }
    m_saver.saveToDisk();
}
Я имел ввиду если существующая логика перекладывается на плечи посетителя.
Если добавляется новая логика, то ее можно или добавить в существующую стратегию, или создать новую, но при этом саму стратегию надо будет вызывать только в базовом классе объекта.
Что скажете, если заменить шаблон visitor частичной специализацией шаблонной функции.
Кажется плюсы Visitor все соблюдаются. Реализацию методов accept() можно вынести в отдельный файл «Visitor'a». Только конечное дествие происходит уже не Visitor, а в «посещаемом классе».
//main.cpp
void _tmain(int argc, _TCHAR* argv[])
{
	Drawer drawer; //аналог visitor
	Orange orange;
	orange.accept(drawer);
}

//Orange.h
class Orange
{
public:
	template<class ActionState> void accept(ActionState& as);
};

//orange.cpp
template<> void Orange::accept(Drawer& as){
	puts("accept drawer");
}
s/частичной специализацией/специализацией/
Вы пишете, что реализацию методов accept() можно вынести в отдельный файл visitor'a, а в вашем коде реализация в файле orange.cpp, не совсем понятно.
Если конечное действие происходит в посещаемом классе, то visitor не нужен, сойдет обычный виртуальный метод:
class Object
{
public:
    virtual void draw() = 0;
};

class Orange : public Object
{
public:
    virtual void draw() { puts("draw orange"); }
};

int main()
{
    Object* orange = new Orange();
    orange->draw();
    return 0;
}

Смысл visitor'а именно в том, чтобы вынести действие из посещаемого класса.
1. Реализацию Orange::accept(Drawer& as) можно перенести в drawer.cpp.
2. Виртуальный метод как раз не используется, потому что определение Orange не зависит от типа ActionState(можно менять, добавлять классы ActionState). Это один из плюсов шаблона Visitor.
s/, потому что/потому, что/
Самый большой недостаток вашей идеи в том, что метод accept в классе Orange невиртуальный. Соответственно вы не сможете хранить посещаемые объекты по указателям на базовый класс. Если же вы не храните объекты по указателям на базовый класс, то исчезает необходимость использовать visitor, можно обойтись простой перегрузкой методов:
class Apple {};
class Orange {};

class Drawer
{
public:
    void draw(Apple& apple);
    void draw(Orange& orange);
};

int main()
{
    Drawer drawer;
    Apple apple;
    Orange orange;
    drawer.draw(apple);
    drawer.draw(orange);
    return 0;
}
Вот мне иногда в PHP не хватает перегрузки методов, это ещё один отличный пример, почему её хочется.
Пользуясь случаем спрошу: мне одному кажется что т.н. «патттерн Visitor» — это не более чем double-dipatch (+адаптация для языков в которых нет перегрузки методов) в новой маркетинговой упаковке?
Поздравляю вас, если вы понимаете, что visitor и double dispatch это связанные вещи, то вы понимаете, как работает паттерн, хотя visitor — это не double dipatch. Правильнее говорить: «использует double dispatch» или «основан на double dispatch». Из википедии: «The visitor takes the instance reference as input, and implements the goal through double dispatch.»
Double dispatch сам по себе не подразумевает наличие иерархии, как посещаемых объектов, так и visitor'ов.
Простой пример double dispatch'а:
class Human
{
public:
    void eat(Apple& apple) { cout << "Yum-yum"; }
    void eat(Orange& orange) { cout << "Yum-yum-yuuuum!"; }
};

class Apple
{
public:
    void feed(Human& human) { human.eat(*this); }
};

class Orange
{
public:
    void feed(Human& human) { human.eat(*this); }
};

int main()
{
    Human human;
    Apple apple;
    apple.feed(human);
    Orange orange;
    orange.feed(human);
    return 0;
}

Как видим, тут нет базового класса visitor'a и базового посещаемого класса, а также ключевого слова accept. Вместо этого голый double dispatch :)
Есть задача: единообразно обойти коллекцию классов и в зависимости от типа текущего элемента коллекции выполнить некоторые действия.
Если ей решить в виде кучи if instance of — получится мрак, если её решить используя двойную диспетчеризацию — - получится то что все называют «паттерн Visitor». Но как-то мне это кажется недостаточным для того чтобы плодить понятия и создавать «паттерн» и дальше пытаться насаждать его.

«Как видим, тут нет базового класса visitor'a и базового посещаемого класса, а также ключевого слова accept. Вместо этого голый double dispatch :)» Естественно там нет всех этих buzzwords — они же являются частью маркетинговых украшений — зрите в корень.

Ещё любителям «паттернов»:
В чём принципиально отличие DTO и ViewModel, ChainofResponibility от последовательности Proxy которые в свою очередь являются применением принципов SOLID к проектированию. С сожалением прихожу к выводу, что отрасль поражает мода на bullshiting пришедшая из маркетинга, когда простые вещи у которых уже есть название стараются называть всё новыми и новыми терминами.


Я могу продолжить ваш список. Мало кто из начинающих Java программистов знает, что такое observer, зато все прекрасно понимают, что такое listener. Я считаю, не нужно расстраиваться по этому поводу, а стараться уметь оперировать различными терминами, маркетинговыми и не очень.
Ведь buzzwords в паттернах играют огромную роль, поскольку позволяют быстро объяснить или узнать, как взаимодействуют компоненты в системе. Одна из основных задач паттерна — описать общеиспользуемый подход одним своим названием.
Я извиняюсь за приступ некрофилии, мне кажется что в вашем случае это не double dispatch. Как гласит Википедия
In software engineering, double dispatch is a special form of multiple dispatch, and a mechanism that dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call.

У вас же все типы объектов известны на стадии компиляции. Диспетчеризацией и не пахнет. В англоязычной вики есть простой пример именно double dispatching.
Агрегатором Box2DObject в Вашем случае является Box2DObject.
Следовательно, логичнее всего поместить его уничтожение в деструктор объекта Box2DObject.
И не порождать лишних сущностей.
Так что если использование Creator'а еще обосновано, то Destroyer, IMNSHO, не нужен в данном случае.
Sign up to leave a comment.

Articles