Весняний РОЗПРОДАЖ

Ланцюжок обов'язків

Також відомий як: Ланцюг відповідальностей, CoR, Chain of Command, Chain of Responsibility

Суть патерна

Ланцюжок обов’язків — це поведінковий патерн проектування, що дає змогу передавати запити послідовно ланцюжком обробників. Кожен наступний обробник вирішує, чи може він обробити запит сам і чи варто передавати запит далі ланцюжком.

Патерн Ланцюжок обов'язків

Проблема

Уявіть, що ви робите систему прийому онлайн-замовлень. Ви хочете обмежити до неї доступ так, щоб тільки авторизовані користувачі могли створювати замовлення. Крім того, певні користувачі, які володіють правами адміністратора, повинні мати повний доступ до замовлень.

Ви швидко збагнули, що ці перевірки потрібно виконувати послідовно. Адже користувача можна спробувати «залогувати» у систему, якщо його запит містить логін і пароль. Але, якщо така спроба не вдалась, то перевіряти розширені права доступу просто немає сенсу.

Проблема, яку вирішує Ланцюжок обов'язків

Запит проходить ряд перевірок перед доступом до системи замовлень.

Протягом наступних кількох місяців вам довелося додати ще декілька таких послідовних перевірок.

  • Хтось слушно зауважив, що непогано було б перевіряти дані, що передаються в запиті, перед тим, як вносити їх до системи — раптом запит містить дані про покупку неіснуючих продуктів.

  • Хтось запропонував блокувати масові надсилання форми з одним і тим самим логіном, щоб запобігти підбору паролів ботами.

  • Хтось зауважив, що непогано було б діставати форму замовлення з кешу, якщо вона вже була одного разу показана.

З часом код перевірок стає все більш заплутаним

З часом код перевірок стає все більш заплутаним.

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

Підтримувати такий код стало не тільки вкрай незручно, але й витратно. Аж ось одного прекрасного дня ви отримуєте завдання рефакторингу...

Рішення

Як і багато інших поведінкових патернів, ланцюжок обов’язків базується на тому, щоб перетворити окремі поведінки на об’єкти. У нашому випадку кожна перевірка переїде до окремого класу з одним методом виконання. Дані запиту, що перевіряється, передаватимуться до методу як аргументи.

А тепер справді важливий етап. Патерн пропонує зв’язати всі об’єкти обробників в один ланцюжок. Кожен обробник міститиме посилання на наступного обробника в ланцюзі. Таким чином, після отримання запиту обробник зможе не тільки опрацювати його самостійно, але й передати обробку наступному об’єкту в ланцюжку.

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

І останній штрих. Обробник не обов’язково повинен передавати запит далі. Причому ця особливість може бути використана різними шляхами.

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

Обробники слідують в ланцюжку один за іншим

Обробники слідують в ланцюжку один за іншим.

Але є й інший підхід, коли обробники переривають ланцюг, тільки якщо вони можуть обробити запит. У цьому випадку запит рухається ланцюгом, поки не знайдеться обробник, який зможе його обробити. Дуже часто такий підхід використовується для передачі подій, що генеруються у класах графічного інтерфейсу внаслідок взаємодії з користувачем.

Наприклад, коли користувач клікає по кнопці, програма будує ланцюжок з об’єкта цієї кнопки, всіх її батьківських елементів і загального вікна програми на кінці. Подія кліку передається цим ланцюжком до тих пір, поки не знайдеться об’єкт, здатний її обробити. Цей приклад примітний ще й тим, що ланцюжок завжди можна виділити з деревоподібної структури об’єктів, в яку зазвичай і згорнуті елементи користувацького інтерфейсу.

Ланцюжок можна виділити навіть із дерева об'єктів

Ланцюжок можна виділити навіть із дерева об’єктів.

Дуже важливо, щоб усі об’єкти ланцюжка мали спільний інтерфейс. Зазвичай кожному конкретному обробникові достатньо знати тільки те, що наступний об’єкт ланцюжка має метод виконати. Завдяки цьому зв’язки між об’єктами ланцюжка будуть більш гнучкими. Крім того, ви зможете формувати ланцюжки на льоту з різноманітних об’єктів, не прив’язуючись до конкретних класів.

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

Приклад спілкування з підтримкою

Приклад спілкування з підтримкою.

Ви купили нову відеокарту. Вона автоматично визначилася й почала працювати під Windows, але у вашій улюбленій Ubuntu «завести» її не вдалося. Ви телефонуєте до служби підтримки виробника, але без особливих сподівань на вирішення проблеми.

Спочатку ви чуєте голос автовідповідача, який пропонує вибір з десяти стандартних рішень. Жоден з варіантів не підходить, і робот з’єднує вас з живим оператором.

На жаль, звичайний оператор підтримки вміє спілкуватися тільки завченими фразами і давати тільки шаблонні відповіді. Після чергової пропозиції «вимкнути і ввімкнути комп’ютер» ви просите зв’язати вас зі справжніми інженерами.

Оператор перекидає дзвінок черговому інженерові, який знемагає від нудьги у своїй комірчині. От він вже точно знає, як вам допомогти! Інженер розповідає вам, де завантажити драйвери та як налаштувати їх під Ubuntu. Запит вирішено. Ви кладете слухавку.

Структура

Структура класів патерна Ланцюжок обов’язківСтруктура класів патерна Ланцюжок обов’язків
  1. Обробник визначає спільний для всіх конкретних обробників інтерфейс. Зазвичай достатньо описати один метод обробки запитів, але іноді тут може бути оголошений і метод встановлення наступного обробника.

  2. Базовий обробник — опціональний клас, який дає змогу позбутися дублювання одного і того самого коду в усіх конкретних обробниках.

    Зазвичай цей клас має поле для зберігання посилання на наступного обробника у ланцюжку. Клієнт зв’язує обробників у ланцюг, подаючи посилання на наступного обробника через конструктор або сетер поля. Також в цьому класі можна реалізувати базовий метод обробки, який би просто перенаправляв запити наступному обробнику, перевіривши його наявність.

  3. Конкретні обробники містять код обробки запитів. При отриманні запиту кожен обробник вирішує, чи може він обробити запит, а також чи варто передати його наступному об’єкту.

    У більшості випадків обробники можуть працювати самостійно і бути незмінними, отримавши всі необхідні деталі через параметри конструктора.

  4. Клієнт може сформувати ланцюжок лише один раз і використовувати його протягом всього часу роботи програми, так і перебудовувати його динамічно, залежно від логіки програми. Клієнт може відправляти запити будь-якому об’єкту ланцюжка, не обов’язково першому з них.

Псевдокод

У цьому прикладі Ланцюжок обов’язків відповідає за показ контекстної допомоги для активних елементів інтерфейсу користувача.

Структура класів прикладу патерна Ланцюжок обов'язків

Графічний інтерфейс побудований за допомогою компонувальника, де кожен елемент має посилання на свій елемент-контейнер. Ланцюжок можна вибудувати, пройшовши по всіх контейнерах, у які вкладено елемент.

Графічний інтерфейс програми зазвичай структурований у вигляді дерева. Клас Діалог, який відображає все вікно програми, — це корінь дерева. Діалог містить Панелі, які, в свою чергу, можуть містити або інші вкладені панелі, або прості елементи на зразок Кнопок.

Прості елементи можуть показувати невеликі підказки, якщо для них вказано допоміжний текст. Але є й більш складні компоненти, для яких цей спосіб демонстрації допомоги занадто простий. Вони визначають власний спосіб відображення контекстної допомоги.

Структура класів прикладу патерна Ланцюжок обов'язків

Приклад виклику контекстної допомоги у ланцюжку об’єктів UI.

Коли користувач наводить вказівник миші на елемент і тисне клавішу F1, програма надсилає цьому елементу запит щодо показу допомоги. Якщо він не містить жодної довідкової інформації, запит подорожує списком контейнерів элемента, доки не знаходиться той, що може відобразити допомогу.

// Інтерфейс обробників.
interface ComponentWithContextualHelp is
    method showHelp()


// Базовий клас простих компонентів.
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string

    // Контейнер, що містить компонент, служить в якості
    // наступної ланки ланцюга.
    protected field container: Container

    // Базова поведінка компонента заключається в тому, щоб
    // показати вспливаючу підказку, якщо для неї задано текст.
    // А якщо ні — перенаправити запит своєму контейнеру, якщо
    // той існує.
    method showHelp() is
        if (tooltipText != null)
            // Показати підказку.
        else
            container.showHelp()


// Контейнери можуть містити як прості компоненти, так й інші
// контейнери. Тут формуються зв'язки ланцюжка. Клас успадкує
// метод showHelp від свого батька.
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// Більшість конкретних компонентів влаштує базова поведінка
// допомоги із вспливаючою підказкою, що вони успадкують від
// класу Component.
class Button extends Component is
    // ...

// Але складні компоненти можуть перевизначати метод показу
// допомоги по-своєму. Але і в цьому випадку вони завжди можуть
// повернутися до базової реалізації, викликавши метод батька.
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            // Показати модальне вікно з допомогою.
        else
            super.showHelp()

// ...те саме, що й вище...
class Dialog extends Container is
    field wikiPageURL: string

    method showHelp() is
        if (wikiPageURL != null)
            // Відкрити сторінку Wiki в браузері.
        else
            super.showHelp()


// Клієнтський код.
class Application is
    // Кожна програма конфігурує ланцюжок по-своєму.
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPageURL = "http://..."
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does..."
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is an OK button that..."
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ...
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // Уявіть, що тут відбудеться.
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

Застосування

Якщо програма має обробляти різноманітні запити багатьма способами, але заздалегідь невідомо, які конкретно запити надходитимуть і які обробники для них знадобляться.

За допомогою Ланцюжка обов’язків ви можете зв’язати потенційних обробників в один ланцюг і по отриманню запита по черзі питати кожного з них, чи не хоче він обробити даний запит.

Якщо важливо, щоб обробники виконувалися один за іншим у суворому порядку.

Ланцюжок обов’язків дозволяє запускати обробників один за одним у тій послідовності, в якій вони стоять в ланцюзі.

Якщо набір об’єктів, здатних обробити запит, повинен задаватися динамічно.

У будь-який момент ви можете втрутитися в існуючий ланцюжок і перевизначити зв’язки так, щоби прибрати або додати нову ланку.

Кроки реалізації

  1. Створіть інтерфейс обробника і опишіть в ньому основний метод обробки.

    Продумайте, в якому вигляді клієнт повинен передавати дані запиту до обробника. Найгнучкіший спосіб — це перетворити дані запиту на об’єкт і повністю передавати його через параметри методу обробника.

  2. Є сенс у тому, щоб створити абстрактний базовий клас обробників, аби не дублювати реалізацію методу отримання наступного обробника в усіх конкретних обробниках.

    Додайте до базового обробника поле для збереження посилання на наступний елемент ланцюжка. Встановлюйте початкове значення цього поля через конструктор. Це зробить об’єкти обробників незмінюваними. Але якщо програма передбачає динамічну перебудову ланцюжків, можете додати ще й сетер для поля.

    Реалізуйте базовий метод обробки так, щоб він перенаправляв запит наступному об’єкту, перевіривши його наявність. Це дозволить повністю приховати поле-посилання від підкласів, давши їм можливість передавати запити далі ланцюгом, звертаючись до батьківської реалізації методу.

  3. Один за іншим створіть класи конкретних обробників та реалізуйте в них методи обробки запитів. При отриманні запиту кожен обробник повинен вирішити:

    • Чи може він обробити запит, чи ні?
    • Чи потрібно передавати запит наступному обробникові, чи ні?
  4. Клієнт може збирати ланцюжок обробників самостійно, спираючись на свою бізнес-логіку, або отримувати вже готові ланцюжки ззовні. В останньому випадку ланцюжки збираються фабричними об’єктами, спираючись на конфігурацію програми або параметри оточення.

  5. Клієнт може надсилати запити будь-якому обробникові ланцюга, а не лише першому. Запит передаватиметься ланцюжком, допоки який-небудь обробник не відмовиться передавати його далі або коли буде досягнуто кінець ланцюга.

  6. Клієнт повинен знати про динамічну природу ланцюжка і бути готовим до таких випадків:

    • Ланцюжок може складатися з одного об’єкта.
    • Запити можуть не досягати кінця ланцюга.
    • Запити можуть досягати кінця, залишаючись необробленими.

Переваги та недоліки

  • Зменшує залежність між клієнтом та обробниками.
  • Реалізує принцип єдиного обов’язку.
  • Реалізує принцип відкритості/закритості.
  • Запит може залишитися ніким не опрацьованим.

Відносини з іншими патернами

  • Ланцюжок обов’язків, Команда Посередник та Спостерігач показують різні способи роботи тих, хто надсилає запити, та тих, хто їх отримує:

    • Ланцюжок обов’язків передає запит послідовно через ланцюжок потенційних отримувачів, очікуючи, що один з них обробить запит.
    • Команда встановлює непрямий односторонній зв’язок від відправників до одержувачів.
    • Посередник прибирає прямий зв’язок між відправниками та одержувачами, змушуючи їх спілкуватися опосередковано, через себе.
    • Спостерігач передає запит одночасно всім зацікавленим одержувачам, але дозволяє їм динамічно підписуватися або відписуватися від таких повідомлень.
  • Ланцюжок обов’язків часто використовують разом з Компонувальником. У цьому випадку запит передається від дочірніх компонентів до їхніх батьків.

  • Обробники в Ланцюжкові обов’язків можуть бути виконані у вигляді Команд. В цьому випадку роль запиту відіграє контекст команд, який послідовно подається до кожної команди у ланцюгу.

    Але є й інший підхід, в якому сам запит є Командою, надісланою ланцюжком об’єктів. У цьому випадку одна і та сама операція може бути застосована до багатьох різних контекстів, представлених у вигляді ланцюжка.

  • Ланцюжок обов’язків та Декоратор мають дуже схожі структури. Обидва патерни базуються на принципі рекурсивного виконання операції через серію пов’язаних об’єктів. Але є декілька важливих відмінностей.

    Обробники в Ланцюжку обов’язків можуть виконувати довільні дії, незалежні одна від одної, а також у будь-який момент переривати подальшу передачу ланцюжком. З іншого боку, Декоратори розширюють певну дію, не ламаючи інтерфейс базової операції і не перериваючи виконання інших декораторів.

Приклади реалізації патерна

Ланцюжок обов’язків на C# Ланцюжок обов’язків на C++ Ланцюжок обов’язків на Go Ланцюжок обов’язків на Java Ланцюжок обов’язків на PHP Ланцюжок обов’язків на Python Ланцюжок обов’язків на Ruby Ланцюжок обов’язків на Rust Ланцюжок обов’язків на Swift Ланцюжок обов’язків на TypeScript