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

Посетитель

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

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

Проблема

Ваша команда разрабатывает приложение, работающее с геоданными в виде графа. Узлами графа могут быть как города, так и другие локации, будь то достопримечательности, большие предприятия и так далее. Каждый узел имеет ссылки на другие, ближайшие к нему узлы. Для каждого типа узла имеется свой класс, а каждый узел представлен отдельным объектом.

Ваша задача — сделать экспорт этого графа в XML. Дело было бы плёвым, если бы вы могли редактировать классы узлов. В этом случае, можно было бы добавить метод экспорта в каждый тип узла, а затем, перебирая узлы географического графа, вызывать этот метод для каждого узла. Решение получилось бы изящным, так как полиморфизм позволил бы не привязываться к конкретным классам узлов.

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

К тому же он сомневался в том, что экспорт в XML вообще уместен в рамках этих классов. Их основная задача была связана с геоданными, а экспорт выглядит как чужеродное поведение в рамках этих классов.

Была и ещё одна причина запрета. На следующей неделе мог бы понадобиться экспорт в другом формате и вам бы снова пришлось трогать эти классы.

Решение

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

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

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

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

foreach (Node node : graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node);
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node);
    // ...

Тут не поможет даже механизм перегрузки методов (доступный в Java и C#). Если назвать все методы одинаково, то неявность реального типа узла всё равно не даст вызвать правильный метод. Механизм перегрузки всё время будет вызывать метод посетителя соответствующий типу Node, а не реального класса поданного узла.

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

// Client code
foreach (Node node : graph)
    node.accept(exportVisitor);

// City
class City is
    method accept(Visitor v) is
        v.doForCity(this);
    // ...

// Industry
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this);
    // ...

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

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

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

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

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

Структура

Схема структуры классов паттерна Посетитель
  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. Создайте интерфейс Посетителя и объявите в нём методы "посещения" для каждого подкласса-компонента, который существует в программе.

  2. Объявите абстрактный метод принятия посетителей в базовом классе иерархии компонентов.

  3. Реализуйте методы принятия во всех конкретных компонентах. Они должны переадресовывать вызовы тому методу посетителя, в котором класс параметра совпадает с текущим классом компонента.

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

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

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

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

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

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

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

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

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

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

Java

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

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