НЕ МОЛЧИ!!!    Сделай что-нибудь, чтобы остановить войну России в Украине.
...бойтесь людей равнодушных - именно с их молчаливого согласия происходят все самые ужасные преступления на свете.   ("Репортаж с петлёй на шее")

Шаблон проектирования Visitor (Посетитель)

Описывается поведенческий шаблон проектирования Visitor, условия его применение, приводится пример реализации.

Содержание

  • Применение
  • Реализация
  • Итоги

Применение

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

Но при этом возникает проблема. Как Посетителю определить, метод для объекта какого класса вызвать? Полиморфизм использовать не получится, классы то разные. Чтобы выбрать нужный метод, можно, конечно, определять тип входного объекта. Но это длинный switch или множество if ‘ов, что не есть хорошо. В шаблоне Visitor проблема разрешается следующим образом. В классы вашей существующей иерархии вводится метод для приема Посетителя, а класс уже знает, какой метод Посетителя вызвать.

Удобно изначально в вашу иерархию классов ввести метод для приема Посетителя. Тогда это будет выглядеть примерно так.

//интерфейс, обеспечивающий обработку новой операции Посетителя 
interface IComponent
{
    //какие-то обязательные методы

    //метод, который будет принимать  
    public function accept(Visitor $visitor): void;
}

//компонент существующей системы классов
class ComponentA implements IComponent
{
    //методы одного из компонентов 

    public function accept(Visitor $visitor): void
    {
        $visitor->operationForComponentA($this);
    }
}

Так что, увы, не обошлось без ложки дегтя — в классы вашей иерархии все таки придется добавить метод для приема Посетителя. Но эта доработка минимальна и не требует модификации существующего кода. Но нет худа без добра. Вводимый метод accept() для приема Посетителя принимает в качестве параметра интерфейс. Поэтому для реализации еще одной новой операции на стороне надо будет лишь создать новый класс Посетителя и передавать его элементам вашей иерархии в существующий метод для приема Посетителя.

Реализация

Итак, для реализации шаблона проектирования Visitor нам понадобятся:

  • IComponent — интерфейс существующей системы классов;
  • ComponentA — класс компонента А системы;
  • ComponentB — класс компонента В системы;
  • IVisitor — интерфейс, определяющий поведение класса Visitor;
  • Visitor1 — класс Посетителя, реализующий новую операцию компонентов существующей системы.
interface IComponent
{
    //обязательные методы компонентов

    //метод, который будет принимать Посетителя
    public function accept(Visitor $visitor): void;
}
class ComponentA implements IComponent
{
    //методы одного из компонентов существующей системы
                        ...

    public function run(): void
    {
        print "Работает ComponentA";
    }

    public function accept(Visitor $visitor): void
    {
        $visitor->operationForComponentA($this);
    }
}

class ComponentB implements IComponent
{
    //методы одного из компонентов существующей системы
                        ...

    public function run(): void
    {
        print "Работает ComponentB";
    }

    public function accept(Visitor $visitor): void
    {
        $visitor->operationForComponentB($this);
    }
}

В интерфейсе Посетителя должны быть объявлены методы реализации вновь вводимой операции для всех компонентов.

interface IVisitor
{
    public function operationForComponentA(ComponentA $component): void;

    public function operationForComponentB(ComponentB $component): void;
}
class Visitor1 implements IVisitor
{
    public function operationForComponentA(ComponentA $component): void
    {
        print "Выполнение новой операции1 для компонента А";
    }

    public function operationForComponentВ(ComponentВ $component): void
    {
        print "Выполнение новой операции1 для компонента В";
    }
}

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

class Visitor2 implements IVisitor
{
    public function operationForComponentA(ComponentA $component): void
    {
        print "Выполнение новой операции2 для компонента А";
    }

    public function operationForComponentВ(ComponentВ $component): void
    {
        print "Выполнение новой операции2 для компонента В";
    }
}

Все вместе будет работать примерно так.

//массив любых объектов, в которых реализован интерфейс IVisitor
$components = [
    new ComponentA(),
    new ComponentB(),
];

//посетитель, реализующий выполнение новой операции1
$visitor1 = new Visitor1();

//посетитель, реализующий выполнение новой операции2
$visitor2 = new Visitor2();

executeOperation1(array $components, Visitor $visitor1);
executeOperation2(array $components, Visitor $visitor2);


//выполняет операцию1 для всех элементов массива
function executeOperation1(array $components, Visitor $visitor)
{
    foreach ($components as $component) {
        $component->accept($visitor1);
    }
}

//выполняет операцию2 для всех элементов массива
function executeOperation2(array $components, Visitor $visitor)
{
    foreach ($components as $component) {
        $component->accept($visitor1);
    }
}

Итоги

Итак, сегодня был рассмотрен поведенческий шаблон проектирования Visitor, позволяющий с минимальными изменениями дополнять систему различных классов новой функциональностью. Шаблон будет полезным в случаях, когда:

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

К недостаткам шаблона следует отнести:

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

Перейти к списку шаблонов проектирования.