Шаблон «Декоратор»: суперсила классов-обёрток
Создаём вселенную супергероев с помощью Java и паттерна «Декоратор».
Dana Moskvina / Skillbox Media
Эта статья продолжает цикл рассказов о шаблонах проектирования. В первой части мы устроились в NASA и работали с данными марсохода, а сегодня сами создадим вселенную и населим её существами со сверхспособностями.
Содержание
Постановка задачи
Написать программу, которая умеет сравнивать двух супергероев и определять, у кого из них больше шансов выжить в битве.
В нашем маленьком мирке будут два типа героев: зелёные и красные. Каждый из них может обладать одной или несколькими сверхспособностями: суперсила, суперловкость и суперинтеллект.
Судьба героя зависит от его шанса на выживание. Он изначально чуть выше у красных героев, но каждая суперспособность увеличивает шанс:
- суперсила — на 4;
- суперловкость — на 3;
- суперинтеллект — на 7.
В битве двух супергероев выживает тот, у кого итоговое значение шанса больше.
В рамках задачи нас не интересует ничего, кроме описания героя и его способности выжить. Поэтому базовый интерфейс супергероя будет выглядеть так:
Класс для зелёного супергероя:
И для красного, более живучего:
Решение 1
Создадим много классов-индивидуальностей
Можем поступить как в Marvel и каждому возможному варианту героя дать своё имя — создать отдельный класс. Например, зелёный супергерой с суперсилой и суперинтеллектом будет Халком, а красного — с суперинтеллектом и суперловкостью — назовём Человеком-пауком.
Всего получится 16 классов: восемь комбинаций из трёх суперспособностей для красных и восемь — для зелёных:
Суперсила | Суперловкость | Суперинтеллект | |
---|---|---|---|
1 | - | - | - |
2 | + | - | - |
3 | - | + | - |
4 | - | - | + |
5 | + | + | - |
6 | - | + | + |
7 | + | - | + |
8 | + | + | + |
Например, так будет выглядеть класс Халка:
Что здесь плохо
Классов и так слишком много, а при добавлении каждой новой суперспособности их число будет увеличиваться вдвое для каждого типа супергероя.
Если же мы захотим добавить не суперспособность, а тип — например, жёлтых супергероев, — то классов станет больше на 2n, где n — варианты суперспособностей.
Как это посчитать
Для каждого типа героя (красного или зелёного) суперсила либо есть, либо нет — ровно два варианта.
Независимо от этого, у каждого такого варианта суперловкость либо есть, либо нет. Итого: для каждого из 2 вариантов ещё по 2 варианта = 4 варианта.
У каждого из этих 4 вариантов суперинтеллект либо есть, либо нет:
4 ∗ 2 = 8 вариантов.
Таким образом, каждая суперспособность увеличивает число вариантов супергероев каждого цвета вдвое, то есть при n суперспособностей их число равно 2n.
Для каждого нового класса придётся вручную задавать описание и реализовывать метод для вычисления шансов на выживание. В этом зоопарке легко запутаться, а любые изменения станут адом.
Решение 2
Перенесём всю логику в базовый класс
Пожертвуем индивидуальностью в пользу уменьшения числа классов: оставим только классы для красного и зелёного супергероя, но добавим базовый, в котором реализуем методы для описания героя и вычисления его шансов на выживание.
Базовый класс получится таким:
Осталось немного изменить RedSuperHero и GreenSuperHero, чтобы они наследовали этот базовый класс. Например, для красного героя будет так:
Теперь, чтобы получить, например, красного супергероя, который наделён суперинтеллектом и суперловкостью, нужно создать экземпляр RedSuperHero и установить в true соответствующие свойства:
Что здесь плохо
Классов теперь немного, а вся логика в одном месте, но:
- при добавлении новой способности придётся добавлять свойства в базовый класс и менять логику реализации методов — теперь уже методы будут становиться всё длиннее и запутаннее;
- если мы захотим чуть усложнить наш мирок и решим, что не все типы героев могут обладать всеми суперспособностями, в классах-наследниках всё равно останутся все методы базового — с наследованием иначе никак;
- удвоить или утроить любую суперспособность — например, наделить героя двойной суперсилой — в такой структуре классов тоже не выйдет: понадобятся ещё классы или новые свойства.
Чтобы обеспечить разнообразие и избавиться от недостатков наследования, можно использовать паттерн проектирования «Декоратор».
Решение 3
Применим паттерн «Декоратор»
Согласно классическому определению «Банды четырёх», этот шаблон проектирования предназначен для динамического подключения дополнительного поведения к объекту. «Декоратор» обычно выступает как альтернатива набору классов-наследников с этим дополнительным поведением.
У шаблона говорящее название: он берёт какой-то объект и добавляет к нему декорации-рюшечки. Но какие бы украшения мы ни применили, в итоге должен получиться объект того же типа — просто с дополнительными возможностями. После декорирования супергероя суперспособностями должен получиться супергерой с суперспособностями, а не трансформер или динозавр, например.
Поэтому:
- Класс-декоратор должен быть того же типа, что и декорируемый класс, — реализовывать тот же интерфейс или наследовать тот же базовый класс.
- Декоратор реализует поведение исходного класса; при этом не изменяет его, а добавляет своё до или после вызова базового поведения.
- Это достигается за счёт того, что декоратор содержит в себе объект базового класса и вызывает его методы там, где требуется дополнить поведение.
Вот как это выглядит на диаграмме классов:
Здесь всего два типа классов: «обычные» — ConcreteComponent — и классы-декораторы, которые наделяют их новыми возможностями. Такие классы ещё называют классами-обёртками, потому что внутри каждой обёртки лежит объект.
Представляйте это как многослойную одежду: первый слой отводит влагу, второй — сохраняет тепло, третий — защищает от ветра и дождя. Всё вместе «оборачивает» человека и наделяет суперспособностью — выдерживать капризы погоды.
Вернёмся к нашим супергероям и применим к ним паттерн «Декоратор». Сначала создадим класс базового декоратора:
Он абстрактный — экземпляр этого класса создать невозможно. Да и не нужно, потому что в этом нет смысла — мы пока ничего не добавляем в него, а просто сохраняем объект супергероя и пользуемся его методами для получения описания и шансов на выживание.
Если раньше вы не сталкивались с абстрактными классами, почитайте о них в этой статье.
А теперь напишем настоящий декоратор для суперсилы:
Классы-обёртки для суперловкости и суперинтеллекта пишутся по аналогии.
Теперь продемонстрируем возможности декораторов и устроим битву между супергероями:
Шансы почти равны, но победит всё же зелёный супергерой с суперинтеллектом и суперловкостью:
Как вызываются методы декорированных объектов
Каждый декоратор при вызове метода (например, вычисления шанса на выживание) обращается к одноимённому методу объекта, который он сохранил для себя в конструкторе. Только после того, как этот метод вернёт значение, метод декоратора прибавит к этому результату своё число в зависимости от суперспособности.
Нюанс в том, что сохранённый в конструкторе объект и сам может оказаться задекорированным. Тогда наш метод не сможет сразу вернуть число, а должен будет спуститься ещё глубже — обратиться к сохранённому уже внутри этого второго декоратора объекту.
Это будет продолжаться до тех пор, пока мы не получим «чистый», ничем не обёрнутый объект, который точно знает свои шансы на выживание. Он отдаст это число первому декоратору, который его вызывал. Тот декоратор увеличит его в соответствии со своей реализацией и передаст результат тому, что его вызвал. И так далее, пока не вернёмся к началу.
Если вы знакомы с рекурсией, то наверняка узнали её в этом алгоритме. А если пока не знакомы, прочитайте о ней здесь.
На схеме — пример расчёта шанса на выживание для первого супергероя из листинга выше.
С шаблоном «Декоратор» мы можем создавать новые суперспособности, не меняя существующие классы, комбинировать их друг с другом и даже генерировать множественные суперспособности — например, создать супергероя с двойной суперловкостью:
Когда не стоит применять «Декоратор»
Если в программе какая-то логика завязана на конкретные классы, то «Декоратор» — не лучшее решение. Например, если зелёные супергерои в результате применения суперловкости должны становиться более ловкими, чем красные, — лучше обойтись обычным наследованием.
Дело в том, что декораторы принимают объекты типа SuperHero и их же возвращают, не разбираясь, какие это конкретно супергерои, так что после декорирования информация об их типе (в нашем случае цвете) теряется.
Подытожим
Шаблон «Декоратор» полезен, когда нужно динамически предоставлять объектам дополнительные возможности. Это хорошая альтернатива наследованию в том случае, когда в логике программы не нужно учитывать конкретные типы декорируемых объектов.
О других шаблонах проектирования, а также алгоритмах, структурах данных, концепциях объектно-ориентированного программирования с примерами на языке Java — на курсе «Профессия Java-разработчик PRO». Освойте востребованный язык, научитесь создавать качественные приложения под разные платформы, а Skillbox поможет с трудоустройством.