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

7 февраля 2020
Шаблон предлагает схему, в которой объект-посетитель получает доступ к целевому объекту для выполнения различных обработок и действий. Это позволяет, не меняя класс целевого объекта, выполнять над ним различные операции, которые логически не имеют к нему прямого отношения.

ПРИМЕР ЗАДАЧИ: для проекта дома вывести в таблицу список мебели и её стоимость.

Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 2 Кб)

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

Для организации доступа Посетителя к объекту Целевого класса, между ними определяется контракт. Для этого в Целевом классе создаётся метод, вида accept($visitor), в теле которого вызывается метод Посетителя visit($this), где $this – это объект Целевого класса. Таким образом, объект Целевого класса передаётся Посетителю.

Теперь перейдём непосредственно к коду.

furniture.class.php

Файл содержит класс мебели Furniture и является Целевым классом, состоит из следующих свойств и методов:
  • $name – название мебели;
  • $price – цена;
  • Конструктор __construct($name, $price) – создаёт объект мебели;
  • getName() – возвращает название;
  • getPrice() – возвращает цену;
  • acceptVisitor($visitor) – принятие Посетителя. Метод открывает Посетителю доступ к объекту с помощью вызова метода посетителя $visitor->visit($this), в аргументах которого передаётся объект мебели. 
<?

/**
 * Мебель
 */
class Furniture
{
    /**
     * Название
     * @var string
     */
    protected $name;

    /**
     * Цена
     * @var string
     */
    protected $price;

    /**
     * Конструктор, создаёт объект мебели
     * @param string $name - название
     * @param string $name - цена
     */
    public function __construct($name, $price)
    {
        $this->name = $name;
        $this->price = $price;
    }

    /**
     * Возвращает название 
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Возвращает цену
     * @return string
     */
    public function getPrice()
    {
        return $this->price;
    }

    /**
     * Принимает посетителя
     * @param Visitor $visitor
     */
    public function acceptVisitor($visitor)
    {
        $visitor->visit($this);
    }
}

?>

home.class.php

Содержит класс дома Home и тоже представляет Целевой класс, который состоит из следующих свойств и методов:
  • $name – название дома;
  • $furniture –мебель, которая есть в доме. Представляет собой массив ссылок на объекты мебели;
  • Конструктор __construct($name) – создаёт объект дома;
  • getName() – возвращает название;
  • loadById($id) – загружает данные дома из БД по его идентификатору (ID). В демонстративных целях реального обращения к базе данных не осуществляется, вместо этого используется загрузка из статичного хранилища;
  • addFurniture($furniture) – добавление мебели в дом;
  • acceptVisitor($visitor) – принятие Посетителя. Метод также открывает Посетителю доступ к дому с помощью вызова метода Посетителя $visitor->visit($this). Обратите внимание, что помимо дома, Посетитель ещё получает доступ к мебели этого дома, см. фрагмент обработки $furniture->acceptVisitor($visitor).  
<?

/**
 * Дом
 */
class Home
{
    /**
     * Название дома
     * @var string
     */
    protected $name;

    /**
     * Мебель дома
     * @var Furniture[]
     */
    protected $furniture = array();

    /**
     * Конструктор, создаёт объект дома
     * @param string $name - название дома
     */
    public function __construct($name = '')
    {
        $this->name = $name;
    }

    /**
     * Возвращает название дома
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Загружает дом по ID
     * @param int $id - идентификатор дома
     * @return bool - вернёт 'true' в случае успешной загрузки и 'false' в случае ошибки
     */
    public function loadById($id)
    {
        //Эмуляция загрузки дома из БД
        if ($id == 121) {
            $this->name = 'Дом из клееного бруса';

            //Подключаем класс для работы с мебелью
            require_once(__DIR__.'/furniture.class.php');

            //Загружаем мебель и добавляем её в дом
            $arFurnitureList = array(
                array(
                    'name' => 'Кухонный стол', 
                    'price' => '8000',
                ),
                array(
                    'name' => 'Стулья на кухню (4 шт)', 
                    'price' => '10000',
                ),
                array(
                    'name' => 'Кухонные подвесные ящики (3 шт)', 
                    'price' => '19500',
                ),
                array(
                    'name' => 'Стол в гостинной', 
                    'price' => '12050',
                ),
                array(
                    'name' => 'Шкаф', 
                    'price' => '11500',
                ),
                array(
                    'name' => 'Диван', 
                    'price' => '18200',
                ),
                array(
                    'name' => 'ТВ-тумба', 
                    'price' => '9600',
                ),
            );
            foreach ($arFurnitureList as $item) {
                $furniture = new Furniture($item['name'], $item['price']);
                $this->addFurniture($furniture);
            }
            
            return true;
        } else {
            return false;
        }
    }

    /**
     * Добавляет мебель
     * @param Furniture $furniture - мебель
     * @return bool - вернёт 'true' в случае успешного добавления и 'false' в случае ошибки
     */
    public function addFurniture($furniture)
    {
        $this->furniture[] = $furniture;
    }

    /**
     * Принимает посетителя
     * @param Visitor $visitor
     */
    public function acceptVisitor($visitor)
    {
        $visitor->visit($this);

        foreach ($this->furniture as $furniture) {
            $furniture->acceptVisitor($visitor);
        }
    }
}

?>

table_furniture_visitor.class.php

Файл содержит следующие элементы:
  1. Visitor – интерфейс Посетителя, в котором объявлен единственный метод visit($obj), служащий для посещения Целевого объекта;
  2. TableFurnitureVisitor – класс таблицы со списком мебели. Реализует интерфейс Посетителя Visitor, состоит из следующих свойств и методов:
    • $home – ссылка на объект Дома (класс Home);
    • $furniture – массив ссылок на объекты Мебели (класс Furniture), которые находятся в доме;
    • visit($obj) – выполняет посещение объекта (дома или мебели) и сохраняется ссылку на объект в своих свойствах;
    • print() – используя данные, ранее полученные во время посещений объектов, строит таблицу со списком мебели и её ценой.
<?

/**
 * Интерфейс посетителя
 */
interface Visitor
{
    /**
     * Выполняет посещение объекта
     * @param object $obj
     */
    public function visit($obj);
}

/**
 * Таблица со сведениями о мебели дома
 */
class TableFurnitureVisitor implements Visitor
{
    private $home = null;
    private $furniture = array();

    /**
     * Посещение объекта (дома или мебели). Выполняет добавление сведений в таблицу
     * @param Object $obj - посещаемый объект
     */
    public function visit($obj)
    {
        if (get_class($obj) == 'Home') {
            $this->home = $obj;
        } else {
            $this->furniture[] = $obj;
        }
    }

    /**
     * Выводит на экран таблицу со сведениями о мебели
     * @return string
     */
    public function print()
    {
        //Выводим над таблицей название дома
        if ($this->home) {
            echo '<div style="margin: 0 0 10px 0;"><b>'.htmlspecialchars($this->home->getName()).'. Состав мебели:</b></div>';
        }

        //Вывод списка мебели
        echo '
        <table style="width: 100%; max-width: 600px;">
        <tr class="tr_head">
            <td style="width: 50%;">Название мебели</td>
            <td>Цена</td>
        </tr>
        ';
        foreach ($this->furniture as $furniture) {
            echo '
            <tr>
                <td>'.htmlspecialchars($furniture->getName()).'</td>
                <td>'.number_format($furniture->getPrice(), 0, ',', ' ').' руб.</td>
            </tr>
            ';
        }
        echo '</table>';
    }
}

?>

test.php

Содержит тестовый скрипт, в котором создаётся объект дома $home и загружаются для него данные из записи c ID = 121
//Загружаем объект дома
$home = new Home();
if ($home->loadById(121) == false) {
    echo 'Ошибка. Не удалось загрузить дом';
} else {

Затем создаётся объект $table для построения таблицы мебели и ему открывается доступ к дому
//Подключаем класс для работы с таблицей мебели дома
require_once(__DIR__.'/table_furniture_visitor.class.php');
$table = new TableFurnitureVisitor();

//Посещаем дом
$home->acceptVisitor($table);

Завершающим шагом выводим таблицу со списком мебели
//Выводим таблицу на экран
$table->print();

Полный код скрипта test.php
<html>
<head>
    <title>Тестовый скрипт</title>
    <style>
        body {
            padding: 15px 15px;
            font-family: arial, sans-serif;
            font-size: 10.5pt;
        }

        table {
            border-collapse: collapse;
        }

        table tr.tr_head {
            background-color: #eee;
        }

        table td {
            padding: 5px 8px;
            border: 1px solid #333;
        }
    </style>
</head>
<body>

<?
//Подключаем класс дома
require_once(__DIR__.'/home.class.php');

//Загружаем объект дома
$home = new Home();
if ($home->loadById(121) == false) {
    echo 'Ошибка. Не удалось загрузить дом';
} else {
    //Подключаем класс для работы с таблицей мебели дома
    require_once(__DIR__.'/table_furniture_visitor.class.php');
    $table = new TableFurnitureVisitor();

    //Посещаем дом
    $home->acceptVisitor($table);

    //Выводим таблицу на экран
    $table->print();
}
?>

</body>
</html>

Результат выполнения скрипта test.php
Название мебели Цена
Кухонный стол 8 000 руб.
Стулья на кухню (4 шт) 10 000 руб.
Кухонные подвесные ящики (3 шт) 19 500 руб.
Стол в гостинной 12 050 руб.
Шкаф 11 500 руб.
Диван 18 200 руб.
ТВ-тумба 9 600 руб.