Двойная отправка - это всего лишь одна из причин использовать этот шаблон .
Но обратите внимание, что это единственный способ реализовать двойную или более диспетчеризацию в языках, использующих одну парадигму диспетчеризации.
Вот причины для использования шаблона:
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()
соответствии с реализацией посетителя. Было бы очень плохая идея , поскольку это позволит создать высокое сцепление между реализациями Публичные и штучных подклассов и , кроме того, вероятно , потребуется использовать трюк 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)