Также известен как Chain of Responsibility

Цепочка обязанностей

Суть паттерна

Связывает объекты-получатели в цепочку и передаёт запрос вдоль этой цепочки, пока его не обработают.

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

Проблема

Для начала определимся, что значит посылать запрос? Не пугайтесь, это синоним обычного вызова метода. Мы говорим «посылать запрос», т.к. клиент не уверен какой именно объект его обработает и обработает ли вообще.

Проблема в том, что объект, инициирующий запрос (например, кнопка), может не располагать информацией о том, какой объект этот запрос обработает. Кнопка не знает, находится ли она на панели, или в окне редакторе или где-то ещё; а значит, она не знает кому именно «сообщить» о своём нажали.

Кроме того, лучше не привязывать классы кнопок к классам бизнес-логики программы, если вы хотите повторно использовать эти кнопки где-нибудь ещё.

Решение

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

Как связаны объекты в цепочке? Каждый из объектов имеет ссылку на следующий объект цепочки (например, кнопка имеет ссылку на панель, панель на диалог, диалог на окно). Все объекты цепочки должны реализовать общий интерфейс, чтобы связка была гибкая и в цепочку можно было подставлять разнообразные классы.

Что происходит, когда клиент обращается к одному из объектов цепочки? Этот объект проверяет, может ли он выполнить операцию. Если да, он её тут же выполняет и возвращает результат клиенту. Если нет, он переадресует операцию следующему объекту в цепочке и так далее либо пока запрос не будет выполнен, либо пока мы не достигнем конца цепочки.

Аналогия из жизни

Связь с отделом поддержки

Представьте, что вы купили новую видеокарту к своему рабочему компьютеру. Она автоматически определилась и заработала под Windows, но в вашей любимой Ubuntu «завести» её не удалось. В надежде, вы звоните в службу поддержки.

Первым вы слышите стандартный автоответчик, предлагающий выбор из десятка стандартных проблем и решений. Ни один из вариантов не подходит и робот соединяет вас с живым оператором.

К сожалению, рядовой оператор поддержки умеет общаться только заученными фразами и давать шаблонные ответы. После очередного предложения «выключить и включить компьютер», вы просите связать вас с его супервайзером.

Оператор перебрасывает звонок супервайзеру, который знает чуточку больше о своей работе. Он понимает в чём заключается ваша проблема, но не имеет быстрого решения.

Супервайзер переадресует ваш звонок дежурному инженеру, изнывающему от скуки в своей коморке. Уж он-то знает как вам помочь и рассказывает где и как можно скачать подходящие драйвера, и как настроить их под Ubuntu.

Запрос удовлетворён. Вы кладёте трубку.

Структура

Схема структуры классов паттерна Цепочка Обязанностей
  1. Обработчик – это общий интерфейс для всех Конкретных обработчиков, определяющий полезные операции, доступные Клиенту.

    Здесь должен быть определён метод получения следующего Обработчика в цепочке. Если Обработчик — это абстрактный класс, то здесь можно хранить и ссылку на следующий объект в цепочке.

  2. Конкретный обработчик обрабатывает запрос, если может. Если не может, переадресует запрос следующему объекту в цепочке.

  3. Клиент отправляет запрос одному из объектов цепочки.

    Какому именно — ясно из контекста. Это не всегда первый объект в цепочке.

Псевдокод

В этом примере Цепочка обязанностей решает какой из окна пользовательского интерфейса будет обрабатывать событие, приходящее от пользователя. Окно интерфейса — это Контейнер, организованный в виде дерева. Оно содержит Панели, а те, в свою очередь, как другие вложенные Панели, так и отдельные элементы интерфейса. Отдельные элементы получают события от пользователя (например, клики) и передают информацию о событии вверх по цепочке своих родителей, пока не найдется элемент, способный обработать то или иное событие.

Таким образом, паттерн Цепочка обязанностей позволяет клиенту отправлять запрос, не беспокоясь о том, какой конкретно объект его обработает. Цепочка обработчиков сама определит это. Причём определение будет происходить динамически — это позволит включать и выключать определённых обработчиков на лету.

// Абстрактный интерфейс обработчика.
abstract class Component is
    field onClick: function

    // Базовый обработчик клика. Здесь будут заканчиваться передачи вызова
    // по цепочке.
    method click(x, y) is
        if (onClick != null)
            onClick(context)

    abstract method render(x, y, width, height)


// Конкретная реализация компонента. Он наследует базовую функциональность
// абстрактного обработчика.
class ContainerComponent extends Component is
    // Расширенный элемент цепочки, который имеет связи с
    // другими компонентами-обработчиками.
    field children: array of Component

    method add(child) is
        children.add(child)

    // Здесь формируются связи цепочки.
    method click(x, y) is
        if (onClick != null) then
            onClick(context)
        else if (child.atCoord(x,y))
            child.click(x, y)


// Если не можешь сам обработать клик, передай его своему дочернему компоненту,
// который находится в координате клика.
class Button extends Component is
    method render() is
        Draw a button.

class Panel extends ContainerComponent is
    method render() is
        Draw a panel and its children.


// Клиентский код.
class Application is
    // Каждое приложение конфигурирует цепочку по-своему.
    method createUI() is
        panel = new Panel(0, 0, 400, 800)
        ok = new Button(250, 760, 50, 20, "OK")
        ok.onClick({ ... })
        panel.add(ok);
        cancel = new Button(320, 760, 50, 20, "Cancel")
        cancel.onClick({ ... })
        panel.add(cancel)

    // Представьте что здесь произойдёт.
    method test() is
        panel.click(251, 761)

Применимость

Если программа содержит более одного объекта, способного обработать запрос, но не знает заранее кто из них его действительно обработает.

Вы связываете потенциальных обработчиков в одну цепь и поочерёдно спрашиваете, хочет ли данный объект обработать запрос. Если нет, двигаетесь дальше по цепочке.

Если набор объектов, способных обработать запрос, должен задаваться динамически.

В любой момент вы можете вмешаться в существующую цепочку и переназначить связи так, чтобы убрать или добавить новое звено.

Шаги реализации

  1. Создайте интерфейс Обработчика. Определите в нём операции, для выполнения которых потребуется найти подходящего обработчика в цепочке.

  2. Добавьте в интерфейс Обработчика метод получитьСледующий для получения следующего Обработчика в цепочке.

  3. Создайте классы Конкретных обработчиков и добавьте в них код связей цепочки. Поместите в каждый класс поле, содержащее ссылку на следующий объект Обработчик. Возвращайте эту ссылку в методе получитьСледующий.

  4. Добавьте код обработки запросов к классам элементов цепочки. Он должен сперва проверять, способен ли текущий объект обработать запрос. Если нет, то операция должна быть переадресована следующему объекту в цепочке.

  5. Цепочку объектов может собирать как сам клиент, так и какая-то внешняя сущность или фабрика.

  6. Клиент вызывает одну из операций Обработчика у первого объекта цепочки. Этот вызов будет рекурсивно передаваться по объектам цепочки пока либо не будет выполнен, либо не найдётся ни одного объекта, который может выполнить операцию.

  7. Стоит помнить, что цепочка объектов — динамичная. Вы не знаете сколько там объектов. Она может быть и вовсе пустой, а значит Клиент должен быть готов к тому, что вызов не вернёт никаких результатов.

Преимущества и недостатки

  • Уменьшает зависимость между клиентом и обработчиками.
  • Соблюдает принцип единственной обязанности класса.
  • Соблюдает принцип открытости/закрытости.
  • Запрос может остаться никем не обработанным.

Отношения с другими паттернами

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

    • Цепочка обязанностей передаёт запрос отправителя через цепочку потенциальных получателей, ожидая, что какой-то из них обработает запрос.
    • Команда соединяет отправителя и получателя через подкласс.
    • Посредник заставляет отправителя и получателя коммуницировать только через себя.
    • Наблюдатель позволяет динамически подключать и отключать обработчиков запроса.
  • Цепочка обязанностей может использовать Команду, чтобы работать с запросом как с объектом.

  • Цепочку обязанностей часто используют вместе с Компоновщиком. В этом случае, запрос передаётся от дочерних компонентов к их родителям.

Реализация в различных языках программирования

Java