Перейти к основному содержимому

Шаблон проектирования посетителей в Java

· 5 мин. чтения

1. Обзор

В этом уроке мы познакомимся с одним из поведенческих шаблонов проектирования GoF — посетителем.

Во-первых, мы объясним его цель и проблему, которую он пытается решить.

Далее мы рассмотрим UML-диаграмму посетителя и реализацию практического примера.

2. Шаблон дизайна посетителя

Целью шаблона посетителя является определение новой операции без внесения изменений в существующую структуру объекта.

Представьте, что у нас есть составной ** ** объект, состоящий из компонентов. Структура объекта фиксирована – мы либо не можем ее изменить, либо не планируем добавлять в структуру новые типы элементов.

Теперь, как мы можем добавить новую функциональность в наш код без изменения существующих классов?

Шаблон проектирования «Посетитель» может быть ответом. Проще говоря, нам нужно будет добавить функцию, которая принимает класс посетителя к каждому элементу структуры.

Таким образом, наши компоненты позволят реализации посетителя «посетить» их и выполнить любое необходимое действие с этим элементом.

Другими словами, мы извлечем алгоритм, который будет применяться к структуре объекта, из классов.

Следовательно, мы будем хорошо использовать принцип Open/Closed, поскольку мы не будем изменять код, но мы все равно сможем расширить функциональность, предоставив новую реализацию Visitor .

3. UML-диаграмма

./900aaecbca24ad63b974df3bcbfb739e.png

На диаграмме 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 .