Pull to refresh
596.32
OTUS
Развиваем технологии, обучая их создателей

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

Level of difficultyEasy
Reading time4 min
Views2.1K

Привет, Хабр!

Если вы когда‑нибудь пытались настроить бизнес‑логику в своём проекте так, чтобы она не выглядела как свалка 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 для более подробного логирования.


Как вы заметили, паттерн упрощает сложные процессы, разбивая их на независимые шаги. А какое применение паттерну находили вы? Делитесь в комментариях!

Всем PHP-разработчикам рекомендую посетить открытый урок «Вебсокеты на PHP, или как написать свой чат», который пройдет 22 января в Otus. Записаться можно по ссылке.

Tags:
Hubs:
Total votes 17: ↑12 and ↓5+10
Comments8

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS