Я всегда думал, что объект нуждается в данных и сообщениях, чтобы действовать на нем. Когда вам нужен метод, который является внешним для объекта? Какое эмпирическое правило вы придерживаетесь, чтобы иметь посетителя? Это предполагает, что вы полностью контролируете граф объектов.
Когда вы решите использовать посетителей для своих объектов?
Ответ 1
Я всегда думал, что объект нуждается в данные и сообщения, чтобы действовать на него. Когда вы хотите использовать метод, который внешнее для объекта? Какое правило thumb вы следуете за посетителем? Это предполагает, что у вас есть полный управление графом объектов.
Иногда бывает неудобно иметь все поведение для определенного объекта, определенного в одном классе. Например, в Java, если ваш модуль требует, чтобы метод toXml
был реализован в связке классов, первоначально определенных в другом модуле, это осложнилось, потому что вы не можете написать toXml
где-то еще, чем исходный файл класса, что означает, что вы можете не расширять систему без изменения существующих источников (в Smalltalk или на других языках вы можете группировать метод в расширении, которые не привязаны к определенному файлу).
В более общем плане существует напряженность в статически типизированных языках между возможностью добавления (1) новых функций к существующим типам данных и (2) добавления новых реализаций типов данных, поддерживающих одни и те же функции, которые называются проблема выражения (страница wikipedia).
Объектно-ориентированные языки превосходят точку 2. Если у вас есть интерфейс, вы можете добавить новые реализации безопасно и легко. Функциональные языки превосходят точку 1. Они полагаются на сопоставление шаблонов/ad-hoc-полиморфизм/перегрузку, поэтому вы можете легко добавлять новые функции к существующим типам.
Шаблон посетителя - это способ поддержки точки 1 в объектно-ориентированном дизайне: вы можете легко расширить систему с помощью новых типов поведения безопасным способом (что не будет иметь места, если вы делаете вид ручного шаблона соответствие с if-else-instanceof
, потому что язык никогда не предупредит вас, если дело не будет рассмотрено).
Затем посетители обычно используются, когда существует фиксированный набор известных типов, который, я думаю, означает то, что вы имели в виду под "полным контролем графа объекта". Примеры включают токен в парсере, дерево с различными типами узлов и аналогичные ситуации.
Итак, в заключение, я бы сказал, что вы были правы в своем анализе:)
PS: шаблон посетителя хорошо работает с составным шаблоном, но они также полезны индивидуально
Ответ 2
Шаблон посетителя особенно полезен при применении операции ко всем элементам довольно сложной структуры данных, для которых обход является нетривиальным (например, перемещение по параллельным элементам или пересечение сильно взаимосвязанной структуры данных) или при реализации двойной -dispatch. Если элементы должны обрабатываться последовательно, и если двойная отправка не нужна, то обычно предпочтительнее использовать пользовательский Iterable и Iterator, тем более что он лучше подходит для других API.
Ответ 3
Иногда это просто вопрос организации. Если у вас есть n-типы объектов (например: классы) с m-видами операций (т.е. Методы), вы хотите, чтобы пары классов n * m/method были сгруппированы по классу или методом? Большинство языков OO сильно склоняются к тому, что вещи сгруппированы по классу, но бывают случаи, когда организация с помощью операции имеет больше смысла. Например, при многофазной обработке объектных графов, как и в компиляторе, часто более полезно думать о каждой фазе (то есть о операции) как о единице, а не думать обо всех операциях, которые могут произойти с определенным типом node.
Обычный прецедент для шаблона Visitor, где он более чем просто строго организован, состоит в том, чтобы разбить нежелательные зависимости. Например, обычно нежелательно, чтобы ваши объекты данных зависели от вашего уровня представления, особенно если вы предполагаете, что у вас может быть несколько слоев представления. Используя шаблон посетителя, детали слоя представления живут в объектах посетителя, а не в методах объектов данных. Сами объекты данных знают только об абстрактном интерфейсе посетителя.
Ответ 4
Я использую его много, когда нахожу, что хочу поместить метод, который будет содержать состояние в Entity/DataObject/BusinessObject, но я действительно не хочу вводить эту объективность в свой объект. Пользователь с постоянным статусом может выполнить эту работу или создать коллекцию объектов-исполнителей с сохранением состояния из моих объектов, не содержащих состояния. Особенно полезно, когда обработка работы собирается обрабатывать потоки исполнителей, многие посетители/работники с постоянным статусом могут ссылаться на ту же группу объектов, не содержащих состояния.
Ответ 5
Для меня единственная причина использовать шаблон посетителя - это когда мне нужно выполнить двойную отправку по структуре данных, подобной графу, например tree/trie.
Ответ 6
Если у вас есть следующая проблема:
Много различных и несвязанных операций необходимо выполнять над объектами node в гетерогенной совокупной структуре. Вы хотите избежать "загрязнения" классов node этими операциями. И вы не хотите запрашивать тип каждого node и набрасывать указатель на нужный тип перед выполнением нужной операции.
Затем вы можете использовать шаблон посетителя с одним из следующих способов:
- Представляет операцию, выполняемую над элементами структуры объекта.
- Определите новую операцию без изменения классов элементов, на которых она работает.
- Классический метод восстановления информации о потерянном типе.
- Сделайте правильную вещь, основанную на типе двух объектов.
- Двойная отправка
Ответ 7
Шаблон посетителя наиболее полезен, когда вам нужно, чтобы поведение изменялось по типу объекта (в иерархии классов), и это поведение можно определить в терминах открытого интерфейса, предоставляемого объектом. Поведение не является неотъемлемой частью этого объекта и не требует инкапсуляции объекта или требует его.
Я часто вижу, что посетители часто встречаются с графами/деревьями объектов, где каждый node является частью иерархии классов. Чтобы клиенты могли ходить по графику/дереву и обрабатывать каждый тип node единообразным образом, шаблон посетителя - действительно самая простая альтернатива.
Например, рассмотрим XML DOM - a node - это базовый класс, а Element, Attribute и другие типы node определяют иерархию классов.
Предположим, что необходимо вывести DOM как JSON. Поведение не является неотъемлемой частью node - если бы это было так, нам пришлось бы добавлять методы к node для обработки всех форматов, которые могут понадобиться клиенту (toJSON()
, toASN1()
, toFastInfoSet()
и т.д.). Мы может даже утверждать, что toXML()
там не принадлежит, хотя это может быть предоставлено для удобства, поскольку оно будет использоваться большинством клиентов и концептуально "ближе" к DOM, поэтому toXML может стать неотъемлемой частью node для удобства - хотя это не обязательно, и может обрабатываться как все другие форматы.
Как node и его подклассы делают свое состояние полностью доступным как методы, у нас есть вся информация, необходимая извне, чтобы иметь возможность конвертировать DOM в какой-то выходной формат. Вместо того, чтобы помещать выходные методы в объект node, мы можем использовать интерфейс Visitor с абстрактным методом accept()
на Node и реализацию в каждом подклассе.
Реализация каждого метода посетителей обрабатывает форматирование для каждого типа node. Он может сделать это, потому что все необходимое состояние доступно из методов каждого типа node.
Используя посетителя, мы открываем дверь для реализации любого желаемого формата вывода без необходимости обременять каждый класс node этой функциональностью.
Ответ 8
Я бы всегда рекомендовал использовать посетителя, когда у вас есть полное представление о том, какие классы реализуют интерфейс. Таким образом, вы не будете делать никаких не очень хороших instanceof
-коллекций, и код станет намного более читаемым. Кроме того, после того, как посетитель был реализован, его можно повторно использовать во многих местах, настоящем и будущем.
Ответ 9
Шаблон посетителя - очень естественное решение проблем с двойной отправкой. Проблема с двойной отправкой - это подмножество динамических проблем диспетчеризации, и это связано с тем, что перегрузки метода определяются статически во время компиляции, в отличие от виртуальных (переопределенных) методов, которые определяются во время выполнения.
Рассмотрим этот сценарий:
public class CarOperations {
void doCollision(Car car){}
void doCollision(Bmw car){}
}
public class Car {
public void doVroom(){}
}
public class Bmw extends Car {
public void doVroom(){}
}
public static void Main() {
Car bmw = new Bmw();
bmw.doVroom(); //calls Bmw.doVroom() - single dispatch, works out that car is actually Bmw at runtime.
CarOperations carops = new CarOperations();
carops.doCollision(bmw); //calls CarOperations.doCollision(Car car) because compiler chose doCollision overload based on the declared type of bmw variable
}
Этот код ниже принят из моего предыдущего ответа и переведен на Java. Проблема несколько отличается от приведенной выше, но демонстрирует сущность шаблона посетителя.
//This is the car operations interface. It knows about all the different kinds of cars it supports
//and is statically typed to accept only certain ICar subclasses as parameters
public interface CarVisitor {
void StickAccelerator(Toyota car);
void ChargeCreditCardEveryTimeCigaretteLighterIsUsed(Bmw car);
}
//Car interface, a car specific operation is invoked by calling PerformOperation
public interface Car {
public string getMake();
public void setMake(string make);
public void performOperation(CarVisitor visitor);
}
public class Toyota implements Car {
private string make;
public string getMake() {return this.make;}
public void setMake(string make) {this.make = make;}
public void performOperation(CarVisitor visitor) {
visitor.StickAccelerator(this);
}
}
public class Bmw implements Car{
private string make;
public string getMake() {return this.make;}
public void setMake(string make) {this.make = make;}
public void performOperation(ICarVisitor visitor) {
visitor.ChargeCreditCardEveryTimeCigaretteLighterIsUsed(this);
}
}
public class Program {
public static void Main() {
Car car = carDealer.getCarByPlateNumber("4SHIZL");
CarVisitor visitor = new SomeCarVisitor();
car.performOperation(visitor);
}
}