handbookНаписать

patterns/behavioral/chain-of-responsibility/README.md · тема · 4 языка · 27 тестов

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

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

Проблема ​

Компания согласует закупки по сумме: до 10 000 решает менеджер, до 100 000 — директор, до миллиона — совет. Первая версия — одна функция с лестницей if amount <= 10_000 … elseif amount <= 100_000 …. Она знает всех согласующих, все пороги и их порядок. Когда на время отпуска директора его звено нужно убрать, а для филиала добавить финансового контролёра между менеджером и директором, правится всё та же функция, и каждый вариант орг-структуры становится новой веткой.

Решение ​

Каждый согласующий — отдельный объект с общим контрактом Approver и ссылкой на следующего. Получив PurchaseRequest, он сам решает: обработать или передать дальше. Если передавать некому, запрос «упал с конца цепочки» — это явная ошибка NoApprover, а не молчаливый null.

Передачу дальше пишут один раз в базовом звене (BaseApprover в PHP и TS, Approver в Python, функция passOn в Go). Конкретные звенья Manager, Director и Board проверяют только своё условие. Отправитель знает лишь первое звено, а порядок и состав решает сборка цепочки.

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

Структура ​

next

builds

PurchaseRequest

+amount: int

+purpose: string

«interface»

Approver

+approve(request) : string

«abstract»

BaseApprover

-next: Approver

+approve(request) : string

Manager

+approve(request) : string

Director

+approve(request) : string

Board

+approve(request) : string

ApprovalChain

+standard() : Approver

Сборка — единственное место, где звенья знакомятся друг с другом: Manager → Director → Board. Звено знает только «следующего» и ничего не знает о его типе, поэтому цепочку без директора (Manager → Board) собирают из тех же классов, ничего в них не меняя.

Как это собирается ​

Цепочку собирает не звено и не отправитель, а код запуска: сервис закупок при старте или DI-контейнер. Он смотрит на конфиг и решает, из каких звеньев она состоит. В примере директор в отпуске, поэтому вместо ApprovalChain::standard() собирается Manager -> Board из тех же классов. Дальше заявки идут по одной: каждая попадает в первое звено, и вызывающий код не знает, кто ответит.

Конец цепочки - обычная ситуация, а не авария. Заявку на 2 000 000 никто не берёт, NoApprover ловит обработчик заявок и записывает отказ с текстом ошибки, а остальные заявки обрабатываются дальше.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Конкретное звено — одна строка решения: «моя сумма — отвечаю, иначе — к базовому звену». Пороги включительные (10 000 ещё у менеджера), каждое звено проверяет только свой предел и не знает чужих. В Go общего базового класса нет, поэтому передачу дальше делает функция passOn(next, request), а звено — структура с полем Next.

ЯзыкОсобенность
PHPАбстрактный BaseApprover со следующим звеном в конструкторе; звенья — final, зовут parent::approve()
GoИнтерфейс и структуры с полем Next; передача дальше — функция passOn, конец цепочки — сентинел ErrNoApprover
Pythonfrozen dataclass с полем successor и super().approve(); цепочка — вложенные конструкторы
TypeScriptКлассический setNext(), который возвращает следующего: manager.setNext(director).setNext(board)

Запуск: make test T=patterns/behavioral/chain-of-responsibility

Плюсы и минусы ​

Тесты одинаковы во всех языках: 5 000 и ровно 10 000 остаются у менеджера, 10 001 уходит директору, 500 000 — совету, 2 000 000 падает с конца цепочки ошибкой с суммой и целью. Последний случай собирает цепочку без директора: 50 000 сразу получает совет, хотя классы звеньев те же.

  • ✅ Отправитель не знает получателя: знает только первое звено.
  • ✅ Звенья добавляются, убираются и переставляются в сборке, без правки самих звеньев.
  • ✅ Каждое звено — одно условие, тестируется отдельно.
  • ❌ Запрос может никем не обработаться: конец цепочки нужно обрабатывать явно.
  • ❌ Порядок звеньев — часть логики, а виден только в сборке; ошибка порядка не ловится типами.
  • ❌ Длинная цепочка плохо отлаживается: чтобы понять, кто ответил, надо пройти её всю.

Когда не применять ​

  • Обработчик выбирается по ключу (тип документа, код страны): словарь или стратегия проще и быстрее.
  • Обработчиков два и набор не меняется: обычный if понятнее.
  • Нужно, чтобы запрос обработали все, а не первый подходящий: это наблюдатель или конвейер, а не цепочка.

Отличия между языками ​

  • PHP — классическая форма GoF: абстрактный базовый класс хранит следующего и передаёт ему запрос, звенья переопределяют approve(). Следующее звено задаётся в конструкторе, поэтому цепочка неизменяема после сборки.
  • Go — наследования нет, поэтому общий код передачи — свободная функция passOn. Звено — значение-структура, цепочка — вложенные литералы Manager{Next: Director{Next: Board{}}}. Конец цепочки — сентинел-ошибка, проверка через errors.Is. Самая известная цепочка в Go — middleware net/http: см. практику.
  • Python — frozen dataclass с наследованием: поле successor (имя next занято встроенной функцией) задаётся первым позиционным аргументом, поэтому цепочка читается как Manager(Director(Board())). Часто цепочку в Python пишут проще — списком функций и циклом for, это тоже цепочка, только без ссылок между звеньями.
  • TypeScript — вариант с setNext(), как в большинстве учебников: метод возвращает следующее звено, и сборка идёт слева направо. Цена — изменяемое состояние: звено можно перецепить после сборки. Если это мешает, следующего передают в конструктор, как в PHP.

Практика ​

Учебная цепочка согласования проста: звено либо отвечает, либо передаёт дальше, порядок задан порогами сумм, а конец цепочки - ошибка.

  • В жизни главная цепочка — middleware веб-фреймворка: аутентификация, лимит запросов, валидация перед обработчиком. Там звено обогащает запрос для следующих, останавливает цепочку ответом, а от порядка звеньев зависит безопасность. Практика: middleware входящего запроса показывает это без сети, с внедряемыми часами и тестом на порядок.
  • Приём вебхуков облачной АТС: проверка подписи, дедупликация повторов и фильтр тестовых номеров стоят звеньями, а заканчивается цепочка маршрутизатором по типу события. Звенья отвечают HTTP-статусами, от которых зависит, повторит ли АТС событие, а дедупликация запоминает его только после успешного ответа следующих звеньев. Практика: приём вебхуков АТС показывает это с CRM-заглушкой, которая умеет падать.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS