Подвійна відправка - лише одна з причин використання цієї моделі .
Але зауважте, що єдиний спосіб реалізації подвійної або більше диспетчеризації мовами використовує єдину парадигму диспетчеризації.
Ось причини використання шаблону:
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
інтерфейси, підкласи, дошка, загальна поведінка тощо) та дозволяють розробникам збагачувати свою бот-стратегію.
Для того, щоб зробити це, ми повинні запропонувати модель, коли дані та поведінка не є щільно сполученими в 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));
}
Недоліки відвідувачів
Шаблон відвідувачів - дуже потужний шаблон, але він також має деякі важливі обмеження, які слід враховувати перед його використанням.
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)