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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

Источник: https://habr.com/ru/companies/otus/articles/873368/


Интересные статьи

Интересные статьи

Российский сервис управления бизнес‑процессами «Битрикс24» представил новую версию собственной ИИ‑модели — BitrixGPT 4x, созданную для упрощения выполнения повседневных задач пользователями....
В данной статье мы рассмотрим процесс создания простого OPC UA сервера с использованием популярной библиотеки node-opcua.
Многие разработчики считают принципы программирования обязательными и используют их по дефолту во всех проектах. На самом деле большинство из них нереализуемы на практике — докажем это на нескольких п...
В данной пошаговой инструкции мы подробно опишем весь процесс получения доступа к WhatsApp Business API через официального партнера Facebook — сервис Gupshup и подключени...
Я давно знаком с Битрикс24, ещё дольше с 1С-Битрикс и, конечно же, неоднократно имел дела с интернет-магазинами которые работают на нём. Да, конечно это дорого, долго, местами неуклюже...