Код
Java
#База знаний

Шаблон «Декоратор»: суперсила классов-обёрток

Создаём вселенную супергероев с помощью Java и паттерна «Декоратор».

Dana Moskvina / Skillbox Media

Эта статья продолжает цикл рассказов о шаблонах проектирования. В первой части мы устроились в NASA и работали с данными марсохода, а сегодня сами создадим вселенную и населим её существами со сверхспособностями.

Содержание

Постановка задачи

Написать программу, которая умеет сравнивать двух супергероев и определять, у кого из них больше шансов выжить в битве.

В нашем маленьком мирке будут два типа героев: зелёные и красные. Каждый из них может обладать одной или несколькими сверхспособностями: суперсила, суперловкость и суперинтеллект.

Судьба героя зависит от его шанса на выживание. Он изначально чуть выше у красных героев, но каждая суперспособность увеличивает шанс:

  • суперсила — на 4;
  • суперловкость — на 3;
  • суперинтеллект — на 7.

В битве двух супергероев выживает тот, у кого итоговое значение шанса больше.

В рамках задачи нас не интересует ничего, кроме описания героя и его способности выжить. Поэтому базовый интерфейс супергероя будет выглядеть так:

public interface SuperHero {
  String getDescription(); // описание
  int getChanceOfSurvival(); // шанс на выживание
}

Класс для зелёного супергероя:

public class GreenSuperHero implements SuperHero {
  @Override
  public String getDescription() {
    return "Зелёный супергерой";
  }

  @Override
  public int getChanceOfSurvival() {
    return 50;
  }
}

И для красного, более живучего:

public class RedSuperHero implements SuperHero {
  @Override
  public String getDescription() {
    return "Красный супергерой";
  }

  @Override
  public int getChanceOfSurvival() {
    return 52;
  }
}

Решение 1


Создадим много классов-индивидуальностей

Можем поступить как в Marvel и каждому возможному варианту героя дать своё имя — создать отдельный класс. Например, зелёный супергерой с суперсилой и суперинтеллектом будет Халком, а красного — с суперинтеллектом и суперловкостью — назовём Человеком-пауком.

Всего получится 16 классов: восемь комбинаций из трёх суперспособностей для красных и восемь — для зелёных:

СуперсилаСуперловкостьСуперинтеллект
1---
2+--
3-+-
4--+
5++-
6-++
7+-+
8+++

Например, так будет выглядеть класс Халка:

public class Hulk extends RedSuperHero {
   @Override
   public String getDescription() {
    return "Халк";
  }

  @Override
  public int getChanceOfSurvival() {
    return super.getChanceOfSurvival() + 4 + 7; 
  }
}

Что здесь плохо

Классов и так слишком много, а при добавлении каждой новой суперспособности их число будет увеличиваться вдвое для каждого типа супергероя.

Если же мы захотим добавить не суперспособность, а тип — например, жёлтых супергероев, — то классов станет больше на 2n, где n — варианты суперспособностей.

Как это посчитать

Для каждого типа героя (красного или зелёного) суперсила либо есть, либо нет — ровно два варианта.

Независимо от этого, у каждого такого варианта суперловкость либо есть, либо нет. Итого: для каждого из 2 вариантов ещё по 2 варианта = 4 варианта.

У каждого из этих 4 вариантов суперинтеллект либо есть, либо нет:

4 ∗ 2 = 8 вариантов.

Таким образом, каждая суперспособность увеличивает число вариантов супергероев каждого цвета вдвое, то есть при n суперспособностей их число равно 2n.

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

Решение 2


Перенесём всю логику в базовый класс

Пожертвуем индивидуальностью в пользу уменьшения числа классов: оставим только классы для красного и зелёного супергероя, но добавим базовый, в котором реализуем методы для описания героя и вычисления его шансов на выживание.

Базовый класс получится таким:

public class BaseSuperHero implements SuperHero {

  private boolean superPower; // есть суперсила
  private boolean superAgility; // есть суперловкость
  private boolean superIntelligence; // есть суперинтеллект

  public boolean hasSuperPower() {
    return superPower;
  }

  public void setSuperPower(boolean superPower) {
    this.superPower = superPower;
  }

  public boolean hasSuperAgility() {
    return superAgility;
  }

  public void setSuperAgility(boolean superAgility) {
    this.superAgility = superAgility;
  }

  public boolean hasSuperIntelligence() {
    return superIntelligence;
  }

  public void setSuperIntelligence(boolean superIntelligence) {
    this.superIntelligence = superIntelligence;
  }

  @Override
  public String getDescription() {
    StringBuilder description = new StringBuilder("");
    if (hasSuperPower())
      description.append(" с суперсилой");
    if (hasSuperAgility())
      description.append(" с суперловкостью");
    if (hasSuperIntelligence())
      description.append(" с суперинтеллектом");
    return description.toString();
  }

  @Override
  public int getChanceOfSurvival() {
    int chanceOfSurvival = 0;
    if (hasSuperPower())
      chanceOfSurvival += 4;
    if (hasSuperAgility())
      chanceOfSurvival += 3;
    if (hasSuperIntelligence())
      chanceOfSurvival += 7;
    return chanceOfSurvival;
  }
}

Осталось немного изменить RedSuperHero и GreenSuperHero, чтобы они наследовали этот базовый класс. Например, для красного героя будет так:

public class RedSuperHero extends BaseSuperHero {
  @Override
  public String getDescription() {
    return "Красный супергерой" + super.getDescription();
  }
}

Теперь, чтобы получить, например, красного супергероя, который наделён суперинтеллектом и суперловкостью, нужно создать экземпляр RedSuperHero и установить в true соответствующие свойства:

BaseSuperHero superHero = new RedSuperHero();
superHero.setSuperIntelligence(true);
superHero.setSuperAgility(true);

Что здесь плохо

Классов теперь немного, а вся логика в одном месте, но:

  • при добавлении новой способности придётся добавлять свойства в базовый класс и менять логику реализации методов — теперь уже методы будут становиться всё длиннее и запутаннее;
  • если мы захотим чуть усложнить наш мирок и решим, что не все типы героев могут обладать всеми суперспособностями, в классах-наследниках всё равно останутся все методы базового — с наследованием иначе никак;
  • удвоить или утроить любую суперспособность — например, наделить героя двойной суперсилой — в такой структуре классов тоже не выйдет: понадобятся ещё классы или новые свойства.

Чтобы обеспечить разнообразие и избавиться от недостатков наследования, можно использовать паттерн проектирования «Декоратор».

Решение 3


Применим паттерн «Декоратор»

Согласно классическому определению «Банды четырёх», этот шаблон проектирования предназначен для динамического подключения дополнительного поведения к объекту. «Декоратор» обычно выступает как альтернатива набору классов-наследников с этим дополнительным поведением.

У шаблона говорящее название: он берёт какой-то объект и добавляет к нему декорации-рюшечки. Но какие бы украшения мы ни применили, в итоге должен получиться объект того же типа — просто с дополнительными возможностями. После декорирования супергероя суперспособностями должен получиться супергерой с суперспособностями, а не трансформер или динозавр, например.

Поэтому:

  • Класс-декоратор должен быть того же типа, что и декорируемый класс, — реализовывать тот же интерфейс или наследовать тот же базовый класс.
  • Декоратор реализует поведение исходного класса; при этом не изменяет его, а добавляет своё до или после вызова базового поведения.
  • Это достигается за счёт того, что декоратор содержит в себе объект базового класса и вызывает его методы там, где требуется дополнить поведение.

Вот как это выглядит на диаграмме классов:

Диаграмма классов. Инфографика: Майя Мальгина для Skillbox Media

Здесь всего два типа классов: «обычные» — ConcreteComponent — и классы-декораторы, которые наделяют их новыми возможностями. Такие классы ещё называют классами-обёртками, потому что внутри каждой обёртки лежит объект.

Представляйте это как многослойную одежду: первый слой отводит влагу, второй — сохраняет тепло, третий — защищает от ветра и дождя. Всё вместе «оборачивает» человека и наделяет суперспособностью — выдерживать капризы погоды.

Вернёмся к нашим супергероям и применим к ним паттерн «Декоратор». Сначала создадим класс базового декоратора:

public abstract class SuperHeroDecorator implements SuperHero {
  protected final SuperHero superHero;

  public SuperHeroDecorator(SuperHero superHero) {
    this.superHero = superHero;
  }

  @Override
  public String getDescription() {
    return superHero.getDescription();
  }

  @Override
  public int getChanceOfSurvival() {
    return superHero.getChanceOfSurvival();
  }
}

Он абстрактный — экземпляр этого класса создать невозможно. Да и не нужно, потому что в этом нет смысла — мы пока ничего не добавляем в него, а просто сохраняем объект супергероя и пользуемся его методами для получения описания и шансов на выживание.

Если раньше вы не сталкивались с абстрактными классами, почитайте о них в этой статье.

А теперь напишем настоящий декоратор для суперсилы:

public class SuperPower extends SuperHeroDecorator {

  public SuperPower(SuperHero superHero) {
    super(superHero);
  }

  @Override
  public String getDescription() {
    return super.getDescription() + " с суперсилой";
  }

  @Override
  public int getChanceOfSurvival() {
    return super.getChanceOfSurvival() + 4;
  }
}

Классы-обёртки для суперловкости и суперинтеллекта пишутся по аналогии.

Теперь продемонстрируем возможности декораторов и устроим битву между супергероями:

public class SuperHeroes {

  public static void main(String[] params) {
    // создадим первого героя
    SuperHero firstHero = new SuperAgility(new SuperIntelligence(new GreenSuperHero()));
    // и второго
    SuperHero secondHero = new SuperPower(new SuperAgility(new RedSuperHero()));
    // представим их публике
    printInfo(firstHero);
    printInfo(secondHero);
    // и устроим битву
    fight(firstHero, secondHero);
  }

  private static void fight(SuperHero first, SuperHero second) {
    if (first.getChanceOfSurvival() > second.getChanceOfSurvival()) {
      printAlive(first);
    } else if (second.getChanceOfSurvival() > first.getChanceOfSurvival()) {
      printAlive(second);
    } else {
      System.out.println("Шансы на выживание равны");
    }
  }

  private static void printInfo(SuperHero superHero) {
    System.out.printf("У супергероя `%1$s` шанс на выживание равен %2$d", superHero.getDescription(), superHero.getChanceOfSurvival());
  }

  private static void printAlive(SuperHero superHero) {
    System.out.printf("Выживет супергерой `%1$s`", superHero.getDescription());
  }
}

Шансы почти равны, но победит всё же зелёный супергерой с суперинтеллектом и суперловкостью:

Вывод в консоли после запуска программы. Скриншот: Екатерина Степанова для Skillbox Media

Как вызываются методы декорированных объектов

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

Нюанс в том, что сохранённый в конструкторе объект и сам может оказаться задекорированным. Тогда наш метод не сможет сразу вернуть число, а должен будет спуститься ещё глубже — обратиться к сохранённому уже внутри этого второго декоратора объекту.

Это будет продолжаться до тех пор, пока мы не получим «чистый», ничем не обёрнутый объект, который точно знает свои шансы на выживание. Он отдаст это число первому декоратору, который его вызывал. Тот декоратор увеличит его в соответствии со своей реализацией и передаст результат тому, что его вызвал. И так далее, пока не вернёмся к началу.

Если вы знакомы с рекурсией, то наверняка узнали её в этом алгоритме. А если пока не знакомы, прочитайте о ней здесь.

На схеме — пример расчёта шанса на выживание для первого супергероя из листинга выше.

Инфографика: Майя Мальгина для Skillbox Media

С шаблоном «Декоратор» мы можем создавать новые суперспособности, не меняя существующие классы, комбинировать их друг с другом и даже генерировать множественные суперспособности — например, создать супергероя с двойной суперловкостью:

SuperHero doubleAgilityHero = new SuperAgility(new SuperAgility(new GreenSuperHero()));

Когда не стоит применять «Декоратор»

Если в программе какая-то логика завязана на конкретные классы, то «Декоратор» — не лучшее решение. Например, если зелёные супергерои в результате применения суперловкости должны становиться более ловкими, чем красные, — лучше обойтись обычным наследованием.

Дело в том, что декораторы принимают объекты типа SuperHero и их же возвращают, не разбираясь, какие это конкретно супергерои, так что после декорирования информация об их типе (в нашем случае цвете) теряется.

Подытожим

Шаблон «Декоратор» полезен, когда нужно динамически предоставлять объектам дополнительные возможности. Это хорошая альтернатива наследованию в том случае, когда в логике программы не нужно учитывать конкретные типы декорируемых объектов.

О других шаблонах проектирования, а также алгоритмах, структурах данных, концепциях объектно-ориентированного программирования с примерами на языке Java — на курсе «Профессия Java-разработчик PRO». Освойте востребованный язык, научитесь создавать качественные приложения под разные платформы, а Skillbox поможет с трудоустройством.

Онлайн-школа для детей Skillbox Kids
Учим детей программированию, созданию игр, сайтов и дизайну. Первое занятие бесплатно! Подробности — по клику.
Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована