Цепочка обязанностей (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 проверяют только своё условие. Отправитель знает лишь первое звено, а порядок и состав решает сборка цепочки.
Цепочка нужна, когда обработчиков несколько, подходящий заранее неизвестен и набор меняется. Если обработчик всегда один или выбор делается по ключу, проще словарь или стратегия.
Структура
Сборка — единственное место, где звенья знакомятся друг с другом: 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 |
| Python | frozen 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 — middlewarenet/http: см. практику. - Python —
frozendataclass с наследованием: полеsuccessor(имяnextзанято встроенной функцией) задаётся первым позиционным аргументом, поэтому цепочка читается какManager(Director(Board())). Часто цепочку в Python пишут проще — списком функций и цикломfor, это тоже цепочка, только без ссылок между звеньями. - TypeScript — вариант с
setNext(), как в большинстве учебников: метод возвращает следующее звено, и сборка идёт слева направо. Цена — изменяемое состояние: звено можно перецепить после сборки. Если это мешает, следующего передают в конструктор, как в PHP.
Практика
Учебная цепочка согласования проста: звено либо отвечает, либо передаёт дальше, порядок задан порогами сумм, а конец цепочки - ошибка.
- В жизни главная цепочка — middleware веб-фреймворка: аутентификация, лимит запросов, валидация перед обработчиком. Там звено обогащает запрос для следующих, останавливает цепочку ответом, а от порядка звеньев зависит безопасность. Практика: middleware входящего запроса показывает это без сети, с внедряемыми часами и тестом на порядок.
- Приём вебхуков облачной АТС: проверка подписи, дедупликация повторов и фильтр тестовых номеров стоят звеньями, а заканчивается цепочка маршрутизатором по типу события. Звенья отвечают HTTP-статусами, от которых зависит, повторит ли АТС событие, а дедупликация запоминает его только после успешного ответа следующих звеньев. Практика: приём вебхуков АТС показывает это с CRM-заглушкой, которая умеет падать.
Связанные темы
- Практика: middleware входящего запроса — аутентификация, лимит и валидация звеньями; порядок решает
- Практика: приём вебхуков АТС - подпись, дедупликация и фильтр перед маршрутизатором по типу события
- Декоратор (Decorator) — та же форма обёртки, но декоратор всегда передаёт вызов дальше, а звено цепочки может остановить его
- Компоновщик (Composite) — запрос часто идёт по цепочке от ребёнка к родителю в дереве компоновщика
- Стратегия (Strategy) — выбирает одного исполнителя заранее, цепочка ищет его на ходу