Когда я должен использовать шаблон дизайна посетителя?
Я продолжаю видеть ссылки на шаблон посетителя в блогах, но я должен признать, я просто не понимаю. Я прочитал статью в Википедии, посвященную шаблону, и понимаю его механику, но все еще не понимаю, когда мне его использовать.
Как человек, который совсем недавно действительно получил шаблон декоратора и теперь видит его применение абсолютно везде, я бы хотел по-настоящему интуитивно понять этот, казалось бы, удобный шаблон.
21 ответ
Я не очень знаком с моделью посетителя. Посмотрим, правильно ли я понял. Предположим, у вас есть иерархия животных
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);
}
Причиной вашего замешательства, вероятно, является то, что Посетитель - смертельный неправильный человек. Многие (выдающиеся 1!) Программисты наткнулись на эту проблему. Что он на самом деле делает, так это реализует двойную диспетчеризацию в языках, которые не поддерживают его изначально (большинство из них этого не делают).
1) Мой любимый пример - Скотт Мейерс, известный автор "Эффективного C++", который назвал это одним из своих самых важных C++, ага! моменты когда-либо.
Все здесь правы, но я думаю, что это не касается "когда". Во-первых, из Design Patterns:
Посетитель позволяет определить новую операцию без изменения классов элементов, с которыми он работает.
Теперь давайте подумаем о простой иерархии классов. У меня есть классы 1, 2, 3 и 4 и методы A, B, C и D. Выложите их, как в электронной таблице: классы - это строки, а методы - столбцы.
Теперь объектно-ориентированное проектирование предполагает, что у вас больше шансов вырастить новые классы, чем новые методы, поэтому проще добавить больше строк, так сказать. Вы просто добавляете новый класс, указываете, что отличается в этом классе, и наследует остальные.
Иногда, однако, классы относительно статичны, но вам нужно часто добавлять больше методов - добавление столбцов. Стандартный способ разработки ОО состоит в добавлении таких методов ко всем классам, что может быть дорогостоящим. Шаблон Visitor делает это легко.
Кстати, именно эту проблему намеревается решить шаблон Scala.
Шаблон проектирования Visitor очень хорошо работает для "рекурсивных" структур, таких как деревья каталогов, структуры XML или контуры документов.
Объект Visitor посещает каждый узел в рекурсивной структуре: каждый каталог, каждый тег XML, что угодно. Объект Visitor не проходит через структуру. Вместо этого методы Visitor применяются к каждому узлу структуры.
Вот типичная рекурсивная структура узлов. Может быть каталогом или тегом 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 к каждому узлу в структуре. В данном случае это посетитель сверху вниз. Вы можете изменить структуру visit
способ сделать снизу вверх или какой-то другой порядок.
Вот суперкласс для посетителей. Используется visit
метод. Он "достигает" каждого узла в структуре. Так как 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
Объект будет "посещать" каждый узел в дереве.
someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )
TreeNode visit
алгоритм будет гарантировать, что каждый TreeNode используется в качестве аргумента для посетителя arrivedAt
метод.
Один из способов взглянуть на это состоит в том, что шаблон посетителя - это способ позволить вашим клиентам добавлять дополнительные методы ко всем вашим классам в определенной иерархии классов.
Это полезно, когда у вас достаточно стабильная иерархия классов, но у вас есть изменяющиеся требования к тому, что нужно делать с этой иерархией.
Классический пример для компиляторов и тому подобное. Абстрактное синтаксическое дерево (AST) может точно определить структуру языка программирования, но операции, которые вы можете выполнять в AST, будут меняться по мере продвижения вашего проекта: генераторы кода, симпатичные принтеры, отладчики, анализ метрик сложности.
Без шаблона посетителя каждый раз, когда разработчик хотел добавить новую функцию, он должен был бы добавить этот метод к каждой функции в базовом классе. Это особенно сложно, когда базовые классы появляются в отдельной библиотеке или создаются отдельной командой.
(Я слышал, что он утверждал, что шаблон Visitor находится в конфликте с хорошими практиками OO, потому что он перемещает операции данных от данных. Шаблон Visitor полезен именно в ситуации, когда обычные практики OO терпят неудачу.)
Двойная отправка - это всего лишь одна из причин использования этого шаблона.
Но обратите внимание, что это единственный способ реализовать двойную или более диспетчеризацию в языках, использующих одну парадигму диспетчеризации.
Вот причины для использования шаблона:
1) Мы хотим определять новые операции, не меняя модель каждый раз, потому что модель не меняется часто, а операции часто меняются.
2) Мы не хотим связывать модель и поведение, потому что мы хотим иметь многократно используемую модель в нескольких приложениях или мы хотим иметь расширяемую модель, которая позволяет клиентским классам определять свое поведение со своими собственными классами.
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);
}
Часть определена сейчас:
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));
}
Недостатки посетителя
Шаблон Visitor является очень мощным шаблоном, но он также имеет некоторые важные ограничения, которые следует учитывать перед его использованием.
1) Риск уменьшить / сломать инкапсуляцию
В некоторых видах операций шаблон посетителя может уменьшить или нарушить инкапсуляцию объектов домена.
Например, как MovePerformingVisitor
класс должен установить координаты фактической части, Piece
Интерфейс должен предоставить способ сделать это:
void setCoordinates(Coordinates coordinates);
Ответственность Piece
Изменения координат теперь открыты для других классов, кроме Piece
подклассы.
Перемещение обработки, выполненной посетителем в Piece
подклассы тоже не вариант.
Это действительно создаст другую проблему как Piece.accept()
принимает любую реализацию посетителя. Он не знает, что выполняет посетитель, и поэтому не знает, нужно ли и как изменить состояние фигуры.
Способ идентифицировать посетителя - выполнить постобработку в Piece.accept()
согласно реализации посетителя. Это было бы очень плохой идеей, поскольку она создала бы высокую связь между реализациями Visitor и подклассами Piece, и, кроме того, вероятно, потребовалось бы использовать трюк как getClass()
, instanceof
или любой маркер, идентифицирующий реализацию Посетителя.
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)
Существует как минимум три веские причины для использования шаблона посетителя:
Уменьшите распространение кода, который незначительно отличается при изменении структуры данных.
Примените одно и то же вычисление к нескольким структурам данных, не изменяя код, который реализует вычисление.
Добавить информацию в устаревшие библиотеки без изменения устаревшего кода.
Пожалуйста, взгляните на статью, которую я написал об этом.
Как уже отмечал Конрад Рудольф, он подходит для случаев, когда нам нужна двойная отправка
Вот пример, чтобы показать ситуацию, когда нам нужна двойная отправка и как посетитель помогает нам в этом.
Пример:
Допустим, у меня есть 3 типа мобильных устройств - iPhone, Android, Windows Mobile.
На всех этих трех устройствах установлено радио Bluetooth.
Предположим, что радио "синий зуб" может быть от двух отдельных OEM-производителей - Intel и Broadcom.
Просто для того, чтобы сделать пример актуальным для нашего обсуждения, давайте также предположим, что API-интерфейсы, предоставляемые Intel-радио, отличаются от тех, которые предоставляются Broadcom-радио.
Вот так выглядят мои занятия -
Теперь я хотел бы ввести операцию - Включение Bluetooth на мобильном устройстве.
Его функция подписи должна выглядеть примерно так:
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
Таким образом, в зависимости от правильного типа устройства и в зависимости от правильного типа радиомодуля Bluetooth, его можно включить, вызвав соответствующие шаги или алгоритм.
В принципе, это становится матрицей 3 x 2, где я пытаюсь векторизовать правильную операцию в зависимости от правильного типа вовлеченных объектов.
Полиморфное поведение в зависимости от типа обоих аргументов.
Теперь шаблон Visitor может быть применен к этой проблеме. Вдохновение приходит со страницы в Википедии: "По сути, посетитель позволяет добавлять новые виртуальные функции в семейство классов, не изменяя сами классы; вместо этого создается класс посетителя, который реализует все соответствующие специализации виртуальной функции. Посетитель принимает ссылку на экземпляр в качестве входных данных и реализует цель посредством двойной отправки ".
Двойная диспетчеризация здесь необходима благодаря матрице 3х2
Вот как будет выглядеть установка -
Я написал пример, чтобы ответить на другой вопрос, код и его объяснение упоминаются здесь.
Я нашел это проще в следующих ссылках:
В http://web.archive.org/web/20120618142236/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; }
}
}
Как вы видите выше, вы BilsterPack
содержат пары таблеток, так что вам нужно умножить количество пар на 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
Интерфейс является общим для использования в других сценариях.
Добавив метод 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
Этот метод предназначен для отчетности и представления, и это нарушает принцип единой ответственности. Так что лучше использовать шаблон посетителя, чтобы преодолеть проблему.
Краткое описание шаблона посетителя. Все классы, которые требуют модификации, должны реализовывать метод accept. Клиенты вызывают этот метод accept для выполнения некоторых новых действий с этим семейством классов, расширяя тем самым их функциональность. Клиенты могут использовать этот метод accept для выполнения широкого спектра новых действий, передавая разные классы посетителей для каждого конкретного действия. Класс посетителя содержит несколько переопределенных методов посещения, определяющих, как выполнить одно и то же конкретное действие для каждого класса в семье. Эти методы посещения получают экземпляр для работы.
Когда вы могли бы рассмотреть возможность его использования
- Когда у вас есть семейство классов, вы знаете, что вам придется добавлять много новых действий, но по какой-то причине вы не сможете изменить или перекомпилировать семейство классов в будущем.
- Если вы хотите добавить новое действие и полностью определить это новое действие в одном классе посетителя, а не распределять его по нескольким классам.
- Когда ваш босс говорит, что вы должны создать ряд классов, которые должны что-то сделать прямо сейчас!... но на самом деле никто точно не знает, что это за штука.
Кей Хорстманн имеет прекрасный пример того, где можно применить Visitor в своей книге по дизайну и шаблонам. Он суммирует проблему:
Сложные объекты часто имеют сложную структуру, состоящую из отдельных элементов. Некоторые элементы могут снова иметь дочерние элементы.... Операция над элементом посещает его дочерние элементы, применяет к ним операцию и объединяет результаты.... Однако добавить новые операции в такой дизайн нелегко.
Причина, по которой это нелегко, заключается в том, что операции добавляются в сами классы структуры. Например, представьте, что у вас есть файловая система:
Вот некоторые операции (функциональные возможности), которые мы могли бы реализовать с помощью этой структуры:
- Показать имена элементов узла (список файлов)
- Вывести рассчитанный размер элементов узла (где размер каталога включает в себя размер всех его дочерних элементов)
- и т.п.
Вы можете добавить функции к каждому классу в FileSystem для реализации операций (и люди делали это в прошлом, так как совершенно очевидно, как это сделать). Проблема заключается в том, что всякий раз, когда вы добавляете новую функциональность (строка "и т. Д." Выше), вам может понадобиться добавлять все больше и больше методов в классы структуры. В какой-то момент, после некоторого количества операций, которые вы добавили в свое программное обеспечение, методы в этих классах больше не имеют смысла с точки зрения функциональной сплоченности классов. Например, у вас есть FileNode
у этого есть метод calculateFileColorForFunctionABC()
для реализации новейших функций визуализации в файловой системе.
Шаблон посетителя (как и многие шаблоны проектирования) возник из-за боли и страданий разработчиков, которые знали, что существует лучший способ изменить свой код, не требуя большого количества изменений повсюду, а также соблюдая принципы хорошего дизайна (высокая согласованность, низкая связь).). По моему мнению, трудно понять полезность многих моделей, пока вы не почувствуете эту боль. Объяснение боли (подобно тому, как мы пытаемся сделать выше с помощью добавляемых функций и т. Д.) Занимает место в объяснении и отвлекает. По этой причине сложно понять закономерности.
Посетитель позволяет нам отделить функциональные возможности от структуры данных (например, FileSystemNodes
) из самих структур данных. Шаблон позволяет дизайну соблюдать целостность - классы структуры данных проще (у них меньше методов), а также функциональные возможности заключены в Visitor
Реализации. Это делается с помощью двойной диспетчеризации (что является сложной частью шаблона): использование accept()
методы в структуре классов и visitX()
методы в классах Visitor (функциональность):
Эта структура позволяет нам добавлять новые функциональные возможности, которые работают над структурой в качестве конкретных посетителей (без изменения классов структуры).
Например, PrintNameVisitor
который реализует функциональность списка каталогов, и PrintSizeVisitor
который реализует версию с размером. Мы могли бы вообразить, что однажды у нас будет "ExportXMLVisitor", который генерирует данные в XML, или другой посетитель, который генерирует их в JSON, и т. Д. У нас может даже быть посетитель, который отображает мое дерево каталогов с использованием графического языка, такого как DOT, для визуализации. с другой программой.
В заключение: сложность Visitor с его двойной диспетчеризацией означает, что его сложнее понять, кодировать и отлаживать. Короче говоря, он имеет высокий гик-фактор и повторяет принцип KISS. В опросе, проведенном исследователями, было показано, что Visitor является спорным паттерном (не было единого мнения о его полезности). Некоторые эксперименты даже показали, что он не облегчает поддержку кода.
На мой взгляд, объем работы по добавлению новой операции более или менее одинаков при использовании Visitor Pattern
или прямое изменение структуры каждого элемента. Кроме того, если бы я должен был добавить новый класс элемента, скажем, Cow
будет затронут интерфейс Operation, который распространяется на все существующие классы элементов, поэтому требуется перекомпиляция всех классов элементов. Так в чем смысл?
Шаблон посетителя как та же подземная реализация для программирования Аспектного Объекта.
Например, если вы определяете новую операцию без изменения классов элементов, с которыми она работает
Я не понимал эту модель, пока не наткнулся на статью дяди Боба и не прочитал комментарии. Рассмотрим следующий код:
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)
выглядит одинаково, это на самом деле отличается, так как он вызывает различные перегрузки посетителя.
Мне очень нравится описание и пример из http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html.
Предполагается, что у вас есть основная иерархия классов, которая является фиксированной; возможно, это от другого поставщика, и вы не можете вносить изменения в эту иерархию. Тем не менее, вы намереваетесь добавить новые полиморфные методы в эту иерархию, что означает, что обычно вам нужно что-то добавлять в интерфейс базового класса. Таким образом, дилемма заключается в том, что вам нужно добавить методы в базовый класс, но вы не можете коснуться базового класса. Как вы справляетесь с этим?
Шаблон проектирования, который решает такую проблему, называется "посетитель" (последний в книге "Шаблоны проектирования") и основывается на схеме двойной диспетчеризации, показанной в последнем разделе.
Шаблон посетителя позволяет расширить интерфейс основного типа, создав отдельную иерархию классов типа Visitor для виртуализации операций, выполняемых с основным типом. Объекты первичного типа просто "принимают" посетителя, а затем вызывают динамически связанную функцию-член посетителя.
Посетитель позволяет добавлять новые виртуальные функции в семейство классов, не изменяя сами классы; вместо этого создается класс посетителя, который реализует все соответствующие специализации виртуальной функции.
Структура посетителей:
Используйте шаблон 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
Ссылаться на
исходная статья
Больше подробностей
шаблон позволяет добавлять поведение к отдельному объекту, статически или динамически, без влияния на поведение других объектов из того же класса
Похожие сообщения:
На основании превосходного ответа @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");
}
}
Что произойдет, если вам нужно добавить метод "Walk" здесь? Это будет болезненно для всего дизайна.
В то же время добавление метода "Прогулка" порождает новые вопросы. А как насчет "Ешь" или "Спи"? Должны ли мы действительно добавлять новый метод в иерархию Animal для каждого нового действия или операции, которые мы хотим добавить? Это некрасиво и самое главное, мы никогда не сможем закрыть интерфейс Animal. Таким образом, с помощью шаблона посетителя мы можем добавить новый метод в иерархию без изменения иерархии!
Итак, просто проверьте и запустите этот пример C#:
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);
}
}
}
Хотя я понял, как и когда, я никогда не понимал, почему. В случае, если это помогает кому-то с опытом в языке, подобном C++, вы должны прочитать это очень внимательно.
Для ленивых мы используем шаблон посетителя, потому что "хотя виртуальные функции отправляются динамически в C++, перегрузка функций выполняется статически".
Или, другими словами, чтобы убедиться, что 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;
}
}
Ваш вопрос, когда знать:
я не первый код с шаблоном посетителя. я кодирую стандарт и жду, когда возникнет необходимость, а затем рефакторинг. Допустим, у вас есть несколько платежных систем, которые вы установили по одной за раз. Во время оформления заказа у вас может быть много условий 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()
Вы можете увидеть, как реализовать это из числа примеров здесь, я просто покажу вам вариант использования.
Если вы хотите иметь функциональные объекты для типов данных объединения, вам понадобится шаблон посетителей.
Вы можете спросить, что такое объекты функций и типы данных объединения, тогда стоит прочитать http://www.ccs.neu.edu/home/matthias/htdc.html
Спасибо за потрясающее объяснение @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");
}
}