Привет, Хабр!
Если вы когда‑нибудь пытались настроить бизнес‑логику в своём проекте так, чтобы она не выглядела как свалка if-else и работала хорошо, то этот материал для вас. Сегодня мы разберём один из самых приятных паттернов — Chain of Responsibility, или «Цепочка обязанностей».
Вместо кучи условий, которые как минимум трудно читать, а как максимум невозможно поддерживать, мы строим гибкую архитектуру. Каждый этап обработки запроса становится отдельным модулем. А благодаря возможности менять порядок этих модулей или добавлять новые, система легко масштабируется.
Используйте Chain of Responsibility, когда:
Логика обработки запроса должна быть модульной.
Нужно динамически менять последовательность обработки.
Вы хотите облегчить добавление новых обработчиков.
Сразу перейдем к коду
Реализация паттерна на примере магазина котиков
Архитектура магазина котиков
Вот как будет выглядеть наш процесс:
Проверка наличия товара.
Проверка возраста покупателя.
Проверка оплаты.
Упаковка заказа.
Каждое из этих действий — это отдельный обработчик в цепочке.
Интерфейс обработчика
Начнём с базового интерфейса, который будут реализовывать наши обработчики.
<?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']); } }
Что ещё можно улучшить?
Динамическая конфигурация цепочки.
Например, настраивать последовательность обработчиков через тот же JSON или YAML.Производительность.
Для больших цепочек можно добавить кэширование результатов, чтобы не проходить одни и те же проверки повторно.Логирование.
Подключаем Monolog для более подробного логирования.
Как вы заметили, паттерн упрощает сложные процессы, разбивая их на независимые шаги. А какое применение паттерну находили вы? Делитесь в комментариях!
Всем PHP-разработчикам рекомендую посетить открытый урок «Вебсокеты на PHP, или как написать свой чат», который пройдет 22 января в Otus. Записаться можно по ссылке.
