Когда следует использовать шаблон дизайна посетителя?

Я продолжаю видеть ссылки на шаблон посетителя в блогах, но я должен признать, что я просто не понимаю. Я прочитал статью wikipedia для шаблона, и я понимаю ее механику, но я все еще смущен, когда буду использовать ее.

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

Ответы

Ответ 1

Я не очень знаком с моделью посетителя. Давайте посмотрим, правильно ли я понял. Предположим, у вас есть иерархия животных

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(Предположим, что это сложная иерархия с устоявшимся интерфейсом.)

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

class Animal
{ public: virtual void makeSound() = 0; };

class Dog : public Animal
{ public: void makeSound(); };

void Dog::makeSound()
{ std::cout << "woof!\n"; }

class Cat : public Animal
{ public: void makeSound(); };

void Cat::makeSound()
{ std::cout << "meow!\n"; }

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

Шаблон Visitor позволяет перемещать каждую новую операцию в подходящий класс, и вам необходимо расширить интерфейс иерархии только один раз. Давай сделаем это. Сначала мы определяем абстрактную операцию (класс "Посетитель" в GoF), который имеет метод для каждого класса в иерархии:

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

Затем мы модифицируем иерархию для принятия новых операций:

class Animal
{ public: virtual void letsDo(Operation *v) = 0; };

class Dog : public Animal
{ public: void letsDo(Operation *v); };

void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }

class Cat : public Animal
{ public: void letsDo(Operation *v); };

void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }

Наконец, мы реализуем фактическую операцию, не изменяя ни Cat, ни Dog:

class Sound : public Operation
{
public:
    void hereIsADog(Dog *d);
    void hereIsACat(Cat *c);
};

void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }

void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }

Теперь у вас есть возможность добавлять операции без изменения иерархии. Вот как это работает:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}

Ответ 2

Причина вашего замешательства, вероятно, заключается в том, что посетитель является фатальным неправильным. Многие (известные программисты 1!) Наткнулись на эту проблему. Фактически он реализует двойную диспетчеризацию на языках, которые его не поддерживают (большинство из них этого не делают).


1) Моим любимым примером является Скотт Мейерс, известный автор "Эффективного С++", который назвал его одним из самым важным С++ ага! моменты когда-либо.

Ответ 3

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

Посетитель позволяет вам определить новый без изменения классов элементов, на которых он работает.

Теперь подумайте о простой иерархии классов. У меня есть классы 1, 2, 3 и 4 и методы A, B, C и D. Разложите их как в электронной таблице: классы - это строки, а методы - столбцы.

Теперь объектно-ориентированный дизайн предполагает, что у вас больше шансов развить новые классы, чем новые методы, поэтому проще добавить несколько строк. Вы просто добавляете новый класс, указываете, что отличается от этого класса, и наследует остальные.

Иногда, однако, классы относительно статичны, но вам нужно чаще добавлять больше методов - добавление столбцов. Стандартный способ в OO-дизайне - добавить такие методы ко всем классам, что может быть дорогостоящим. Шаблон посетителя делает это легко.

Кстати, это проблема, которую макет шаблона Scala намеревается решить.

Ответ 4

Шаблон дизайна Посетитель отлично работает для "рекурсивных" структур, таких как деревья каталогов, структуры XML или контуры документа.

Объект посетителя посещает каждый node в рекурсивной структуре: каждый каталог, каждый тег XML, что угодно. Объект посетителя не пересекает структуру. Вместо этого методы Visitor применяются к каждой структуре node.

Здесь типичная рекурсивная структура node. Может быть каталогом или тегом XML. [Если ваш человек Java, представьте себе множество дополнительных методов для создания и поддержки списка детей.]

class TreeNode( object ):
    def __init__( self, name, *children ):
        self.name= name
        self.children= children
    def visit( self, someVisitor ):
        someVisitor.arrivedAt( self )
        someVisitor.down()
        for c in self.children:
            c.visit( someVisitor )
        someVisitor.up()

Метод visit применяет объект Visitor к каждому node в структуре. В этом случае это посетитель сверху вниз. Вы можете изменить структуру метода visit, чтобы сделать снизу вверх или какой-либо другой порядок.

Здесь суперкласс для посетителей. Он используется методом visit. Он "достигает" каждого node в структуре. Поскольку метод visit вызывает up и down, посетитель может отслеживать глубину.

class Visitor( object ):
    def __init__( self ):
        self.depth= 0
    def down( self ):
        self.depth += 1
    def up( self ):
        self.depth -= 1
    def arrivedAt( self, aTreeNode ):
        print self.depth, aTreeNode.name

Подкласс может делать такие вещи, как подсчет узлов на каждом уровне и накапливать список узлов, генерируя хорошие иерархические номера разделов.

Здесь приложение. Он создает древовидную структуру, someTree. Он создает Visitor, dumpNodes.

Затем он применяет dumpNodes к дереву. Объект dumpNode будет "посещать" каждый node в дереве.

someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )

Алгоритм TreeNode visit гарантирует, что каждый TreeNode используется как аргумент метода Visitor arrivedAt.

Ответ 5

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

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

Классический пример для компиляторов и т.п. Абстрактное дерево синтаксиса (AST) может точно определить структуру языка программирования, но операции, которые вы можете захотеть сделать в AST, будут меняться по мере продвижения вашего проекта: генераторы кода, симпатичные принтеры, отладчики, анализ метрик сложности.

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

(Я слышал, что он утверждал, что шаблон посетителя противоречит хорошим методам OO, поскольку он перемещает данные данных от данных. Шаблон посетителя полезен именно в том случае, когда обычная практика OO терпит неудачу. )

Ответ 6

Существует не менее трех очень важных причин для использования шаблона посетителей:

  • Уменьшить распространение кода, который немного отличается при изменении структур данных.

  • Применить одно и то же вычисление к нескольким структурам данных, не изменяя код, который реализует вычисление.

  • Добавить информацию в устаревшие библиотеки без изменения устаревшего кода.

Пожалуйста, посмотрите статью, которую я написал об этом.

Ответ 7

Как уже отмечал Конрад Рудольф, он подходит для случаев, когда нам нужна двойная отправка

Вот пример, показывающий ситуацию, когда нам нужна двойная отправка и как посетитель помогает нам в этом.

Пример:

Предположим, у меня есть 3 типа мобильных устройств - iPhone, Android, Windows Mobile.

Все эти три устройства имеют Bluetooth-радио, установленное в них.

Предположим, что радиостанция с синим зубом может быть от двух отдельных OEM-производителей - Intel и Broadcom.

Чтобы сделать пример подходящим для нашего обсуждения, давайте также предположим, что API-интерфейсы, предоставляемые радиостанцией Intel, отличаются от тех, которые выставлены радио Broadcom.

Вот как выглядят мои классы -

введите описание изображения здесь введите описание изображения здесь

Теперь я хотел бы ввести операцию "Включение Bluetooth на мобильном устройстве".

Его сигнатура функции должна выглядеть примерно так:

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

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

В принципе, он становится матрицей 3 x 2, где-в Im пытается вектор правильной операции в зависимости от правильного типа объектов.

Полиморфное поведение в зависимости от типа обоих аргументов.

введите описание изображения здесь

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

Двойная отправка необходима из-за матрицы 3x2

Вот как будет выглядеть настройка - введите описание изображения здесь

Я написал пример, чтобы ответить на другой вопрос, код и его объяснение упоминаются здесь.

Ответ 8

Я нашел это проще в следующих ссылках:

В http://www.remondo.net/visitor-pattern-example-csharp/ я нашел пример, который показывает фиктивный пример, который показывает, что является преимуществом шаблона посетителя. Здесь у вас есть разные классы контейнеров для Pill:

namespace DesignPatterns
{
    public class BlisterPack
    {
        // Pairs so x2
        public int TabletPairs { get; set; }
    }

    public class Bottle
    {
        // Unsigned
        public uint Items { get; set; }
    }

    public class Jar
    {
        // Signed
        public int Pieces { get; set; }
    }
}

Как вы видите выше, You BilsterPack содержит пары Pills ', поэтому вам нужно умножить количество пар на 2. Также вы можете заметить, что Bottle использует unit другим типом данных, который необходимо разыграть.

Таким образом, в основном методе вы можете рассчитать количество таблеток, используя следующий код:

foreach (var item in packageList)
{
    if (item.GetType() == typeof (BlisterPack))
    {
        pillCount += ((BlisterPack) item).TabletPairs * 2;
    }
    else if (item.GetType() == typeof (Bottle))
    {
        pillCount += (int) ((Bottle) item).Items;
    }
    else if (item.GetType() == typeof (Jar))
    {
        pillCount += ((Jar) item).Pieces;
    }
}

Обратите внимание, что приведенный выше код нарушает Single Responsibility Principle. Это означает, что вы должны изменить основной код метода, если добавляете новый тип контейнера. Также делать переключение дольше - плохая практика.

Итак, введя следующий код:

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int)bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

Вы перевели ответственность за подсчет количества Pill в класс с именем PillCountVisitor (И мы удалили оператор переключения регистра). Это означает, что всякий раз, когда вам нужно добавить новый тип контейнера для таблеток, вы должны изменить только класс PillCountVisitor. Также обратите IVisitor интерфейс IVisitor является общим для использования в других сценариях.

Добавив метод Accept в класс контейнера для таблеток:

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

мы разрешаем посетителю посещать классы контейнера для таблеток.

В конце мы рассчитываем количество таблеток, используя следующий код:

var visitor = new PillCountVisitor();

foreach (IAcceptor item in packageList)
{
    item.Accept(visitor);
}

Это означает: каждый контейнер с таблетками позволяет посетителю PillCountVisitor видеть количество своих таблеток. Он умеет считать твои таблетки.

На visitor.Count имеет значение таблетки.

В http://butunclebob.com/ArticleS.UncleBob.IuseVisitor вы видите реальный сценарий, в котором вы не можете использовать полиморфизм (ответ), чтобы следовать принципу единой ответственности. На самом деле в:

public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

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

Ответ 9

По моему мнению, объем работы по добавлению новой операции более или менее одинаковый с использованием Visitor Pattern или прямой модификации каждой структуры элемента. Кроме того, если бы я должен был добавить новый класс элемента, скажем Cow, это повлияет на интерфейс операции, и это распространяется на весь существующий класс элементов, что требует перекомпиляции всех классов элементов. Итак, в чем смысл?

Ответ 10

Cay Horstmann имеет отличный пример того, как применять посетитель в своей книге дизайна и шаблонов OO. Он суммирует проблему:

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

Причина в том, что это непросто, потому что операции добавляются внутри самих классов структуры. Например, представьте, что у вас есть файловая система:

FileSystem class diagram

Вот некоторые операции (функциональные возможности), которые мы могли бы реализовать с помощью этой структуры:

  • Отобразить имена элементов node (список файлов)
  • Отобразить вычисленный размер элементов node (где размер каталога включает в себя размер всех его дочерних элементов)
  • и др.

Вы можете добавлять функции к каждому классу в FileSystem для реализации операций (и люди делали это в прошлом, поскольку это очень очевидно, как это сделать). Проблема в том, что всякий раз, когда вы добавляете новую функциональность (строка "и т.д." Выше), вам может потребоваться добавить все более и более методов в классы структуры. В какой-то момент после некоторого количества операций, которые вы добавили в свое программное обеспечение, методы в этих классах уже не имеют смысла с точки зрения функциональной сплоченности классов. Например, у вас есть FileNode, у которого есть метод calculateFileColorForFunctionABC(), чтобы реализовать последние функции визуализации в файловой системе.

Шаблон посетителя (как и многие шаблоны проектирования) рождался из-за боли и страданий разработчиков, которые знали, что существует лучший способ изменить их код, не требуя много изменений во всем мире, а также уважая хорошие принципы проектирования (высокая степень сцепления, низкое сцепление). Это мое мнение, что трудно понять полезность множества образцов, пока вы не почувствуете эту боль. Объясняя боль (как мы пытаемся сделать выше, с добавленными функциями "и т.д." ) Занимает место в объяснении и является отвлечением. По этой причине понимание шаблонов затруднено.

Посетитель позволяет нам отделить функциональные возможности от структуры данных (например, FileSystemNodes) от самих структур данных. Шаблон позволяет дизайну уважать классы сцепления - структуры данных проще (у них меньше методов), а также функциональные возможности заключены в реализации Visitor. Это делается с помощью двойной диспетчеризации (что является сложной частью шаблона): использование методов accept() в структурных классах и visitX() методов в классах посетителей (функциональных):

FileSystem class diagram with Visitor applied

Эта структура позволяет нам добавлять новые функциональные возможности, которые работают над структурой как конкретные посетители (без изменения классов структуры).

FileSystem class diagram with Visitor applied

Например, PrintNameVisitor, который реализует функциональность списка каталогов, и PrintSizeVisitor, который реализует версию с размером. В один прекрасный день мы могли бы представить "ExportXMLVisitor", который генерирует данные в XML или другого посетителя, который генерирует его в JSON, и т.д. У нас может даже быть посетитель, который отображает мое дерево каталогов с помощью графический язык, такой как DOT, для визуализации с другой программой.

Как последнее примечание: сложность Visitor с его двойной отправкой означает, что сложнее понять, закодировать и отлаживать. Короче говоря, он имеет высокий фактор geek и снова переходит в принцип KISS. В опросе, проведенном исследователями, посетитель оказался спорным (не было консенсуса относительно его полезности). Некоторые эксперименты даже показали, что код не упрощается для ведения.

Ответ 11

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

Если вы можете использовать его

  • Когда у вас есть семейство классов, вы знаете, что вам придется добавлять много новых действий, но по какой-то причине вы не можете изменить или перекомпилировать семейство классов в будущем.
  • Если вы хотите добавить новое действие и создать новое действие, полностью определенное внутри одного класса посетителя, а не распространяться на несколько классов.
  • Когда ваш босс говорит, что вы должны создать ряд классов, которые должны сделать что-то прямо сейчас!... но никто на самом деле точно не знает, что это такое.

Ответ 12

Шаблон посетителя как одна и та же подземная реализация для программирования объектов Aspect.

Например, если вы определяете новую операцию без изменения классов элементов, на которых она работает

Ответ 13

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

Вот примеры использования шаблона:

1) Мы хотим определить новые операции без изменения модели в каждый момент времени, потому что модель не меняется часто часто меняются операции wile.

2) Мы не хотим сочетать модель и поведение, потому что мы хотим иметь многоразовую модель в нескольких приложениях или , мы хотим иметь расширяемую model, которые позволяют клиентским классам определять свое поведение со своими собственными классами.

3) У нас есть общие операции, которые зависят от конкретного типа модели, но мы не хотим реализовывать логику в каждом подклассе, поскольку это приведет к взрыву общей логики в нескольких классах и поэтому в нескольких местах.

4) Мы используем дизайн модели домена, а модельные классы той же иерархии выполняют слишком много разных вещей, которые могут быть собраны где-то еще.

5) Нам нужна двойная отправка.
У нас есть переменные, объявленные с типами интерфейсов, и мы хотим иметь возможность обрабатывать их по их типу времени выполнения... конечно, без использования if (myObj instanceof Foo) {} или любого трюка.
Идея состоит в том, чтобы передать эти переменные методам, которые объявляют конкретный тип интерфейса в качестве параметра для применения конкретной обработки. Такой способ невозможен из-за того, что языки используют однонаправленную отправку, потому что выбранные вызовы во время выполнения зависят только от типа среды выполнения приемника.
Обратите внимание, что в Java метод (подпись) для вызова выбирается во время компиляции и зависит от объявленного типа параметров, а не от типа его выполнения.

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

Обратите внимание, что обход элементов (итерация) для применения посетителя на каждом из них не является основанием для использования шаблона.
Вы используете шаблон, потому что вы разделяете модель и обработку.
И, используя шаблон, вы получаете преимущество от возможности итератора.
Эта способность очень мощная и выходит за пределы итерации по общему типу с помощью конкретного метода, поскольку accept() является общим методом.
Это специальный прецедент. Поэтому я поставлю это в сторону.


Пример в Java

Я проиллюстрирую добавленную ценность шаблона шахматным примером, где мы хотели бы определить обработку, поскольку игрок запрашивает перемещение части.

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

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Каждый подкласс Piece будет реализовывать его, например:

public class Pawn implements Piece{

    @Override
    public boolean checkMoveValidity(Coordinates coord) {
        ...
    }

    @Override
    public void performMove(Coordinates coord) {
        ...
    }

    @Override
    public Piece computeIfKingCheck() {
        ...
    }

}

И то же самое для всех подклассов Piece.
Вот диаграммный класс, который иллюстрирует этот дизайн:

[модельная диаграмма классов

Этот подход представляет три важных недостатка:

- поведение, такое как performMove() или computeIfKingCheck(), вероятно, будет использовать общую логику.
Например, независимо от того, какой конкретный Piece, performMove() окончательно установит текущую часть в определенное место и потенциально возьмет кусок противника.
Разделение связанных действий в нескольких классах вместо того, чтобы собирать их, поражает каким-то образом единый шаблон ответственности. Упрощение их ремонтопригодности.

- обработка как checkMoveValidity() не должна быть чем-то, что подклассы Piece могут видеть или изменять.
Это проверка, которая выходит за рамки действий человека или компьютера. Эта проверка выполняется при каждом действии, запрошенном игроком, чтобы гарантировать, что требуемый ход элемента действителен.
Поэтому мы даже не хотим предоставлять это в интерфейсе Piece.

- В играх с шахматами, бросающих вызов разработчикам ботов, обычно приложение предоставляет стандартный API (Piece интерфейсы, подклассы, Board, общее поведение и т.д.) и позволяет разработчикам обогащать свою ботовую стратегию. Чтобы это сделать, мы должны предложить модель, в которой данные и поведение не тесно связаны в реализациях Piece.

Так что давайте перейдем к использованию шаблона посетителя!

У нас есть два вида структуры:

- классы моделей, которые допускают посещения (фрагменты)

- посетители, которые посещают их (движущиеся операции)

Вот диаграмма классов, которая иллюстрирует шаблон:

введите описание изображения здесь

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

Вот интерфейс PieceMovingVisitor (поведение, указанное для каждого типа Piece):

public interface PieceMovingVisitor {

    void visitPawn(Pawn pawn);

    void visitKing(King king);

    void visitQueen(Queen queen);

    void visitKnight(Knight knight);

    void visitRook(Rook rook);

    void visitBishop(Bishop bishop);

}

Piece теперь определен:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Его ключевой метод:

void accept(PieceMovingVisitor pieceVisitor);

Он обеспечивает первую отправку: вызов, основанный на приемнике Piece.
Во время компиляции метод привязан к методу accept() интерфейса Piece и во время выполнения ограниченный метод будет вызываться в классе времени выполнения Piece.
И реализация метода accept() будет выполнять вторую отправку.

Действительно, каждый подкласс Piece, который хочет быть посещенным объектом PieceMovingVisitor, вызывает метод PieceMovingVisitor.visit(), передавая как сам аргумент.
Таким образом, компилятор ограничивается, как только время компиляции, тип объявленного параметра с конкретным типом.
Существует вторая отправка.
Вот подкласс Bishop, который иллюстрирует, что:

public class Bishop implements Piece {

    private Coordinates coord;

    public Bishop(Coordinates coord) {
        super(coord);
    }

    @Override
    public void accept(PieceMovingVisitor pieceVisitor) {
        pieceVisitor.visitBishop(this);
    }

    @Override
    public Coordinates getCoordinates() {
        return coordinates;
    }

   @Override
    public void setCoordinates(Coordinates coordinates) {
        this.coordinates = coordinates;
   }

}

И вот пример использования:

// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();

// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);

// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
    piece.accept(new MovePerformingVisitor(coord));
}

Недостатки посетителей

Шаблон посетителя - очень мощный шаблон, но он также имеет некоторые важные ограничения, которые вы должны рассмотреть перед его использованием.

1) Риск уменьшить/разорвать инкапсуляцию

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

Например, поскольку класс MovePerformingVisitor должен установить координаты фактической части, интерфейс Piece должен предоставить способ сделать это:

void setCoordinates(Coordinates coordinates);

Ответственность за изменения координат Piece теперь открыта для других классов, чем подклассы Piece.
Перемещение обработки, выполненной посетителем в подклассы Piece, также не является вариантом.
Это действительно создаст еще одну проблему, поскольку Piece.accept() принимает любую реализацию посетителя. Он не знает, что делает посетитель, и поэтому не знает, как и как изменить состояние Piece.
Способ идентификации посетителя состоял бы в том, чтобы выполнить пост-обработку в Piece.accept() в соответствии с реализацией посетителя. Это было бы очень плохой идеей, так как это создало бы высокую связь между реализациями Visitor и подклассами Piece, и, кроме того, для этого, вероятно, потребуется использовать трюк как getClass(), instanceof или любой маркер, идентифицирующий реализацию Visitor.

2) Требование изменить модель

В отличие от некоторых других моделей поведения в качестве Decorator, например, шаблон посетителя является навязчивым.
Нам действительно нужно изменить начальный класс получателя, чтобы предоставить метод accept() для принятия для посещения.
У нас не было проблем для Piece и его подклассов, так как они наши классы.
В встроенных или сторонних классах все не так просто.
Нам нужно обернуть или наследовать (если можно) их, чтобы добавить метод accept().

3) Направления

Шаблон создает множественные косвенные значения.
Двойная отправка означает два вызова вместо одного:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor)

И у нас могут быть дополнительные указания, так как посетитель изменяет состояние посещенного объекта.
Это может выглядеть как цикл:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)

Ответ 14

Visitor

Посетитель позволяет добавлять новые виртуальные функции в семейство классов без изменения самих классов; вместо этого создается класс посетителя, который реализует все соответствующие специализации виртуальной функции

Структура посетителей:

введите описание изображения здесь

Использовать шаблон посетителя, если:

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

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

Если новый объект Visitable добавлен, он требует изменения кода в классах Visitor и ConcreteVisitor. Для решения этой проблемы необходимо обходное решение: использовать отражение, которое будет влиять на производительность.

Фрагмент кода:

import java.util.HashMap;

interface Visitable{
    void accept(Visitor visitor);
}

interface Visitor{
    void logGameStatistics(Chess chess);
    void logGameStatistics(Checkers checkers);
    void logGameStatistics(Ludo ludo);    
}
class GameVisitor implements Visitor{
    public void logGameStatistics(Chess chess){
        System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");    
    }
    public void logGameStatistics(Checkers checkers){
        System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");    
    }
    public void logGameStatistics(Ludo ludo){
        System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");    
    }
}

abstract class Game{
    // Add game related attributes and methods here
    public Game(){

    }
    public void getNextMove(){};
    public void makeNextMove(){}
    public abstract String getName();
}
class Chess extends Game implements Visitable{
    public String getName(){
        return Chess.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Checkers extends Game implements Visitable{
    public String getName(){
        return Checkers.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Ludo extends Game implements Visitable{
    public String getName(){
        return Ludo.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}

public class VisitorPattern{
    public static void main(String args[]){
        Visitor visitor = new GameVisitor();
        Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
        for (Visitable v : games){
            v.accept(visitor);
        }
    }
}

Пояснение:

  • Visitable (Element) - это интерфейс, и этот метод интерфейса должен быть добавлен к набору классов.
  • Visitor - это интерфейс, который содержит методы для выполнения операции над элементами Visitable.
  • GameVisitor - это класс, реализующий интерфейс Visitor (ConcreteVisitor).
  • Каждый элемент Visitable принимает Visitor и вызывает соответствующий метод интерфейса Visitor.
  • Вы можете рассматривать Game как Element и конкретные игры типа Chess,Checkers and Ludo как ConcreteElements.

В приведенном выше примере Chess, Checkers and Ludo - три разные игры (и Visitable классы). В один прекрасный день я столкнулся со сценарием для регистрации статистики каждой игры. Поэтому, не изменяя индивидуальный класс для реализации функций статистики, вы можете централизовать эту ответственность в классе GameVisitor, что делает трюк для вас без изменения структуры каждой игры.

выход:

Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser

Обратитесь к

статья oodesign

sourcemaking статья

для более подробной информации

Decorator

Шаблон

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

Похожие сообщения:

Рисунок декоратора для IO

Когда использовать шаблон декоратора?

Ответ 15

Основываясь на превосходном ответе @Federico A. Ramponi.

Представьте, что у вас есть эта иерархия:

public interface IAnimal
{
    void DoSound();
}

public class Dog : IAnimal
{
    public void DoSound()
    {
        Console.WriteLine("Woof");
    }
}

public class Cat : IAnimal
{
    public void DoSound(IOperation o)
    {
        Console.WriteLine("Meaw");
    }
}

Что произойдет, если вам нужно добавить метод "Прогулка"? Это будет болезненно для всего дизайна.

В то же время добавление метода "Прогулка" порождает новые вопросы. Как насчет "Ешь" или "Сон"? Должен ли мы действительно добавлять новый метод в иерархию животных для каждого нового действия или операции, которые мы хотим добавить? Это уродливое и самое главное, мы никогда не сможем закрыть интерфейс Animal. Таким образом, с шаблоном посетителя мы можем добавить новый метод в иерархию без изменения иерархии!

Итак, просто проверьте и запустите этот пример С#:

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var animals = new List<IAnimal>
            {
                new Cat(), new Cat(), new Dog(), new Cat(), 
                new Dog(), new Dog(), new Cat(), new Dog()
            };

            foreach (var animal in animals)
            {
                animal.DoOperation(new Walk());
                animal.DoOperation(new Sound());
            }

            Console.ReadLine();
        }
    }

    public interface IOperation
    {
        void PerformOperation(Dog dog);
        void PerformOperation(Cat cat);
    }

    public class Walk : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Dog walking");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Cat Walking");
        }
    }

    public class Sound : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Woof");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Meaw");
        }
    }

    public interface IAnimal
    {
        void DoOperation(IOperation o);
    }

    public class Dog : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }

    public class Cat : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }
}

Ответ 16

Пока я понял, как и когда, я никогда не понимал, почему. Если это помогает любому, у кого есть фон на языке С++, вы хотите внимательно прочитать это.

Для лени, мы используем шаблон посетителя, потому что ", в то время как виртуальные функции динамически отправляются на С++, перегрузка функций выполняется статически" .

Или, по-другому, чтобы убедиться, что CollideWith (ApolloSpacecraft &) вызывается, когда вы передаете ссылку SpaceShip, которая фактически привязана к объекту ApolloSpacecraft.

class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
  virtual void CollideWith(SpaceShip&) {
    cout << "ExplodingAsteroid hit a SpaceShip" << endl;
  }
  virtual void CollideWith(ApolloSpacecraft&) {
    cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
  }
}

Ответ 17

Мне очень нравится описание и пример из http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html.

Предполагается, что у вас установлена ​​иерархия первичного класса; возможно, от другого поставщика, и вы не можете внести изменения в эту иерархию. Однако ваше намерение заключается в том, что youd хотел бы добавить новые полиморфные методы в эту иерархию, а это означает, что обычно вам нужно добавить что-то в интерфейс базового класса. Поэтому дилемма заключается в том, что вам нужно добавить методы в базовый класс, но вы не можете прикоснуться к базовому классу. Как вы обойдете это?

Шаблон проектирования, который решает эту проблему, называется "посетителем" (последний в книге "Шаблоны проектирования" ), и он основывается на двойной схеме диспетчеризации, показанной в последнем разделе.

Шаблон посетителя позволяет расширить интерфейс первичного типа, создав отдельную иерархию классов типа Visitor для виртуализации операций, выполняемых по первому типу. Объекты первичного типа просто "принимают" посетителя, а затем вызывают динамическую связанную функцию посетителей.

Ответ 18

Я не понимал эту модель, пока не наткнулся на статью дяди Боба и не прочитал комментарии. Рассмотрим следующий код:

public class Employee
{
}

public class SalariedEmployee : Employee
{
}

public class HourlyEmployee : Employee
{
}

public class QtdHoursAndPayReport
{
    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        foreach (Employee e in employees)
        {
            if (e is HourlyEmployee he)
                PrintReportLine(he);
            if (e is SalariedEmployee se)
                PrintReportLine(se);
        }
    }

    public void PrintReportLine(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hours");
    }
    public void PrintReportLine(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    }
}

class Program
{
    static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }
}

Хотя это может выглядеть хорошо, так как подтверждает единственную ответственность, оно нарушает принцип Open/Closed. Каждый раз, когда у вас есть новый тип сотрудника, вы должны будете добавить, если с проверкой типа. И если вы этого не сделаете, вы никогда не узнаете об этом во время компиляции.

С помощью шаблона посетителя вы можете сделать свой код чище, так как он не нарушает принцип открытия/закрытия и не нарушает Единую ответственность. И если вы забудете реализовать визит, он не скомпилируется:

public abstract class Employee
{
    public abstract void Accept(EmployeeVisitor v);
}

public class SalariedEmployee : Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public class HourlyEmployee:Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public interface EmployeeVisitor
{
    void Visit(HourlyEmployee he);
    void Visit(SalariedEmployee se);
}

public class QtdHoursAndPayReport : EmployeeVisitor
{
    public void Visit(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hourly");
        // generate the line of the report.
    }
    public void Visit(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    } // do nothing

    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        QtdHoursAndPayReport v = new QtdHoursAndPayReport();
        foreach (var emp in employees)
        {
            emp.Accept(v);
        }
    }
}

class Program
{

    public static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }       
}  
}

Волшебство в том, что хотя v.Visit(this) выглядит одинаково, на самом деле он отличается, так как вызывает разные перегрузки посетителя.

Ответ 19

Если вы хотите иметь функциональные объекты для типов данных объединения, вам понадобится шаблон посетителя.

Вы можете задаться вопросом, какие объекты функции и типы данных объединения есть, тогда стоит прочитать http://www.ccs.neu.edu/home/matthias/htdc.html

Ответ 20

Спасибо за потрясающее объяснение @Federico A. Ramponi, я только что сделал это в java- версии. Надеюсь, это может быть полезно.

Также, как указал @Konrad Rudolph, это фактически двойная диспетчеризация, использующая два конкретных экземпляра вместе для определения методов времени выполнения.

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

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showTheHobby(food);
        Katherine katherine = new Katherine();
        katherine.presentHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void embed(Katherine katherine);
}


class Hearen {
    String name = "Hearen";
    void showTheHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine {
    String name = "Katherine";
    void presentHobby(Hobby hobby) {
        hobby.embed(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void embed(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

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

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showHobby(food);
        Katherine katherine = new Katherine();
        katherine.showHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void insert(Katherine katherine);
}

abstract class Person {
    String name;
    protected Person(String n) {
        this.name = n;
    }
    abstract void showHobby(Hobby hobby);
}

class Hearen extends  Person {
    public Hearen() {
        super("Hearen");
    }
    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine extends Person {
    public Katherine() {
        super("Katherine");
    }

    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void insert(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

Ответ 21

Ваш вопрос, когда знать:

я не первый код с шаблоном посетителя. я кодирую стандарт и жду, когда возникнет необходимость, а затем рефакторинг. Допустим, у вас есть несколько платежных систем, которые вы установили по одной за раз. Во время оформления заказа у вас может быть много условий if (или instanceOf), например:

//psuedo code
    if(payPal) 
    do paypal checkout 
    if(stripe)
    do strip stuff checkout
    if(payoneer)
    do payoneer checkout

Теперь представьте, что у меня было 10 способов оплаты, это выглядит ужасно. Поэтому, когда вы видите, что посетитель приходит, чтобы разделить все это, вы в конечном итоге вызываете что-то вроде этого:

new PaymentCheckoutVistor(paymentType).visit()

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