1. Обзор
В этом уроке мы познакомимся с одним из поведенческих шаблонов проектирования GoF — посетителем.
Во-первых, мы объясним его цель и проблему, которую он пытается решить.
Далее мы рассмотрим UML-диаграмму посетителя и реализацию практического примера.
2. Шаблон дизайна посетителя
Целью шаблона посетителя является определение новой операции без внесения изменений в существующую структуру объекта.
Представьте, что у нас есть составной ** ** объект, состоящий из компонентов. Структура объекта фиксирована – мы либо не можем ее изменить, либо не планируем добавлять в структуру новые типы элементов.
Теперь, как мы можем добавить новую функциональность в наш код без изменения существующих классов?
Шаблон проектирования «Посетитель» может быть ответом. Проще говоря, нам нужно будет добавить функцию, которая принимает класс посетителя к каждому элементу структуры.
Таким образом, наши компоненты позволят реализации посетителя «посетить» их и выполнить любое необходимое действие с этим элементом.
Другими словами, мы извлечем алгоритм, который будет применяться к структуре объекта, из классов.
Следовательно, мы будем хорошо использовать принцип Open/Closed, поскольку мы не будем изменять код, но мы все равно сможем расширить функциональность, предоставив новую реализацию Visitor
.
3. UML-диаграмма
На диаграмме UML выше у нас есть две иерархии реализации, специализированные посетители и конкретные элементы.
Прежде всего, клиент использует реализацию Visitor и применяет ее к структуре объекта. Составной объект перебирает свои компоненты и применяет посетителя к каждому из них.
Теперь особенно актуально то, что конкретные элементы (ConcreteElementA
и ConcreteElementB)
принимают посетителя,
просто позволяя ему посещать
их.
Наконец, этот метод одинаков для всех элементов структуры, он выполняет двойную диспетчеризацию с передачей себя (через ключевое слово this
) в метод посещения посетителя.
4. Реализация
Наш пример будет настраиваемым объектом Document
, состоящим из конкретных элементов JSON и XML; элементы имеют общий абстрактный суперкласс Element.
Класс документа
:
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
имеет абстрактный метод, который принимает интерфейс посетителя :
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);
}
// ...
}
Во- первых, мы создаем Element
Visitor, он содержит алгоритм, который мы применим к нашим элементам.
Затем мы настраиваем наш документ
с соответствующими компонентами и применяем посетителя, который будет принят каждым элементом структуры объекта.
Вывод будет таким:
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. Заключение
Шаблон Visitor отлично подходит для отделения алгоритма от классов, с которыми он работает. Кроме того, это упрощает добавление новой операции, просто предоставляя новую реализацию посетителя.
Кроме того, мы не зависим от интерфейсов компонентов, и если они разные, это нормально, так как у нас есть отдельный алгоритм обработки для каждого конкретного элемента.
Более того, посетитель может в конечном итоге агрегировать данные на основе элемента, который он пересекает.
Чтобы увидеть более специализированную версию шаблона проектирования посетителя, ознакомьтесь с шаблоном посетителя в Java NIO — использование шаблона в JDK.
Как обычно, полный код доступен на проекте Github .