Также известен как CoR, Chain of Command, 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