Autumn SALE

Декоратор

Також відомий як: Wrapper, Обгортка, Decorator

Суть патерна

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

Патерн Декоратор

Проблема

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

Основою бібліотеки є клас Notifier з методом send, який приймає на вхід рядок-повідомлення і надсилає його всім адміністраторам електронною поштою. Стороння програма повинна створити й налаштувати цей об’єкт, вказавши, кому надсилати сповіщення, та використовувати його щоразу, коли щось відбувається.

Структура бібліотеки до застосування Декоратора

Сторонні програми використовують головний клас сповіщень.

В якийсь момент стало зрозуміло, що користувачам не вистачає одних тільки email-сповіщень. Деякі з них хотіли б отримувати сповіщення про критичні проблеми через SMS. Інші хотіли б отримувати їх у вигляді Facebook-повідомлень. Корпоративні користувачі хотіли би бачити повідомлення у Slack.

Бібліотека після додавання інших типів сповіщення

Кожен тип сповіщення живе у власному підкласі.

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

Але потім хтось резонно запитав, чому не можна увімкнути кілька типів сповіщень одночасно? Адже, якщо у вашому будинку раптом почалася пожежа, ви б хотіли отримати сповіщення по всіх каналах, чи не так?

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

Бібліотека після комбінування класів сповіщень

Комбінаторний вибух підкласів при поєднанні типів сповіщень.

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

Рішення

Спадкування — це перше, що приходить в голову багатьом програмістам, коли потрібно розширити яку-небудь чинну поведінку. Проте механізм спадкування має кілька прикрих проблем.

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

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

Спадкування проти Агрегації

Спадкування проти Агрегації

Декоратор має альтернативну назву — обгортка. Вона більш вдало описує суть патерна: ви розміщуєте цільовий об’єкт у іншому об’єкті-обгортці, який запускає базову поведінку об’єкта, а потім додає до результату щось своє.

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

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

Схема рішення з Декоратором

Розширені способи надсилання сповіщень стають декораторами.

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

Програма може створювати складні стеки декораторів

Програма може збирати складові об’єкти з декораторів.

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

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

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

Приклад патерна Декоратор

Одяг можна одягати кількома шарами, отримуючи комбінований ефект.

Будь-який одяг — це аналог Декоратора. Застосовуючи Декоратор, ви не змінюєте початковий клас і не створюєте дочірніх класів. Так само з одягом: вдягаючи светра, ви не перестаєте бути собою, але отримуєте нову властивість — захист від холоду. Ви можете піти далі й одягти зверху ще один декоратор — плащ, щоб захиститися від дощу.

Структура

Структура класів патерна ДекораторСтруктура класів патерна Декоратор
  1. Компонент задає загальний інтерфейс обгорток та об’єктів, що загортаються.

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

  3. Базовий декоратор зберігає посилання на вкладений об’єкт-компонент. Це може бути як конкретний компонент, так і один з конкретних декораторів. Базовий декоратор делегує всі свої операції вкладеному об’єкту. Додаткова поведінка житиме в конкретних декораторах.

  4. Конкретні декоратори — це різні варіації декораторів, що містять додаткову поведінку. Вона виконується до або після виклику аналогічної поведінки загорнутого об’єкта.

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

Псевдокод

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

Структура класів прикладу патерна Декоратор

Приклад шифрування й компресії даних за допомогою обгорток.

Програма обгортає клас даних у шифруючу та стискаючу обгортку, які при читанні видають оригінальні дані, а при записі — зашифровані та стислі.

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

// Загальний інтерфейс компонентів.
interface DataSource is
    method writeData(data)
    method readData():data

// Один з конкретних компонентів реалізує базову
// функціональність.
class FileDataSource implements DataSource is
    constructor FileDataSource(filename) { ... }

    method writeData(data) is
        // Записати дані до файлу.

    method readData():data is
        // Прочитати дані з файлу.

// Базовий клас усіх декораторів містить код обгортування.
class DataSourceDecorator implements DataSource is
    protected field wrappee: DataSource

    constructor DataSourceDecorator(source: DataSource) is
        wrappee = source

    method writeData(data) is
        wrappee.writeData(data)

    method readData():data is
        return wrappee.readData()

// Конкретні декоратори додають щось своє до базової поведінки
// обгорнутого компонента.
class EncryptionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Зашифрувати подані дані.
        // 2. Передати зашифровані дані до методу writeData
        // обгорнутого об'єкта (wrappee).

    method readData():data is
        // 1. Отримати дані з методу readData обгорнутого
        // об'єкта (wrappee).
        // 2. Розшифрувати їх, якщо вони зашифровані.
        // 3. Повернути результат.

// Декорувати можна не тільки базові компоненти, але й вже
// обгорнуті об'єкти.
class CompressionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Запакувати подані дані.
        // 2. Передати запаковані дані до методу writeData
        // обгорнутого об'єкта (wrappee).

    method readData():data is
        // 1. Отримати дані з методу readData обгорнутого
        // об'єкта (wrappee).
        // 2. Розпакувати їх, якщо вони запаковані.
        // 3. Повернути результат.


// Варіант 1. Простий приклад збирання та використання
// декораторів.
class Application is
    method dumbUsageExample() is
        source = new FileDataSource("somefile.dat")
        source.writeData(salaryRecords)
        // До файлу було занесено чисті дані.

        source = new CompressionDecorator(source)
        source.writeData(salaryRecords)
        // До файлу було занесено стислі дані.

        source = new EncryptionDecorator(source)
        // Зараз у source знаходиться зв'язка з трьох об'єктів:
        // Encryption > Compression > FileDataSource

        source.writeData(salaryRecords)
        // До файлу було занесено стислі та зашифровані дані.


// Варіант 2. Клієнтський код, який використовує зовнішнє
// джерело даних. Клас SalaryManager нічого не знає про те, як
// саме буде зчитано та записано дані. Він отримує вже готове
// джерело даних.
class SalaryManager is
    field source: DataSource

    constructor SalaryManager(source: DataSource) { ... }

    method load() is
        return source.readData()

    method save() is
        source.writeData(salaryRecords)
    // ...Інші корисні методи...


// Програма може різним шляхом збирати об'єкти, які декоруються
// залежно від умов використання.
class ApplicationConfigurator is
    method configurationExample() is
        source = new FileDataSource("salary.dat")
        if (enabledEncryption)
            source = new EncryptionDecorator(source)
        if (enabledCompression)
            source = new CompressionDecorator(source)

        logger = new SalaryManager(source)
        salary = logger.load()
    // ...

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

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

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

Якщо не можна розширити обов’язки об’єкта за допомогою спадкування.

У багатьох мовах програмування є ключове слово final, яке може заблокувати спадкування класу. Розширити такі класи можна тільки за допомогою Декоратора.

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

  1. Переконайтеся, що у вашому завданні присутні основний компонент і декілька опціональних доповнень-надбудов над ним.

  2. Створіть інтерфейс компонента, який описував би загальні методи як для основного компонента, так і для його доповнень.

  3. Створіть клас конкретного компонента й помістіть в нього основну бізнес-логіку.

  4. Створіть базовий клас декораторів. Він повинен мати поле для зберігання посилань на вкладений об’єкт-компонент. Усі методи базового декоратора повинні делегувати роботу вкладеному об’єкту.

  5. Конкретний компонент, як і базовий декоратор, повинні дотримуватися одного і того самого інтерфейсу компонента.

  6. Створіть класи конкретних декораторів, успадковуючи їх від базового декоратора. Конкретний декоратор повинен виконувати свою додаткову функціональність, а потім (або перед цим) викликати цю ж операцію загорнутого об’єкта.

  7. Клієнт бере на себе відповідальність за конфігурацію і порядок загортання об’єктів.

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

  • Більша гнучкість, ніж у спадкування.
  • Дозволяє додавати обов’язки «на льоту».
  • Можна додавати кілька нових обов’язків одразу.
  • Дозволяє мати кілька дрібних об’єктів, замість одного об’єкта «на всі випадки життя».
  • Важко конфігурувати об’єкти, які загорнуто в декілька обгорток одночасно.
  • Велика кількість крихітних класів.

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

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

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

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

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

  • Компонувальник та Декоратор мають схожі структури класів, бо обидва побудовані на рекурсивній вкладеності. Вона дозволяє зв’язати в одну структуру нескінченну кількість об’єктів.

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

    Але вони можуть і співпрацювати: Компонувальник може використовувати Декоратор, щоб перевизначити функції окремих частин дерева компонентів.

  • Архітектура, побудована на Компонувальниках та Декораторах, часто може поліпшуватися за рахунок впровадження Прототипу. Він дозволяє клонувати складні структури об’єктів, а не збирати їх заново.

  • Стратегія змінює поведінку об’єкта «зсередини», а Декоратор змінює його «ззовні».

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

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

Декоратор на C# Декоратор на C++ Декоратор на Go Декоратор на Java Декоратор на PHP Декоратор на Python Декоратор на Ruby Декоратор на Rust Декоратор на Swift Декоратор на TypeScript