Реализация паттерна Chain of Responsibility на примере котиков в PHP

Kate

Administrator
Команда форума
Если вы когда‑нибудь пытались настроить бизнес‑логику в своём проекте так, чтобы она не выглядела как свалка if-else и работала хорошо, то этот материал для вас. Сегодня мы разберём один из самых приятных паттернов — Chain of Responsibility, или «Цепочка обязанностей».

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

Используйте Chain of Responsibility, когда:

  1. Логика обработки запроса должна быть модульной.
  2. Нужно динамически менять последовательность обработки.
  3. Вы хотите облегчить добавление новых обработчиков.
Сразу перейдем к коду

Реализация паттерна на примере магазина котиков​

Архитектура магазина котиков​

Вот как будет выглядеть наш процесс:

  1. Проверка наличия товара.
  2. Проверка возраста покупателя.
  3. Проверка оплаты.
  4. Упаковка заказа.
Каждое из этих действий — это отдельный обработчик в цепочке.

Интерфейс обработчика​

Начнём с базового интерфейса, который будут реализовывать наши обработчики.

<?php

interface HandlerInterface
{
public function setNext(HandlerInterface $handler): HandlerInterface;
public function handle(array $request): ?array;
}

abstract class AbstractHandler implements HandlerInterface
{
private ?HandlerInterface $nextHandler = null;

public function setNext(HandlerInterface $handler): HandlerInterface
{
$this->nextHandler = $handler;
return $handler;
}

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

return $request;
}
}
Интерфейс HandlerInterface определяет контракт для всех обработчиков, а базовый класс AbstractHandler реализует передачу запроса следующему обработчику.

Обработчики​

Теперь создадим обработчики для проверки заказа. Начнем с проверки наличия заказа:

<?php

class StockHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if ($request['stock'] <= 0) {
throw new RuntimeException('Товара нет в наличии.');
}

error_log("Товар в наличии: {$request['stock']} единиц.");
return parent::handle($request);
}
}
Теперь реализуем проверку возраста покупателя:

<?php

class AgeVerificationHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if ($request['age'] < 18) {
throw new RuntimeException('Покупатель слишком молод.');
}

error_log("Возраст покупателя ({$request['age']}) прошёл проверку.");
return parent::handle($request);
}
}
Проверка оплаты:

<?php

class PaymentHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
if (empty($request['payment']) || !$request['payment']) {
throw new RuntimeException('Оплата не прошла.');
}

error_log("Оплата успешно завершена: {$request['payment_id']}.");
return parent::handle($request);
}
}
Упаковка и подготовка к доставке:

<?php

class PackagingHandler extends AbstractHandler
{
public function handle(array $request): ?array
{
error_log("Товар упакован и готов к доставке.");
$request['status'] = 'ready_for_delivery';
return parent::handle($request);
}
}

Сборка цепочки​

Теперь объединим все обработчики в цепочку.

<?php

$request = [
'stock' => 5,
'age' => 25,
'payment' => true,
'payment_id' => 'PAY12345',
];

$stockHandler = new StockHandler();
$ageHandler = new AgeVerificationHandler();
$paymentHandler = new PaymentHandler();
$packagingHandler = new PackagingHandler();

$stockHandler->setNext($ageHandler)
->setNext($paymentHandler)
->setNext($packagingHandler);

try {
$result = $stockHandler->handle($request);
echo "Заказ успешно обработан: " . json_encode($result, JSON_PRETTY_PRINT);
} catch (RuntimeException $e) {
error_log("Ошибка обработки заказа: " . $e->getMessage());
echo "Ошибка: " . $e->getMessage();
}
Если что‑то идёт не так, выбрасываем RuntimeException, а все важные этапы логируются через error_log (или можно заменить на тот же Monolog).

Не забываем покрыть код тестами:

<?php

use PHPUnit\Framework\TestCase;

class ChainTest extends TestCase
{
public function testStockHandlerFailsWhenOutOfStock()
{
$handler = new StockHandler();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Товара нет в наличии.');

$handler->handle(['stock' => 0]);
}

public function testChainProcessesRequestSuccessfully()
{
$request = [
'stock' => 5,
'age' => 25,
'payment' => true,
'payment_id' => 'PAY12345',
];

$stockHandler = new StockHandler();
$ageHandler = new AgeVerificationHandler();
$paymentHandler = new PaymentHandler();
$packagingHandler = new PackagingHandler();

$stockHandler->setNext($ageHandler)
->setNext($paymentHandler)
->setNext($packagingHandler);

$result = $stockHandler->handle($request);

$this->assertEquals('ready_for_delivery', $result['status']);
}
}

Что ещё можно улучшить?​

  1. Динамическая конфигурация цепочки.
    Например, настраивать последовательность обработчиков через тот же JSON или YAML.
  2. Производительность.
    Для больших цепочек можно добавить кэширование результатов, чтобы не проходить одни и те же проверки повторно.
  3. Логирование.
    Подключаем Monolog для более подробного логирования.
 
Сверху