Декораторы
LINEДекоратор - это структурный паттерн программирования, который позволяет динамически добавлять функционал к объекту. Декораторы представляют гибкую альтернативу наследованию, дают возможность написания менее связного кода и более простого тестирования.
Ниже мы рассмотрим причины применения декоратора, их возможные способы применения, диаграмму классов, и то как поддержка этого паттерна есть на уровня языка в python.
Система оружия для RPG
Представьте, вы хотите сделать игру в стиле RPG, где будут герои, монстры, локации, квесты и т.д. Сейчас вы заняты созданием системы снаряжения, а точнее системы оружия. Для начала вы создаете интерфейс Weapon, который представляет любое оружие с одним методом, который возвращает урон этого оружия, т.к. в дальнейшем вы хотите рассмотреть различные эффекты для разных оружий.
Пока рассмотрим три различных эффекта у оружий
- орижие с 50% дает двукратный урон (либо в общем случае с k% вероятностью n-кратный урон)
- оружие становится лучше при использовании, и с каждой 1000 ударов увеличивает урон на 10% (либо в общем случае каждые n ударов увеличивает урон на k%)
- оружие просто дает дополнительный фиксированный урон n с каждой атакой
Реализовать 3 класса несложно, но в какой то момент вы захотите комбинировать эффекты, скажем 1 и 2, или сразу все 3. В таком случае нам придется писать классы для оружия, которые объединяют несколько эффектов. Для 3-х эффектов всего будет 7 классов (для следующих эффектов: 1, 2, 3, 1 и 2, 1 и 3, 2 и 3, 1 и 2 и 3).
Чем больше эффектов, тем больше классов. Мы пойдем другим путем и будем использовать декораторы.
Давайте посмотрим на систему с такой точки зрения: у нас есть оружие (например меч) с базовым уроном. Затем мы отдадим меч кузнецу, который может этот меч заточить. Затем отдадим меч травнику, который нанесет на лезвие ножа яд. Затем отдадим меч еще кому нибудь, кто сможет его украсить или выгравировать узор. И т.д.
Каждый следующий человек в этой цепочке берет меч, и возвращает меч. Но каждый из них также и добавляет мечу новую функциональность, или "декорирует" его. Примерно так паттерн декоратор работает в жизни.
В итоге мы получим следующий меч. Каждая окружность разного цвета - это декоратор.

Как это будет выглядеть для нашей игры. У нас есть базовое оружие (Weapon), которое имеет урон - это может быть меч, лук, кинжал, посох и т.д.

У нас есть конкретный класс оружия, например меч.

У нас есть абстрактный декоратор. Это класс, который представляет любой абстрактный эффект для оружия. Важный момент - декоратор сам является оружием (наследуется от Weapon). Это важно, т.к. каждый из тех, кто декорировал наш меч (оружие в общем случае) принимал меч и возвращал меч.

Теперь перейдем к конкретным декораторам. У нас их было 3. Первый с вероятностью 50% возвращает двойной урон. Этот (и все следющие) декоратор наследуется от абстрактного декоратора, который наследуется от Weapon, а значит любой декоратор - тоже оружие. Также любой декоратор содержит ссылку на оружие, которое оно декорирует. Это может быть как само оружие, так и любой другой декоратор.

Декоратор, который увеличивает урон при использовании оружия

Декоратор, который просто добавляет фиксированный урон

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

Если мы захотим добавить еще один декоратор, ничего менять не придется - мы просто унаследуемся от абстрактного декоратора и добавим логику в метод damage. После чего можем скоминировать декоратор с уже имеющимися. Таким образом каждый новый класс может сильно разнообразить возможности игровой механики, при этом почти не увеличивая сложность системы. Такой дизайн соответствует принципу открытости/закрытости - мы всегда можем добавить функционал, но основная логика остается неизменной.
UML диаграмма
В общем случае диаграмма выглядит так

В конктерном примере выше, для которого эта диаграмма превратится в изображенную ниже, компонентом был класс Weapon, конкретным компонентом класс Sword.

Примеры из Java
Классическим примером декораторов служат классы ввода-ввывода в Java. Есть 2 класса, которые представляют из себя байтовые потоки ввода/вывода информации - InputStream и OutputStream. Также есть такие классы, как BufferedInputStream, GzipInputStream, ObjectInputStream, FileInputStream, FilterInputStream и множество других, которые сами являются InputStream, т.е. это декораторы InputStream. Мы можем взять небольшой функционал (чтение байтов из потока ввода) и обернуть классом, который умеет эти байты сжимать, буферизировать, фильтровать, превращать в объекты и т.д.
Примеры использования
Ниже приведены частые практические примеры использования паттерна декоратор с минимальным кодом.
- буферизация
Представим, что у нас есть класс Loader с одним методом load(int from, int to), который каким то образом загружает с сервера строки (это может быть что угодно). Зачастую выгоднее сразу загрузить несколько строк одним запросом, чем каждый раз скачивать пару строк. Таким образом, мы можем написать класс BufferedLoader, который наследуется от Loader и при запросе к себе посылает запрос оригинальному loader, при этом запрашивая данных больше, чем нужно и сохраняет их к себе. Если в следующий запрос эти данные у него есть - он сразу возвращает ответ.

Обычно операция буферизации нужна для операций ввода-вывода, например для чтения с диска. Само чтение обычно происходит быстро, но на позиционирование головки диска и нахождения нужного участка уходит много времени. Идея буферизации приводит к следующему применению - кешированию.
- Кеширование
В целом кеширование и буфферизация выполняют схожий функционал. Но при буферизации мы говорим, что нам проще взять большой кусок информации сразу, а не получать ее на порциями. При кешировании мы сохраняем часть информации, доступ к которой будет быстрее чем из основного места хранения. Наиболее частым примером - кеш базы данных. Мы берем объект из базы данных (или строку и создаем из нее объект), сохраняем в некоторую структуру (hashmap отлично подойдет), а в следующий запрос проверяем, есть ли уже эта информация в hashmap. В целом такой декоратор будет напоминать пример с буферизацией.
- Повторный вызов
Вполне возможно, что в ходе некоторой операции (особенно связанной с сетью, жеским диском и т.д.) случится непредвиденная ситуация, которая не позволит получить желаемый результат. Декоратор для повторного вызова просто пытается сделать вызов метода своего вложенного компонента, если вызов успешен - возвращает результат. Если нет - пробует снова еще несколько раз(возможно через некоторый промежуток аремени)
- Логирование
Декоратор для логирования выглядит очень просто и в тоже время естественно. Все что он делает - пишет в файл информацию о вызове метода, или, например, время выполнения метода (ниже приведен такой пример). В любой момент можно убрать или добавить этот декоратор, например для тестов.

- Ведение статистики
Пример похожий на логирование, но в этом примере декоратор не пишет всю информацию а только собирает ее, для последующего анализа (какие параметры чаще приходят, как часто вызывается метод, и т.д.). Такой декоратор должен иметь методы для возвращения статистики в некотором формате, который другой сервис забирает в определенное время (например, каждый день в 00:00).
- Фильтрация/валидация
Декоратор, который проверяет входные параметры. Например, если метод делает что то с коллекцией элементов, то такой декаратор может отфильтровывать пустые строки, null значения и т.д., заменять такие значения на значения по умолчанию, выкидывать exception или что то подобное.

- синхронизация
Еще один интересный способ применения декоратора - синхронизация. Если у нас есть класс вроде коллекции элементов, куда можно добавлять/удалять элементы, то в многопоточной среде без соответствующих примитивов синхронизации мы получим некорректный результат. Добавить сразу синхронизацию в класс тоже плохая идея, т.к. в однопоточной среде это будет приводить к ненужным задержкам.
Мы можем обернуть весь функционал декоратором, который будет просто синхронизировать основной метод - тогда основной класс останется быстрым, но не потокобезопасным, а декоратор обеспечит безопасность с точки зрения многопоточности, если такая нужна. Просто дописать sinchronized - это самый простой вариант для такого декоратора.

- реактивность
Подобные декораторы основаны на паттерне observer и действуют следующим образом. Сначала они принимают в себя некоторый объект Observable, у которого есть метод notify (или подобный). Во время выполнения основного метода, может произойти ситуация (появление исключения, недопустимые параметры, недопустимое время для выполнения данного метода, все что угодно), о которой нужно узнать. Это похоже на логирование, но мы ищем конкретную ситуацию, только одну. Если она происходит - вызываем метод notify объекта Observable. Любой, кто подписан на этот источник событий (объкты типа Observer) узнать о наступления события и выполнят свою логику.
Например, такой сценарий. Декоратор, который проверяет входные параметры и если они равны запрещенному значению (каким конкретно зависит от системы, для примера это просто проверка на null), вызывает метод notify, который оповещает конкретный Observer, у которого есть сслыка на службу отправки сообщений, которая отправляет сообщение конкретному человеку, оповещая его о наступлении некоторого события.
Реактивная она именно поэтому - никто не знает, когда случится это событие (возможно никогда). Но мы можем указать, что должно произойти после.

Преобразования типа
Преобразовать тип выходного значения мы не можем из за строгой типизации (это относится к большинству языков, но не ко всем). Но всегда можем изменить строки. Например, в результате оригинального вызова execute нам пришли строка, в которую записаны проценты каких то рассчетов в виде "10%;55,5%;0,999%" и т.д. А вам нужно, чтобы эти значения были в диапазоне от 0(0%) до 1(100), разделенные пробелом и вместо запятых были точки. Но возможно формат еще поменяется, так что нужно быть к этому готовым.

фейковый декоратор
Такой декоратор будет крайне полезен для тестов - он просто возвращает заранее заданное значение. Например, есть класс который выполняет некоторые вычисления (наш компонент) и возвращает некоторый результат в виде числа int. Другой использует это значение. Если мы хотим протестировать этот (последний) класс, то должны изолировать его от компонента (т.е. сделать так, чтобы компонент никак не влиял на успешность прохождения тестов). Там, где в реальном коде компонент выполняет рассчеты, в тестах он сразу возвращает число.

Конечно это не все примеры использования декораторов.
Поддержка на уровне языка на примере python
В некоторых языках есть поддержка дкораторов на уровне языка. В таких языках обычно функция - это полноценный объект. Для примера посмотрим на язык python.
Напишем функцию damage, которая просто возвращает число 10. Мы можем вызвать эту функцию, и вывести на экран результат работы функции.

Т.к. функция - это объект, то мы можем создать переменную со значением функция, а после "вызвать" эту переменную, как функцию. Если функциональный подход в программировании вам не знаком, то это может показаться странным.

Т.к. как функция - это объект, то мы можем передать функцию как параметр в другую функцию, и вызвать функцию параметр в вызывающей функции, т.е. мы передаем в функции В функцию А как параметр и вызывам функцию А внутри функции В.

Но можем сделать хитрее. Определим функцию wrapper внутри функции В, и скажем что функция В возвращает функцию wrapper (если функция объект, а объект можно вернуть - то и функцию можно вернуть).

Может показаться странным, что мы не получили никакого вывода, но все верно. Мы вызвали функцию В и передали ей как параметр функцию А, внутри В мы создали еще одну функцию, которая вызывает А и возвращаем ее. Эту функцию (wrapper) никто не вызывал. Но можно присвоить ее переменной и вызвать (или просто вызвать дописав скобки справа от выражения т.е. function_B(function_A)())

Зачем все так усложнять? А вот зачем. Раз функция В возвращает функцию, а не значение, то мы можем присвоить эту функцию финкции А. Сначала просто вызовем функцию А, получим обычный вывод А, затем отделим вызовы с помощью 3-х тире, присвоим функцию В функции А и снова вызовем А.

Теперь при вызове функции А на самом деле вызывается В, которая вызывает А. Так мы получили декаратор функции А. Где поддержка языка? В возможности аннотировать функции, которые будут декорировать данную функцию. В данном случае В будет декоратором для А, поэтому старим аннотацию @function_B перед function_A.
Готово. Ставим аннотацию - функция А ведет себя как функция В, декорирующая А. Убираем - функция А снова сама по себе.

Заключение
В заключении перечислим основные особенности паттерна декоратор:
- Для внешнего мира декоратор выглядит также, как и объект, который он декорирует.
- Если декоратор не реализует один и тот же интерфейс вместе с компонентом, то это скорее просто композиция, т.к. у нас нет возможности заменить декоратором изначальный объект.
- При использовании нескольких декораторов важен порядок декорирования.
- использование дополнительных методов (не включенных в изначальный интерфейс) в декораторах уменьшает возможности их комбинирования, но если этот декоратор служит только для одной цели, такой подход уместен (например, декоратор для сбора статистики, который имеет методы, возвращающие собранные данные. Если мы обернем его в еще один декоратор, то вызывать эти методы не будет возможности).
- Декораторы предоставляют возможности создания более гибких систем, чем с помощью наследования.
- Декораторы позволяют следовать принципап открытости/закрытости (т.к. мы всегда можем добавить еще декораторов) и принципу единой ответственности (т.к. каждый декоратор добавляет конкретный функционал, и каждый декоратор имеет одну ответственность).
- Наличие объектов, которые нужно многократно декорировать, делают код создания таких объектов большим и трудно понимаемым.