Visitor

Назначение

Предоставляет удобный и единообразный способ обработать разнородные узлы структуры данных любой сложности, при этом, обеспечивая разделение кода, обрабатывающего узлы и собственно кода узлов. Шаблон применяют когда:

  • требуется выполнить операцию на всех узлах сложной структуры данных;
  • необходимо очистить бизнес-логику от кода этой дополнительной операции и механизма обхода структуры;
  • для разных узлов структуры операция применяется по-разному;
  • можно всё-таки чуть-чуть поменять код узлов структуры; *

Описание

Допустим существует сложная структура данных (дерево, к примеру). Необходимо без существенного изменения кода структуры реализовать обработку её узлов. Изменять код структуры нельзя потому что:

  1. он уже отлажен;
  2. не хочется вносить в классы код, который на самом деле не относится к этим классам (они успешно работали до этого без этого “навязанного” функционала);
  3. что если надо будет добавить какой-то новый, но аналогичный способ обработки узлов этой структуры?

Например, сейчас нам надо выгрузить какой-нибудь граф (географических объектов) в 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, как внешний класс, мог влезть в данные этого узла.

Реализация

Client
GuiClient
Клиент оперирует структурой данных из узлов и заинтересован в проведении вычислений над узлами
NodeStructure
ProjectElement
Класс определяющий структуру узлов, может быть, например, корневым узлом в дереве
Node
ProjectElement
Интерфейс для всех узлов в структуре данных. Определяет метод, в который можно передать некоторый Visitor.
Visitor
ProjectEstimatorVisitor
Интерфейс, обобщающий все классы, способные производить некоторые вычисления над узлами рассматриваемой структуры данных. Содержит отдельные методы для каждого известного типа узлов.
ConcreteNodeA
Task, Project
Реализация одной разновидности узлов, может обладать своим особым поведением. Метод accept вызывает метод visit для типа узла, который имеет этот узел. Примечание: overloading связывается статически, во время компиляции, поэтому без такого вызова не обойтись.
ConcreteVisitor1
DurationEstimatorVisitor, CostEstimatorVisitor
Реализация класса Visitor, выполняющего определенную операцию над узлами. Например, подсчет стоимости/человеко-часов. Внутри определенного метода известно какая конкретно реализация у переданного узла, поэтому можно полагаться на специфичное для него поведение.

Примеры

Есть приложение для управления проектами. Элементом проекта является другой проект (подпроект) или задача: проект может быть представлен деревом, листья которого это задачи, задачи объединяются в подпроекты. Необходимо уметь оценивать общую стоимость и длительность всего проекта. Для этого требуется произвести некоторые вычисления по всем составляющим проекта: вложенным подпроектам и задачам. При этом изменять код классов проекта и задачи не хочется, так как в нём сосредоточены только те операции, которые действительно относятся к этим сущностям в предметной области (согласно принципу 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

https://refactoring.guru/design-patterns/visitor

Baeldung - Visitor Design Pattern in Java