Ланцюжок обов'язків
Суть патерна
Ланцюжок обов’язків — це поведінковий патерн проектування, що дає змогу передавати запити послідовно ланцюжком обробників. Кожен наступний обробник вирішує, чи може він обробити запит сам і чи варто передавати запит далі ланцюжком.
Проблема
Уявіть, що ви робите систему прийому онлайн-замовлень. Ви хочете обмежити до неї доступ так, щоб тільки авторизовані користувачі могли створювати замовлення. Крім того, певні користувачі, які володіють правами адміністратора, повинні мати повний доступ до замовлень.
Ви швидко збагнули, що ці перевірки потрібно виконувати послідовно. Адже користувача можна спробувати «залогувати» у систему, якщо його запит містить логін і пароль. Але, якщо така спроба не вдалась, то перевіряти розширені права доступу просто немає сенсу.
Протягом наступних кількох місяців вам довелося додати ще декілька таких послідовних перевірок.
-
Хтось слушно зауважив, що непогано було б перевіряти дані, що передаються в запиті, перед тим, як вносити їх до системи — раптом запит містить дані про покупку неіснуючих продуктів.
-
Хтось запропонував блокувати масові надсилання форми з одним і тим самим логіном, щоб запобігти підбору паролів ботами.
-
Хтось зауважив, що непогано було б діставати форму замовлення з кешу, якщо вона вже була одного разу показана.
З кожною новою «фічою» код перевірок, що виглядав як величезний клубок умовних операторів, все більше і більше «розбухав». При зміні одного правила доводилося змінювати код усіх інших перевірок. А щоб застосувати перевірки до інших ресурсів, довелося також продублювати їхній код в інших класах.
Підтримувати такий код стало не тільки вкрай незручно, але й витратно. Аж ось одного прекрасного дня ви отримуєте завдання рефакторингу...
Рішення
Як і багато інших поведінкових патернів, ланцюжок обов’язків базується на тому, щоб перетворити окремі поведінки на об’єкти. У нашому випадку кожна перевірка переїде до окремого класу з одним методом виконання. Дані запиту, що перевіряється, передаватимуться до методу як аргументи.
А тепер справді важливий етап. Патерн пропонує зв’язати всі об’єкти обробників в один ланцюжок. Кожен обробник міститиме посилання на наступного обробника в ланцюзі. Таким чином, після отримання запиту обробник зможе не тільки опрацювати його самостійно, але й передати обробку наступному об’єкту в ланцюжку.
Передаючи запити до першого обробника ланцюжка, ви можете бути впевнені, що всі об’єкти в ланцюзі зможуть його обробити. При цьому довжина ланцюжка не має жодного значення.
І останній штрих. Обробник не обов’язково повинен передавати запит далі. Причому ця особливість може бути використана різними шляхами.
У прикладі з фільтрацією доступу обробники переривають подальші перевірки, якщо поточну перевірку не пройдено. Адже немає сенсу витрачати даремно ресурси, якщо і так зрозуміло, що із запитом щось не так.
Але є й інший підхід, коли обробники переривають ланцюг, тільки якщо вони можуть обробити запит. У цьому випадку запит рухається ланцюгом, поки не знайдеться обробник, який зможе його обробити. Дуже часто такий підхід використовується для передачі подій, що генеруються у класах графічного інтерфейсу внаслідок взаємодії з користувачем.
Наприклад, коли користувач клікає по кнопці, програма будує ланцюжок з об’єкта цієї кнопки, всіх її батьківських елементів і загального вікна програми на кінці. Подія кліку передається цим ланцюжком до тих пір, поки не знайдеться об’єкт, здатний її обробити. Цей приклад примітний ще й тим, що ланцюжок завжди можна виділити з деревоподібної структури об’єктів, в яку зазвичай і згорнуті елементи користувацького інтерфейсу.
Дуже важливо, щоб усі об’єкти ланцюжка мали спільний інтерфейс. Зазвичай кожному конкретному обробникові достатньо знати тільки те, що наступний об’єкт ланцюжка має метод виконати
. Завдяки цьому зв’язки між об’єктами ланцюжка будуть більш гнучкими. Крім того, ви зможете формувати ланцюжки на льоту з різноманітних об’єктів, не прив’язуючись до конкретних класів.
Аналогія з життя
Ви купили нову відеокарту. Вона автоматично визначилася й почала працювати під Windows, але у вашій улюбленій Ubuntu «завести» її не вдалося. Ви телефонуєте до служби підтримки виробника, але без особливих сподівань на вирішення проблеми.
Спочатку ви чуєте голос автовідповідача, який пропонує вибір з десяти стандартних рішень. Жоден з варіантів не підходить, і робот з’єднує вас з живим оператором.
На жаль, звичайний оператор підтримки вміє спілкуватися тільки завченими фразами і давати тільки шаблонні відповіді. Після чергової пропозиції «вимкнути і ввімкнути комп’ютер» ви просите зв’язати вас зі справжніми інженерами.
Оператор перекидає дзвінок черговому інженерові, який знемагає від нудьги у своїй комірчині. От він вже точно знає, як вам допомогти! Інженер розповідає вам, де завантажити драйвери та як налаштувати їх під Ubuntu. Запит вирішено. Ви кладете слухавку.
Структура
-
Обробник визначає спільний для всіх конкретних обробників інтерфейс. Зазвичай достатньо описати один метод обробки запитів, але іноді тут може бути оголошений і метод встановлення наступного обробника.
-
Базовий обробник — опціональний клас, який дає змогу позбутися дублювання одного і того самого коду в усіх конкретних обробниках.
Зазвичай цей клас має поле для зберігання посилання на наступного обробника у ланцюжку. Клієнт зв’язує обробників у ланцюг, подаючи посилання на наступного обробника через конструктор або сетер поля. Також в цьому класі можна реалізувати базовий метод обробки, який би просто перенаправляв запити наступному обробнику, перевіривши його наявність.
-
Конкретні обробники містять код обробки запитів. При отриманні запиту кожен обробник вирішує, чи може він обробити запит, а також чи варто передати його наступному об’єкту.
У більшості випадків обробники можуть працювати самостійно і бути незмінними, отримавши всі необхідні деталі через параметри конструктора.
-
Клієнт може сформувати ланцюжок лише один раз і використовувати його протягом всього часу роботи програми, так і перебудовувати його динамічно, залежно від логіки програми. Клієнт може відправляти запити будь-якому об’єкту ланцюжка, не обов’язково першому з них.
Псевдокод
У цьому прикладі Ланцюжок обов’язків відповідає за показ контекстної допомоги для активних елементів інтерфейсу користувача.
Графічний інтерфейс програми зазвичай структурований у вигляді дерева. Клас Діалог
, який відображає все вікно програми, — це корінь дерева. Діалог містить Панелі
, які, в свою чергу, можуть містити або інші вкладені панелі, або прості елементи на зразок Кнопок
.
Прості елементи можуть показувати невеликі підказки, якщо для них вказано допоміжний текст. Але є й більш складні компоненти, для яких цей спосіб демонстрації допомоги занадто простий. Вони визначають власний спосіб відображення контекстної допомоги.
Коли користувач наводить вказівник миші на елемент і тисне клавішу F1
, програма надсилає цьому елементу запит щодо показу допомоги. Якщо він не містить жодної довідкової інформації, запит подорожує списком контейнерів элемента, доки не знаходиться той, що може відобразити допомогу.
Застосування
Якщо програма має обробляти різноманітні запити багатьма способами, але заздалегідь невідомо, які конкретно запити надходитимуть і які обробники для них знадобляться.
За допомогою Ланцюжка обов’язків ви можете зв’язати потенційних обробників в один ланцюг і по отриманню запита по черзі питати кожного з них, чи не хоче він обробити даний запит.
Якщо важливо, щоб обробники виконувалися один за іншим у суворому порядку.
Ланцюжок обов’язків дозволяє запускати обробників один за одним у тій послідовності, в якій вони стоять в ланцюзі.
Якщо набір об’єктів, здатних обробити запит, повинен задаватися динамічно.
У будь-який момент ви можете втрутитися в існуючий ланцюжок і перевизначити зв’язки так, щоби прибрати або додати нову ланку.
Кроки реалізації
-
Створіть інтерфейс обробника і опишіть в ньому основний метод обробки.
Продумайте, в якому вигляді клієнт повинен передавати дані запиту до обробника. Найгнучкіший спосіб — це перетворити дані запиту на об’єкт і повністю передавати його через параметри методу обробника.
-
Є сенс у тому, щоб створити абстрактний базовий клас обробників, аби не дублювати реалізацію методу отримання наступного обробника в усіх конкретних обробниках.
Додайте до базового обробника поле для збереження посилання на наступний елемент ланцюжка. Встановлюйте початкове значення цього поля через конструктор. Це зробить об’єкти обробників незмінюваними. Але якщо програма передбачає динамічну перебудову ланцюжків, можете додати ще й сетер для поля.
Реалізуйте базовий метод обробки так, щоб він перенаправляв запит наступному об’єкту, перевіривши його наявність. Це дозволить повністю приховати поле-посилання від підкласів, давши їм можливість передавати запити далі ланцюгом, звертаючись до батьківської реалізації методу.
-
Один за іншим створіть класи конкретних обробників та реалізуйте в них методи обробки запитів. При отриманні запиту кожен обробник повинен вирішити:
- Чи може він обробити запит, чи ні?
- Чи потрібно передавати запит наступному обробникові, чи ні?
-
Клієнт може збирати ланцюжок обробників самостійно, спираючись на свою бізнес-логіку, або отримувати вже готові ланцюжки ззовні. В останньому випадку ланцюжки збираються фабричними об’єктами, спираючись на конфігурацію програми або параметри оточення.
-
Клієнт може надсилати запити будь-якому обробникові ланцюга, а не лише першому. Запит передаватиметься ланцюжком, допоки який-небудь обробник не відмовиться передавати його далі або коли буде досягнуто кінець ланцюга.
-
Клієнт повинен знати про динамічну природу ланцюжка і бути готовим до таких випадків:
- Ланцюжок може складатися з одного об’єкта.
- Запити можуть не досягати кінця ланцюга.
- Запити можуть досягати кінця, залишаючись необробленими.
Переваги та недоліки
- Зменшує залежність між клієнтом та обробниками.
- Реалізує принцип єдиного обов’язку.
- Реалізує принцип відкритості/закритості.
- Запит може залишитися ніким не опрацьованим.
Відносини з іншими патернами
-
Ланцюжок обов’язків, Команда Посередник та Спостерігач показують різні способи роботи тих, хто надсилає запити, та тих, хто їх отримує:
- Ланцюжок обов’язків передає запит послідовно через ланцюжок потенційних отримувачів, очікуючи, що один з них обробить запит.
- Команда встановлює непрямий односторонній зв’язок від відправників до одержувачів.
- Посередник прибирає прямий зв’язок між відправниками та одержувачами, змушуючи їх спілкуватися опосередковано, через себе.
- Спостерігач передає запит одночасно всім зацікавленим одержувачам, але дозволяє їм динамічно підписуватися або відписуватися від таких повідомлень.
-
Ланцюжок обов’язків часто використовують разом з Компонувальником. У цьому випадку запит передається від дочірніх компонентів до їхніх батьків.
-
Обробники в Ланцюжкові обов’язків можуть бути виконані у вигляді Команд. В цьому випадку роль запиту відіграє контекст команд, який послідовно подається до кожної команди у ланцюгу.
Але є й інший підхід, в якому сам запит є Командою, надісланою ланцюжком об’єктів. У цьому випадку одна і та сама операція може бути застосована до багатьох різних контекстів, представлених у вигляді ланцюжка.
-
Ланцюжок обов’язків та Декоратор мають дуже схожі структури. Обидва патерни базуються на принципі рекурсивного виконання операції через серію пов’язаних об’єктів. Але є декілька важливих відмінностей.
Обробники в Ланцюжку обов’язків можуть виконувати довільні дії, незалежні одна від одної, а також у будь-який момент переривати подальшу передачу ланцюжком. З іншого боку, Декоратори розширюють певну дію, не ламаючи інтерфейс базової операції і не перериваючи виконання інших декораторів.