«1. Обзор
В этом уроке мы познакомимся с одним из поведенческих шаблонов проектирования GoF — посетителем.
Во-первых, мы объясним его цель и проблему, которую он пытается решить.
Далее мы рассмотрим UML-диаграмму посетителя и реализацию практического примера.
2. Шаблон проектирования \»Посетитель\»
Целью шаблона \»Посетитель\» является определение новой операции без внесения изменений в структуру существующего объекта.
Представьте, что у нас есть составной объект, состоящий из компонентов. Структура объекта фиксирована — мы либо не можем ее изменить, либо не планируем добавлять в структуру новые типы элементов.
Теперь, как мы можем добавить новую функциональность в наш код, не изменяя существующие классы?
Шаблон проектирования «Посетитель» может быть ответом. Проще говоря, нам нужно будет добавить функцию, которая принимает класс посетителя к каждому элементу структуры.
Таким образом, наши компоненты позволят реализации посетителя «посетить» их и выполнить любое необходимое действие над этим элементом.
Другими словами, мы будем извлекать из классов алгоритм, который будет применяться к структуре объекта.
Следовательно, мы будем хорошо использовать принцип Open/Closed, поскольку мы не будем изменять код, но мы все равно сможем расширить функциональность, предоставив новую реализацию Visitor.
3. Диаграмма UML
На приведенной выше диаграмме UML у нас есть две иерархии реализации, специализированные посетители и конкретные элементы.
Прежде всего, клиент использует реализацию Visitor и применяет ее к структуре объекта. Составной объект перебирает свои компоненты и применяет посетителя к каждому из них.
Теперь особенно актуально то, что конкретные элементы (ConcreteElementA и ConcreteElementB) принимают посетителя, просто позволяя ему посещать их.
Наконец, этот метод одинаков для всех элементов структуры, он выполняет двойную диспетчеризацию с передачей себя (через ключевое слово this) в метод посещения посетителя.
4. Реализация
Наш пример будет настраиваемым объектом Document, состоящим из конкретных элементов JSON и XML; элементы имеют общий абстрактный суперкласс Element.
Класс Document:
public class Document extends Element {
List<Element> elements = new ArrayList<>();
// ...
@Override
public void accept(Visitor v) {
for (Element e : this.elements) {
e.accept(v);
}
}
}
Класс Element имеет абстрактный метод, который принимает интерфейс Visitor:
public abstract void accept(Visitor v);
Поэтому при создании нового элемента назовите его JsonElement, нам придется обеспечить реализацию этого метода.
Однако из-за характера шаблона Посетитель реализация будет такой же, поэтому в большинстве случаев нам потребуется скопировать и вставить шаблонный код из другого, уже существующего элемента:
public class JsonElement extends Element {
// ...
public void accept(Visitor v) {
v.visit(this);
}
}
Поскольку наши элементы позволяют посещать их любому посетителю, допустим, мы хотим обработать наши элементы Document, но каждый из них по-своему, в зависимости от типа его класса.
Таким образом, у нашего посетителя будет отдельный метод для данного типа:
public class ElementVisitor implements Visitor {
@Override
public void visit(XmlElement xe) {
System.out.println(
"processing an XML element with uuid: " + xe.uuid);
}
@Override
public void visit(JsonElement je) {
System.out.println(
"processing a JSON element with uuid: " + je.uuid);
}
}
Здесь наш конкретный посетитель реализует два метода, соответственно по одному на каждый тип Элемента.
Это дает нам доступ к конкретному объекту структуры, над которым мы можем выполнять необходимые действия.
5. Тестирование
Для тестирования давайте взглянем на класс VisitorDemo:
public class VisitorDemo {
public static void main(String[] args) {
Visitor v = new ElementVisitor();
Document d = new Document(generateUuid());
d.elements.add(new JsonElement(generateUuid()));
d.elements.add(new JsonElement(generateUuid()));
d.elements.add(new XmlElement(generateUuid()));
d.accept(v);
}
// ...
}
Сначала мы создаем ElementVisitor, он содержит алгоритм, который мы применим к нашим элементам.
Затем мы настраиваем наш документ с соответствующими компонентами и применяем посетителя, который будет принят каждым элементом структуры объекта.
Вывод будет таким:
processing a JSON element with uuid: fdbc75d0-5067-49df-9567-239f38f01b04
processing a JSON element with uuid: 81e6c856-ddaf-43d5-aec5-8ef977d3745e
processing an XML element with uuid: 091bfcb8-2c68-491a-9308-4ada2687e203
Он показывает, что посетитель посетил каждый элемент нашей структуры, в зависимости от типа элемента, он отправил обработку соответствующему методу и может получить данные из каждого базового объекта. .
6. Недостатки
Как и у каждого шаблона проектирования, даже у посетителя есть свои недостатки, в частности, его использование усложняет поддержку кода, если нам нужно добавить новые элементы в структуру объекта.
«Например, если мы добавим новый YamlElement, то нам нужно будет обновить всех существующих посетителей с помощью нового метода, необходимого для обработки этого элемента. Следуя этому далее, если у нас есть десять или более конкретных посетителей, может быть обременительно обновлять их всех.
Помимо этого, при использовании этого шаблона бизнес-логика, относящаяся к одному конкретному объекту, распространяется на все реализации посетителей.
7. Заключение
Шаблон Посетитель отлично подходит для отделения алгоритма от классов, с которыми он работает. Кроме того, это упрощает добавление новой операции, просто предоставляя новую реализацию посетителя.
Кроме того, мы не зависим от интерфейсов компонентов, и если они разные, это нормально, так как у нас есть отдельный алгоритм обработки для каждого конкретного элемента.
Кроме того, посетитель может в конечном итоге агрегировать данные на основе элемента, который он пересекает.
Чтобы увидеть более специализированную версию шаблона проектирования посетителя, ознакомьтесь с шаблоном посетителя в Java NIO — использование шаблона в JDK.
Как обычно, полный код доступен в проекте Github.