Также известен как Visitor

Посетитель

Суть паттерна

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

Проблема

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

Зачем кому-то нужно добавлять новое поведение объектам, не меняя их код? Почему нельзя просто взять и добавить какое-то поведение в нужные классы? Вот некоторые причины:

  • Когда новое поведение идёт вразрез с тем что класс уже делает. Подумайте о принципе единственной обязанности, прежде чем вписать новый код в класс. Нарушив принцип пару раз, вы превратите свои классы в безобразную свалку.

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

  • Классы могут находиться в сторонней библиотеке, к которой у вас нет доступа. Поэтому просто дописать новый метод в класс не получится. Бывает обратная ситуация — когда вы пишете стороннюю библиотеку, и у будущего пользователя не будет возможности дописать что-то в ваш код.

Решение

Посетитель обходит связанную структуру один объект за другим, и, в зависимости от типа посещённого объекта, выполняет над ним какое-то действие.

Аналогия из жизни

Страховой агент

Представьте начинающего страхового агента, жаждущего получить новых клиентов.

Он беспорядочно ходит от дома к дому, предлагая свои услуги. Для каждого из «типов» домов, которые он посещает, у него имеется особое предложение.

  • Придя в дом к обычной семье, он предлагает оформить медицинскую страховку.
  • Придя в банк, он предлагает страховку от грабежа.
  • Придя на фабрику, он предлагает страховку предприятия от пожара и наводнения.

Структура

Схема структуры классов паттерна Посетитель
  1. Посетитель объявляет общий интерфейс для всех типов посетителей. Здесь должны быть объявлены методы посещения, по одному на каждый класс Конкретных компонентов.

    Это важно! Методы должны принимать в параметрах компонент того типа, которому соответствует принимающий метод. Другими словами, код метода должен знать с каким конкретно типом компонента он работает. Это знание не должно быть заложено только в названии метода. В языках, поддерживающих перегрузку методов, они могут и вовсе иметь одно название и отличаться типом параметра.

  2. Конкретный посетитель реализует все операции, объявленные в интерфейсе Посетителя.

    Каждый конкретный посетитель реализует какое-то особенное поведение над компонентами.

  3. Компонент определяет метод получения Посетителя. Этот метод должен принимать в параметрах любой из объектов Посетителей.

  4. Конкретный компонент реализует метод принятия. Цель этого метода — вызвать тот метод посещения, который соответствует типу компонента. Так посетитель узнает с каким компонентом он работает.

  5. Связанная структура — это коллекция или сложный составной объект (например, дерево Компоновщика).

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

Псевдокод

В этом примере Посетитель добавляет в существующую иерархию классов геометрических фигур возможность экспорта в XML.

// Сложная иерархия компонентов.
interface Graphic is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

class Shape implements Graphic is
    field id

    // Метод принятия посетителя должен быть реализован в каждом компоненте, а
    // не только в базовом классе. Это поможет программе определить какой метод
    // посетителя нужно вызвать, в случае если вы не знаете тип компонента.
    method accept(v: Visitor) is
        v.visitDot(this);

class Dot extends Shape is
    field x, y
    // ...
    method accept(v: Visitor) is
        v.visitDot(this);

class Circle extends Dot is
    field radius
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this);

class Rectangle extends Shape is
    field width, height
    // ...
    method accept(v: Visitor) is
        v.visitRectangle(this);

class CompoundGraphic implements Graphic is
    field children: array of Graphic
    // ...
    method accept(v: Visitor) is
        v.visitCompoundGraphic(this);


// Интерфейс посетителей должен содержать методы посещения каждого компонента.
// Важно, чтобы иерархия компонентов менялась редко, так как при добавлении
// нового компонента придётся менять всех существующих посетителей.
interface Visitor is
    method visitDot(d: Dot)
    method visitCircle(c: Circle)
    method visitRectangle(r: Rectangle)
    method visitCompoundGraphic(cs: CompoundGraphic)

// Конкретный посетитель реализует одну операцию для всей иерархии компонентов.
// Новая операция = новый посетитель. Посетитель выгодно применять, когда новые
// компоненты добавляются очень редко, а команды добавляются очень часто.
class XMLExportVisitor is
    method visitDot(d: Dot) is
        Export dot's id and center coordinates.

    method visitCircle(c: Circle) is
        Export circle's id, center coordinates and radius.

    method visitRectangle(r: Rectangle) is
        Export rectangle's id, left-top coordinates, width and height.

    method visitCompoundGraphic(cg: CompoundGraphic) is
        Export shape's id and the list of children ids.


// Приложение может применять посетителя к любому набору объектов компонентов,
// даже не уточняя их типы. Нужный метод посетителя будет выбран благодаря
// проходу через метод accept.
class Application is
    field allGraphics: array of Graphic

    method export() is
        exportVisitor = new XMLExportVisitor()

        foreach (allGraphics as graphic)
            graphics.accept(exportVisitor)

Вам не кажется, что вызов метода accept() – это лишнее звено здесь?

Если так, почитайте отдельно про проблему раннего и позднего связывания в нашей статье «Посетитель и Double Dispatch».

Применимость

Вам нужно выполнить операцию над всеми элементами сложной структуры данных (например, деревом объектов), причём все элементы разнородны и не имеют общего интерфейса.

Посетитель позволяет добавить одну и ту же операцию в объекты различных типов.

Над объектами сложной структуры данных, надо выполнять несколько не связанных между собой операций и вы не хотите «засорять» классы такими операциями.

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

Шаги реализации

  1. Создайте интерфейс Посетителя и объявите в нём методы посетить(к: КомпонентXXX) для каждого подкласса-компонента, с которым будет работать посетитель.

  2. Добавьте метод принять(п: Посетитель) в базовый класс иерархии компонентов.

  3. Иерархия компонентов должна знать только о базовом интерфейсе Посетителей. С другой стороны, классы посетителей будут знать обо всех классах компонентов.

  4. Для каждого нового поведения создайте свой конкретный класс посетителя и реализуйте в нём интерфейс Посетителей. Приспособьте это поведение для всех посещаемых компонентов.

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

Преимущества и недостатки

  • Упрощает добавление новых операций над всей связанной структурой объектов.
  • Объединяет родственные операции в одном классе.
  • Посетитель может накоплять состояние при обходе структуры.
  • Паттерн неоправдан, если иерархия компонентов часто меняется.
  • – Нарушает инкапсуляцию компонентов.

Отношения с другими паттернами

  • Посетитель это более мощный аналог Команды, которую можно выполнить над несколькими объектами сразу.

  • Вы можете выполнить какое-то действие над всем деревом Компоновщика при помощи Посетителя.

  • Посетитель можно использовать совместно с Итератором. Итератор будет отвечать за обход структуры данных, а Посетитель, за выполнение действий над каждым её компонентом.

Реализация в различных языках программирования

Java

Дополнительные материалы

  • Подробней о том, почему Посетитель нельзя заменить простой перегрузкой методов читайте в нашей статье «Посетитель и Double Dispatch».