PHP: Цепочка обязанностей

Chain of Responsibility Цепочка обязанностей Chain of Responsibility

Цепочка обязанностей — это поведенческий паттерн, позволяющий передавать запрос по цепочке потенциальных обработчиков, пока один из них не обработает запрос.

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

Подробней о Цепочке обязанностей

Особенности паттерна в PHP

Сложность:

Популярность:

Применимость: Паттерн встречается в PHP не так уж часто, так как для его применения нужно, чтобы в программе были цепи объектов. Пожалуй, самым известным примером использования этого паттерна в PHP является концепция HTTP Request Middleware, описанная в PSR-15. Это обработчики запросов, которые программа запускает перед тем, как выполнить основной обработчик запроса. Если их собрать в одну цепь (что чаще всего и происходит в реальных приложениях), то получится конструкция, очень схожая с паттерном Цепочка Обязанностей.

Пример: Структура паттерна

Этот пример показывает структуру паттерна Цепочка обязанностей, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом. После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.

ChainOfResponsibilityStructural.php: Пример структуры паттерна

<?php

namespace RefactoringGuru\ChainOfResponsibility\Structural;

/**
 * Интерфейс Обработчика объявляет метод построения цепочки обработчиков. Он
 * также объявляет метод для выполнения запроса.
 */
interface Handler
{
    public function setNext(Handler $handler);

    public function handle($request);
}

/**
 * Поведение цепочки по умолчанию может быть реализовано внутри базового класса
 * обработчика.
 */
abstract class AbstractHandler implements Handler
{
    /**
     * @var Handler
     */
    private $nextHandler;

    /**
     * @param Handler $handler
     * @return Handler
     */
    public function setNext(Handler $handler)
    {
        $this->nextHandler = $handler;
        // Возврат обработчика отсюда позволит связать обработчики простым
        // способом, вот так:
        // $monkey->setNext($squirrel)->setNext($dog);
        return $handler;
    }

    public function handle($request)
    {
        if ($this->nextHandler) {
            return $this->nextHandler->handle($request);
        }
    }
}

/**
 * Все Конкретные Обработчики либо обрабатывают запрос, либо передают его
 * следующему обработчику в цепочке.
 */
class MonkeyHandler extends AbstractHandler
{
    public function handle($request)
    {
        if ($request == "Banana") {
            return "Monkey: I'll eat the ".$request.".\n";
        } else {
            return parent::handle($request);
        }
    }
}

class SquirrelHandler extends AbstractHandler
{
    public function handle($request)
    {
        if ($request == "Nut") {
            return "Squirrel: I'll eat the ".$request.".\n";
        } else {
            return parent::handle($request);
        }
    }
}

class DogHandler extends AbstractHandler
{
    public function handle($request)
    {
        if ($request == "MeatBall") {
            return "Dog: I'll eat the ".$request.".\n";
        } else {
            parent::handle($request);
        }
    }
}

/**
 * Обычно клиентский код приспособлен для работы с единственным обработчиком. В
 * большинстве случаев клиенту даже неизвестно, что этот обработчик является
 * частью цепочки.
 */
function clientCode(Handler $handler)
{
    foreach (["Nut", "Banana", "Cup of coffee"] as $food) {
        print("Client: Who wants a ".$food."?\n");
        $result = $handler->handle($food);
        if ($result) {
            print("  ".$result);
        } else {
            print("  ".$food." was left untouched.\n");
        }
    }
}

/**
 * Другая часть клиентского кода создает саму цепочку.
 */
$monkey = new MonkeyHandler();
$squirrel = new SquirrelHandler();
$dog = new DogHandler();

$monkey->setNext($squirrel)->setNext($dog);

/**
 * Клиент должен иметь возможность отправлять запрос любому обработчику, а не
 * только первому в цепочке.
 */
print("Chain: Monkey > Squirrel > Dog\n\n");
clientCode($monkey);
print("\n");

print("Subchain: Squirrel > Dog\n\n");
clientCode($squirrel);

Output.txt: Результат выполнения

Chain: Monkey > Squirrel > Dog

Client: Who wants a Nut?
  Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
  Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
  Cup of coffee was left untouched.

Subchain: Squirrel > Dog

Client: Who wants a Nut?
  Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
  Banana was left untouched.
Client: Who wants a Cup of coffee?
  Cup of coffee was left untouched.

Пример: Пример из жизни

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

Пример: Пожалуй, самым известным применением паттерна Цепочка обязанностей (CoR) в мире PHP являются промежуточные обработчики HTTP-запросов, называемые middleware. Они стали настолько популярными, что были реализованы в самом языке как часть PSR-15.

Всё это работает следующим образом: HTTP-запрос должен пройти через стек объектов middleware, прежде чем приложение его обработает. Каждое middleware может либо отклонить дальнейшую обработку запроса, либо передать его следующему middleware. Как только запрос успешно пройдёт все middleware, основной обработчик приложения сможет окончательно его обработать.

Можно отметить, что такой подход – своего рода инверсия первоначального замысла паттерна. Действительно, в стандартной реализации запрос передаётся по цепочке только в том случае, если текущий обработчик НЕ МОЖЕТ его обработать, тогда как middleware передаёт запрос дальше по цепочке, когда считает, что приложение МОЖЕТ обработать запрос. Тем не менее, поскольку middleware соединены цепочкой, вся концепция по-прежнему считается примером паттерна CoR.

ChainOfResponsibilityRealWorld.php: Пример из жизни

<?php

namespace RefactoringGuru\ChainOfResponsibility\RealWorld;

/**
 * Паттерн Цепочка обязанностей
 *
 * Назначение: Позволяет избежать привязки отправителя запроса к его получателю,
 * предоставляя возможность обработать запрос нескольким объектам.  Связывает в
 * цепочку объекты-получатели, а затем передаёт запрос по цепочке, пока некий
 * получатель не обработает его.
 *
 * Пример: Пожалуй, самым известным применением паттерна Цепочка обязанностей
 * (CoR) в мире PHP являются промежуточные обработчики HTTP-запросов, называемые
 * middleware. Они стали настолько популярными, что были реализованы в самом
 * языке как часть PSR-15.
 *
 * Всё это работает следующим образом: HTTP-запрос должен пройти через стек
 * объектов  middleware, прежде чем приложение его обработает. Каждое middleware
 * может либо  отклонить дальнейшую обработку запроса, либо передать его
 * следующему middleware. Как только запрос успешно пройдёт все middleware,
 * основной обработчик приложения сможет окончательно его обработать.
 *
 * Можно отметить, что такой подход – своего рода инверсия первоначального
 * замысла паттерна. Действительно, в стандартной реализации запрос передаётся
 * по цепочке только в том случае, если текущий обработчик НЕ МОЖЕТ его
 * обработать, тогда как middleware передаёт запрос дальше по цепочке, когда
 * считает, что  приложение МОЖЕТ обработать запрос. Тем не менее, поскольку
 * middleware соединены цепочкой, вся концепция по-прежнему считается примером
 * паттерна CoR.
 */

/**
 * Классический паттерн CoR объявляет для объектов, составляющих цепочку,
 * единственную роль – Обработчик. В нашем примере давайте проводить различие
 * между middleware и конечным обработчиком приложения, который выполняется,
 * когда запрос проходит через все объекты middleware.
 *
 * Базовый класс Middleware объявляет интерфейс для связывания объектов
 * middleware в цепочку.
 */
abstract class Middleware
{
    /**
     * @var Middleware
     */
    private $next;

    /**
     * Этот метод можно использовать для построения цепочки объектов middleware.
     */
    public function linkWith(Middleware $next): Middleware
    {
        $this->next = $next;

        return $next;
    }

    /**
     * Подклассы должны переопределить этот метод, чтобы предоставить свои
     * собственные проверки. Подкласс может обратиться к родительской реализации
     * проверки, если сам не в состоянии обработать запрос.
     */
    public function check(string $email, string $password): bool
    {
        if (! $this->next) {
            return true;
        }

        return $this->next->check($email, $password);
    }
}

/**
 * Это Конкретное Middleware проверяет, существует ли пользователь с указанными
 * учётными данными.
 */
class UserExistsMiddleware extends Middleware
{
    private $server;

    public function __construct(Server $server)
    {
        $this->server = $server;
    }

    public function check(string $email, string $password): bool
    {
        if (! $this->server->hasEmail($email)) {
            print("UserExistsMiddleware: This email is not registered!\n");

            return false;
        }

        if (! $this->server->isValidPassword($email, $password)) {
            print("UserExistsMiddleware: Wrong password!\n");

            return false;
        }

        return parent::check($email, $password);
    }
}

/**
 * Это Конкретное Middleware проверяет, имеет ли пользователь,  связанный с
 * запросом, достаточные права доступа.
 */
class RoleCheckMiddleware extends Middleware
{
    public function check(string $email, string $password): bool
    {
        if ($email === "admin@example.com") {
            print("RoleCheckMiddleware: Hello, admin!\n");

            return true;
        }
        print("RoleCheckMiddleware: Hello, user!\n");

        return parent::check($email, $password);
    }
}

/**
 * Это Конкретное Middleware проверяет, не было ли превышено максимальное число
 * неудачных запросов авторизации.
 */
class ThrottlingMiddleware extends Middleware
{
    private $requestPerMinute;

    private $request;

    private $currentTime;

    public function __construct(int $requestPerMinute)
    {
        $this->requestPerMinute = $requestPerMinute;
        $this->currentTime = time();
    }

    /**
     * Обратите внимание, что вызов parent::check можно вставить как в начале
     * этого метода, так и в конце.
     *
     * Это даёт значительно большую свободу действий, чем простой цикл по всем
     * объектам middleware. Например, middleware может изменить порядок
     * проверок,  запустив свою проверку после всех остальных.
     */
    public function check(string $email, string $password): bool
    {
        if (time() > $this->currentTime + 60) {
            $this->request = 0;
            $this->currentTime = time();
        }

        $this->request++;

        if ($this->request > $this->requestPerMinute) {
            print("ThrottlingMiddleware: Request limit exceeded!\n");
            die();
        }

        return parent::check($email, $password);
    }
}

/**
 * Это класс приложения, который осуществляет реальную обработку запроса. Класс
 * Сервер использует паттерн CoR для выполнения набора различных промежуточных
 * проверок перед запуском некоторой бизнес-логики, связанной с запросом.
 */
class Server
{
    private $users = [];

    /**
     * @var Middleware
     */
    private $middleware;

    /**
     * Клиент может настроить сервер с помощью цепочки объектов middleware.
     */
    public function setMiddleware(Middleware $middleware)
    {
        $this->middleware = $middleware;
    }

    /**
     * Сервер получает email и пароль от клиента и отправляет запрос авторизации
     * в middleware.
     */
    public function logIn(string $email, string $password)
    {
        if ($this->middleware->check($email, $password)) {
            print("Server: Authorization has been successful!\n");

            // Выполняем что-нибудь полезное для авторизованных пользователей.

            return true;
        }

        return false;
    }

    public function register(string $email, string $password)
    {
        $this->users[$email] = $password;
    }

    public function hasEmail(string $email): bool
    {
        return isset($this->users[$email]);
    }

    public function isValidPassword(string $email, string $password): bool
    {
        return $this->users[$email] === $password;
    }
}

/**
 * Клиентский код.
 */
$server = new Server();
$server->register("admin@example.com", "admin_pass");
$server->register("user@example.com", "user_pass");

// Все middleware соединены в цепочки. Клиент может построить различные
// конфигурации цепочек в зависимости от своих потребностей.
$middleware = new ThrottlingMiddleware(2);
$middleware
    ->linkWith(new UserExistsMiddleware($server))
    ->linkWith(new RoleCheckMiddleware());

// Сервер получает цепочку из клиентского кода.
$server->setMiddleware($middleware);

// ...

do {
    print("\nEnter your email:\n");
    $email = readline();
    print("Enter your password:\n");
    $password = readline();
    $success = $server->logIn($email, $password);
} while (! $success);

Output.txt: Результат выполнения

Enter your email:
asd
Enter your password:
123
UserExistsMiddleware: This email is not registered!

Enter your email:
admin@example.com
Enter your password:
wrong
UserExistsMiddleware: Wrong password!

Enter your email:
admin@example.com
Enter your password:
letmein
ThrottlingMiddleware: Request limit exceeded!



Enter your email:
admin@example.com
Enter your password:
admin_pass
RoleCheckMiddleware: Hello, admin!
Server: Authorization has been successful!