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

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

ПРИМЕР ЗАДАЧИ: сделать отправку и приём сообщений между пользователем-клиентом и службой техподдержки.

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

chat_support_mediator.classes.php

Содержит классы:
  • ChatSupportMediator – чат между пользователем и техподдержкой;
  • Message – сообщение участника чата;
  • User –  абстрактный класс пользователя;
  • UserClient – пользователь-клиент, участник чата, наследник класса User;
  • TechAgent – специалист техподдержки, участник чата, наследник класса User.

Рассмотрим классы подробнее.

ChatSupportMediator

Чат для общения между клиентами и сотрудниками техподдержки. Именно этот класс выступает в роли посредника, осуществляя обмен сообщений между участниками чата. Класс содержит следующие свойства и методы:
  • $users – массив ссылок на участников чата, представленные объектами класса User;
  • $messagesHistory – массив с историей сообщений;
  • addUser($user) – добавляет пользователя в чат;
  • getMessagesHistory() – возвращает массив с историей сообщений чата;
  • sendMessage($message) – отправляет сообщение $message участникам чата и затем помещает сообщения в историю переписки. Метод обходит свойство-массив $users, содержащее участников чата и передаёт сообщение $message, каждому пользователю (за исключением автора сообщения), вызывая у них метод receiveMessage($message).
/**
 * Чат техподдержки (класс-посредник при обмене сообщений между пользователем и техподдержкой)
 */
class ChatSupportMediator
{
    /**
     * Массив участников чата
     * @var User[]
     */
    private $users = array();

    /**
     * Массив с историей сообщений
     * @var Message[]
     */
    private $messagesHistory = array();

    /**
     * Добавляет пользователя в участники чата
     * @param User $user - добавляемый пользователь
     */
    public function addUser($user)
    {
        $this->users[$user->getId()] = $user;
    }

    /**
     * Возвращает массив с историей сообщений чата
     * @return Message[]
     */
    public function getMessagesHistory()
    {
        return $this->messagesHistory;
    }

    /**
     * Отправляет сообщение участникам чата
     * @param Message $message - сообщение
     */
    public function sendMessage($message)
    {
        //Передаём сообщение участникам чата
        foreach ($this->users as $user) {
            if ($user->getId() != $message->getAuthorId()) {
                $user->receiveMessage($message);
            }
        }

        //Добавляем сообщение в историю чата
        $this->messagesHistory[] = $message;
    }
}

Message

Представляет класс сообщения. Состоит из следующих свойств и методов:
  • $author – автор сообщения (объект класса User);
  • $text – текст сообщения;
  • $dateCreate – дата создания (указывается в виде метки времени);
  • Конструктор __construct($author, $text) – конструктор, создаёт объект сообщения;
  • getAuthorId() – возвращает ID автора сообщения;
  • getText() – возвращает текст сообщения;
  • getDateCreate() – возвращает дату создания сообщения;
  • getAuthorTitle() – возвращает заголовок автора. Используется, чтобы выводить не только имя автора сообщения, но ещё при необходимости добавлять другую информацию. Например, для сотрудников техподдержки в вывод добавляется их должность.
/**
 * Сообщение
 */
class Message
{
    /**
     * Автор сообщения
     * @var User
     */
    private $author;

    /**
     * Текст сообщения
     * @var string
     */
    private $text;

    /**
     * Дата создания (указывается в виде метки времени)
     * @var int
     */
    private $dateCreate;

    /**
     * Конструктор. Создаёт объект сообщения
     * @param User $author - автор сообщения
     * @param string $text - текст сообщения
     */
    public function __construct($author, $text)
    {
        $this->author = $author;
        $this->text = $text;
        $this->dateCreate = time();
    }

    /**
     * Возвращает ID автора сообщения
     * @return int
     */
    public function getAuthorId()
    {
        return $this->author->getId();
    }

    /**
     * Возвращает текст сообщения
     * @return string
     */
    public function getText()
    {
        return $this->text;
    }

    /**
     * Возвращает дату создания сообщения
     * @return int
     */
    public function getDateCreate()
    {
        return $this->dateCreate;
    }


    /**
     * Возвращает заголовок автора
     * @return string
     */
    public function getAuthorTitle()
    {
        return $this->author->getTitle();
    }
}

User

Представляет абстрактный класс пользователя чата. Состоит из следующих свойств и методов:
  • $chat – чат, в котором принимает участие пользователь. Содержит ссылку на объект класса ChatSupportMediator;
  • $id – ID пользователя;
  • $name – имя пользователя;
  • Конструктор __construct($chat, $id, $name) – конструктор, создаёт объект пользователя (в аргументах указывается чат, ID пользователя и его имя);
  • getId() – возвращает ID пользователя;
  • getName() – возвращает имя пользователя;
  • getTitle() – возвращает заголовок пользователя. Используется, чтобы выводить не только имя автора сообщения, но ещё при необходимости добавлять другую информацию. Например, для сотрудников техподдержки в вывод добавляется их должность.
  • sendMessage($text) – выполняет отправку сообщения в чат. Внутри себя метод передаёт сообщение в чат с помощью вызова одноимённого метода sendMessage($message) у свойства $chat (объект класса ChatSupportMediator), который затем пересылает сообщение всем остальным участникам чата;
  • receiveMessage($message) – выполняет приём сообщения из чата. В нашем тестовом примере текст присланного сообщения выводится на экран.
/**
 * Абстрактный класс пользователя чата
 */
abstract class User
{
    /**
     * Чат, в котором участвует пользователь
     */
    private $chat;

    /**
     * ID пользователя
     * @var int
     */
    protected $id;

    /**
     * Имя
     * @var string
     */
    protected $name;

    /**
     * Конструктор. Создаёт объект пользователя
     * @param ChatSupportMediator $chat - чат, в котором участвует пользователь
     * @param int $id - ID пользователя
     * @param string $name - имя
     */
    public function __construct($chat, $id, $name)
    {
        $this->id = $id;
        $this->name = $name;

        //Добавляем пользователя в чат
        $this->chat = $chat;
        $this->chat->addUser($this);
    }

    /**
     * Возвращает ID пользователя
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

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

    /**
     * Возвращает заголовок пользователя
     * @return string
     */
    public function getTitle()
    {
        return $this->name;
    }

    /**
     * Выполняет отправку сообщения в чат
     */
    public function sendMessage($text)
    {
        $message = new Message($this, $text);
        $this->chat->sendMessage($message);
    }

    /**
     * Приём сообщения из чата
     * @param Message $message - сообщение
     */
    public function receiveMessage($message)
    {
        echo '
        КОМУ: '.$this->getTitle().'<br>
        ОТ: '.$message->getAuthorTitle().'<br>
        '.$message->getText().'<br><br>';
        
        //Тут размещается код, который выводит сообщение в чат пользователя
    }
}

UserClient

Пользователь-клиент, наследник класса User. Своих новых свойств и методов класс не имеет, использует свойства и методы родительского класса User.
/**
 * Пользователь-клиент
 */
class UserClient extends User
{}

TechAgent

Сотрудник техподдержки, наследник класса User. Содержит следующие свойства и методы:
  • $position – должность сотрудника;
  • Конструктор __construct($chat, $id, $name, $position) – конструктор, создаёт объект сотрудника (в аргументах указывается чат, ID пользователя, его имя и должность);
  • getTitle() – возвращает заголовок пользователя. В отличие от родительского метода, данный метод в результат добавляет ещё должность сотрудника.
/**
 * Специалист техподдержки
 */
class TechAgent extends User
{
    /**
     * Должность сотрудника
     * @var string
     */
    private $position;

    /**
     * Конструктор. Создаёт объект специалиста
     * @param ChatSupportMediator $chat - чат, в котором участвует пользователь
     * @param int $id - ID сотрудника
     * @param string $name - имя
     * @param string $position - должность сотрудника
     */
    public function __construct($chat, $id, $name, $position)
    {
        //Вызываем конструктор родителя
        parent::__construct($chat, $id, $name);

        $this->position = $position;
    }

    /**
     * Возвращает заголовок пользователя
     * @return string
     */
    public function getTitle()
    {
        return $this->name.' ('.($this->position).')';
    }
}

test.php

Содержит тестовый скрипт, в котором сначала создаётся чат $chat (объект класса ChatSupportMediator):
//Создаём чат
$chat = new ChatSupportMediator();

Далее создаются участники чата – клиент и сотрудник техподдержики:
//Создаём пользователя-клиента
$userId = 'u1';
$userClient = new UserClient($chat, $userId, 'Ольга');

//Создаём специалиста техподдержки
$userId = 't1';
$techAgent_1 = new TechAgent($chat, $userId, 'Александр Зуев', 'Бухгалтерская служба');

Затем происходит обмен сообщениями между клиентом и техподдержкой
//Переписка между лиентом и специалистом техподдержки
$userClient->sendMessage('Здравствуйте. Почему повысился тариф?');

$techAgent_1->sendMessage('Здравствуйте. Сейчас посмотрю, 2-3 минуты');
$techAgent_1->sendMessage('Посмотрел. Прошлая стоимость тарифа сохранялась на протяжении почти трёх лет, но из-за инфляции услуга для нас стала убыточной');
$techAgent_1->sendMessage('Компания была вынуждена рано или поздно повысить цены');
$techAgent_1->sendMessage('За месяц до повышения мы оповестили всех клиентов о грядущем увеличении стоимости на 15%');

$userClient->sendMessage('Ясно. Скажите, а можно вызвать мастера на дом?');
$techAgent_1->sendMessage('Думаю, что да. Подождите минутку, позову в чат профильного специалиста');

Отмечу, что у класса пользователя User в методе приёма сообщений receiveMessage($message) реализован вывод на экран текста пришедшего сообщения, а так же имени отправителя и получателя.

В конце скрипта выводится история сообщений. Полный код скрипта test.php
<?
//Подключаем классы для работы с чатом техподдержки
require_once(__DIR__.'/chat_support_mediator.classes.php');

//Создаём чат
$chat = new ChatSupportMediator();

//Создаём пользователя-клиента
$userId = 'u1';
$userClient = new UserClient($chat, $userId, 'Ольга');

//Создаём специалиста техподдержки
$userId = 't1';
$techAgent_1 = new TechAgent($chat, $userId, 'Александр Зуев', 'Бухгалтерская служба');

//Переписка между лиентом и специалистом техподдержки
$userClient->sendMessage('Здравствуйте. Почему повысился тариф?');

$techAgent_1->sendMessage('Здравствуйте. Сейчас посмотрю, 2-3 минуты');
$techAgent_1->sendMessage('Посмотрел. Прошлая стоимость тарифа сохранялась на протяжении почти трёх лет, но из-за инфляции услуга для нас стала убыточной');
$techAgent_1->sendMessage('Компания была вынуждена рано или поздно повысить цены');
$techAgent_1->sendMessage('За месяц до повышения мы оповестили всех клиентов о грядущем увеличении стоимости на 15%');

$userClient->sendMessage('Ясно. Скажите, а можно вызвать мастера на дом?');
$techAgent_1->sendMessage('Думаю, что да. Подождите минутку, позову в чат профильного специалиста');

//Создаём второго специалиста техподдержки
$userId = 't2';
$techAgent_2 = new TechAgent($chat, $userId, 'Илья Туманов', 'Консультант розничных клиентов');

//Продолжаем переписку
$techAgent_2->sendMessage('Ольга, здравствуйте. В какой день и во сколько вам будет удобно принять нашего мастера?');
$userClient->sendMessage('Удобно на 5 февраля, в 10-11 часов утра');
$techAgent_2->sendMessage('Отлично. Заявку на выезд мастера создал');
$userClient->sendMessage('Спасибо');

//Выводим историю переписки чата
echo '<br>ИСТОРИЯ ПЕРЕПИСКИ:<br><br>';
$num = 0;
$messageHistory = $chat->getMessagesHistory();
foreach ($messageHistory as $message) {
    $num++;
    echo $num.') '.$message->getAuthorTitle().'<br>';
    echo $message->getText().'<br>';
    echo '--------------------<br>';
}
?>

Результат работы скрипта test.php
КОМУ: Александр Зуев (Бухгалтерская служба)
ОТ: Ольга
Здравствуйте. Почему повысился тариф?

КОМУ: Ольга
ОТ: Александр Зуев (Бухгалтерская служба)
Здравствуйте. Сейчас посмотрю, 2-3 минуты

КОМУ: Ольга
ОТ: Александр Зуев (Бухгалтерская служба)
Посмотрел. Прошлая стоимость тарифа сохранялась на протяжении почти трёх лет, но из-за инфляции услуга для нас стала убыточной

КОМУ: Ольга
ОТ: Александр Зуев (Бухгалтерская служба)
Компания была вынуждена рано или поздно повысить цены

КОМУ: Ольга
ОТ: Александр Зуев (Бухгалтерская служба)
За месяц до повышения мы оповестили всех клиентов о грядущем увеличении стоимости на 15%

КОМУ: Александр Зуев (Бухгалтерская служба)
ОТ: Ольга
Ясно. Скажите, а можно вызвать мастера на дом?

КОМУ: Ольга
ОТ: Александр Зуев (Бухгалтерская служба)
Думаю, что да. Подождите минутку, позову в чат профильного специалиста

КОМУ: Ольга
ОТ: Илья Туманов (Консультант розничных клиентов)
Ольга, здравствуйте. В какой день и во сколько вам будет удобно принять нашего мастера?

КОМУ: Александр Зуев (Бухгалтерская служба)
ОТ: Илья Туманов (Консультант розничных клиентов)
Ольга, здравствуйте. В какой день и во сколько вам будет удобно принять нашего мастера?

КОМУ: Александр Зуев (Бухгалтерская служба)
ОТ: Ольга
Удобно на 5 февраля, в 10-11 часов утра

КОМУ: Илья Туманов (Консультант розничных клиентов)
ОТ: Ольга
Удобно на 5 февраля, в 10-11 часов утра

КОМУ: Ольга
ОТ: Илья Туманов (Консультант розничных клиентов)
Отлично. Заявку на выезд мастера создал

КОМУ: Александр Зуев (Бухгалтерская служба)
ОТ: Илья Туманов (Консультант розничных клиентов)
Отлично. Заявку на выезд мастера создал

КОМУ: Александр Зуев (Бухгалтерская служба)
ОТ: Ольга
Спасибо

КОМУ: Илья Туманов (Консультант розничных клиентов)
ОТ: Ольга
Спасибо


ИСТОРИЯ ПЕРЕПИСКИ:

1) Ольга
Здравствуйте. Почему повысился тариф?
--------------------
2) Александр Зуев (Бухгалтерская служба)
Здравствуйте. Сейчас посмотрю, 2-3 минуты
--------------------
3) Александр Зуев (Бухгалтерская служба)
Посмотрел. Прошлая стоимость тарифа сохранялась на протяжении почти трёх лет, но из-за инфляции услуга для нас стала убыточной
--------------------
4) Александр Зуев (Бухгалтерская служба)
Компания была вынуждена рано или поздно повысить цены
--------------------
5) Александр Зуев (Бухгалтерская служба)
За месяц до повышения мы оповестили всех клиентов о грядущем увеличении стоимости на 15%
--------------------
6) Ольга
Ясно. Скажите, а можно вызвать мастера на дом?
--------------------
7) Александр Зуев (Бухгалтерская служба)
Думаю, что да. Подождите минутку, позову в чат профильного специалиста
--------------------
8) Илья Туманов (Консультант розничных клиентов)
Ольга, здравствуйте. В какой день и во сколько вам будет удобно принять нашего мастера?
--------------------
9) Ольга
Удобно на 5 февраля, в 10-11 часов утра
--------------------
10) Илья Туманов (Консультант розничных клиентов)
Отлично. Заявку на выезд мастера создал
--------------------
11) Ольга
Спасибо
--------------------

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