Назначение
Предоставляет удобный и единообразный способ обработать разнородные узлы структуры данных любой сложности, при этом, обеспечивая разделение кода, обрабатывающего узлы и собственно кода узлов. Шаблон применяют когда:
- требуется выполнить операцию на всех узлах сложной структуры данных;
- необходимо очистить бизнес-логику от кода этой дополнительной операции и механизма обхода структуры;
- для разных узлов структуры операция применяется по-разному;
- можно всё-таки чуть-чуть поменять код узлов структуры; *
Описание
Допустим существует сложная структура данных (дерево, к примеру). Необходимо без существенного изменения кода структуры реализовать обработку её узлов. Изменять код структуры нельзя потому что:
- он уже отлажен;
- не хочется вносить в классы код, который на самом деле не относится к этим классам (они успешно работали до этого без этого “навязанного” функционала);
- что если надо будет добавить какой-то новый, но аналогичный способ обработки узлов этой структуры?
Например, сейчас нам надо выгрузить какой-нибудь граф (географических объектов) в XML. Потом нас попросят выгрузить этот же граф в JSON чуть по-другому.
Поэтому мы не вносим код операции внутрь кода структуры данных, а создаем
отдельный объект Visitor, который обрабатывает узлы этой структуры, причем
каждый тип узла обрабатывается по-своему. На каждый тип узла есть свой отдельный
метод. Но как сделать так, чтобы каждый метод вызывался для соответствующего
типа узла? Ведь для этого потребуется ужасная цепочка if-else
? Для этого
используется механизм Double Dispatch. То есть всё-таки немного потребуется
изменить код узлов структуры, но изменения будут минимальны и тривиальны:
interface Visitor {
void doForCity(Node n);
void doForIndustry(Node n);
}
class City {
public void accept(Visitor v){
v.doForCity(this);
}
}
class Industry {
public void accept(Visitor v){
v.doForIndustry(this);
}
}
Недостатки:
- всё-таки требуется чуть изменить код узлов структуры (то есть если они во внешней библиотеке, такое не получится);
- для каждого типа узлов требуется свой метод в интерфейсе Visitor - соответственно, при расширении, потребуется добавить новый метод в интерфейс;
- ингода требуется расширить интерфейс узла структуры данных, чтоб Vistior, как внешний класс, мог влезть в данные этого узла.
Реализация
accept
вызывает метод visit
для типа узла, который имеет этот узел.
Примечание: overloading связывается статически, во время компиляции, поэтому без такого вызова не обойтись.Примеры
Есть приложение для управления проектами. Элементом проекта является другой проект (подпроект) или задача: проект может быть представлен деревом, листья которого это задачи, задачи объединяются в подпроекты. Необходимо уметь оценивать общую стоимость и длительность всего проекта. Для этого требуется произвести некоторые вычисления по всем составляющим проекта: вложенным подпроектам и задачам. При этом изменять код классов проекта и задачи не хочется, так как в нём сосредоточены только те операции, которые действительно относятся к этим сущностям в предметной области (согласно принципу single responsibility).
Для решения задачи применяем шаблон Vistior. Для этого определяется иерархия по посетителям Vistior - это те классы, которые производят вычисления на элементах проектов (также один класс на каждый тип вычислений по single responsibility) и иерархия по элементам проекта (почти как Composite Pattern – Design Patterns (ep 14))
Клиент производит вычисления путем передачи в корневой проект предварительно
созданный объект нужного Visitor класса. Vistior классы имеют на каждый тип
элемента проекта свой отдельный метод, внутри которого возможно обращение к
специфичным методам соответствующего типа. Какой именно метод у Vistior
вызвать, решается на уровне реализации элементов проекта. То есть каждый элемент
проекта вызывает свой метод.
Пример из Clean Code. Проблема полиморфных фигур: есть некоторое количество фигур, которые различаются характеристиками и требуется вычислять характеристики этих фигур: площадь, периметр. Необходимо придумать гибкую архитектуру, которая позволит легко добавлять как новые фигуры, так и в имеющиеся фигуры новые характеристики.
Есть два подхода:
- процедурный – объекты фигур не содержат поведения, оно реализовано в одном
месте, например в цепочке
if-else
или вswitch-case
public double area(Object shape)throws NoSuchShapeException {
if(shape instanceof Square){
Square s=(Square)shape;
return s.side*s.side;
}
else if(shape instanceof Rectangle){
Rectangle r=(Rectangle)shape;
return r.height*r.width;
}
else if(shape instanceof Circle){
Circle c=(Circle)shape;
return PI*c.radius*c.radius;
}
throw new NoSuchShapeException();
}
Удобно добавить новые операции, например perimeter()
: классы фигур и зависящие
от них классы неизменны. При добавлении новой фигуры придется менять все функции
которые работают с фигурами:area()
, perimeter()
.
- объектный – как на рисунке, объекты инкапсулируют свое поведение внутри себя.
Удобно при добавлении новой фигуры - надо просто добавить новый класс с
соответствующей реализацией, но не удобно во все фигуры добавлять новый метод
perimeter()
.
Альтернатива: реализовать через шаблон Visitor и двойную диспетчеризацию:
interface Shape {
void accept(ShapeVisitor visitor);
}
interface ShapeVisitor {
void visitSquare(Square square);
void visitCircle(Circle circle);
}
class Square implements Shape {
@Getter
private Point topLeft;
@Getter
private double side;
@Override
void accept(ShapeVisitor visitor) {
visitor.visitSquare(this);
}
}
class Circle implements Shape {
@Getter
private Point center;
@Getter
private double radius;
@Override
void accept(ShapeVisitor visitor) {
visitor.visitCircle(this);
}
}
class AreaCalculatingShapeVisitor implements ShapeVisitor {
@Getter
private double result;
@Override
void visitSquare(Square square) {
this.result = square.getSide() * square.getSide();
}
@Override
void visitCircle(Circle circle) {
this.result = PI * circle.getRadius() * circle.getRadius();
}
}
class PerimeterCalculatingShapeVisitor implements ShapeVisitor { /*...*/ }
Варианты
- Есть неидеальная версия Vistor, в которой навигация по структуре данных осуществляется не внутри структуры, а в абстрактном классе Visitor. Этот вариант имеет большую применимость, но хуже тем, что Visitor делает предположения о структуре с которой он работает, следовательно, менее переиспользуем и гибок.
Чем отличается
Composite позволяет организовать данные в иерархическую структуру, но
предполагает, что код операций будет выполняться внутри классов Component
.
Часто Visitor работает на данных организованный при помощи шаблона Composite.
Iterator также предоставляет способ обхода структур данных произвольной сложности.
Ссылки
https://java-design-patterns.com/patterns/visitor/
https://github.com/iluwatar/java-design-patterns/tree/master/visitor