Паттерн декоратора в C#

Первоначально было опубликовано здесь

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

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

Код примера из этой заметки вы можете найти на Github.

Концептуализация проблемы

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

Начальная версия библиотеки основана на классе Notifier, который имеет несколько полей, таких как список адресов электронной почты, конструктор и единственный метод Notify. Метод Notify принимает аргумент сообщения от клиента и отправляет его на список электронных адресов, предоставленный конструктором Notifier. Некоторое другое приложение выступает в роли клиента и настраивает Notifier один раз, а затем использует его каждый раз, когда происходит что-то важное во время рабочего процесса развертывания.

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

Теперь пользователям требуется гораздо больше, чем просто уведомления по электронной почте. Многие из них хотели бы получать SMS-уведомления о критических проблемах. Другие хотели бы получать уведомления Microsoft Teams, а команды DevOps по тем или иным причинам хотели бы получать уведомления Slack.

Здесь нет проблем! Мы можем расширить класс Notifier и добавить дополнительные методы уведомлений в новые подклассы. Теперь клиент может инстанцировать соответствующий класс уведомления и использовать его для дальнейших уведомлений.

Но затем кто-то задал вопрос на миллион долларов. «Почему вы не можете использовать несколько уведомлений одновременно? Если лодка тонет, вы, вероятно, захотите получить информацию по всем каналам».

Продолжая наше предыдущее решение, мы можем решить эту проблему, расширив класс Notifier специальными подклассами, которые объединяют несколько методов уведомления в одном классе. Теперь пришло время для простой математики. Каждый метод уведомления может либо существовать, либо не существовать в подклассе. Таким образом, существует два состояния для существования метода уведомления. В настоящее время у нас есть 3 различных метода уведомления. Таким образом, существует 2 в степени 3 различных комбинаций подклассов или 8 классов. Если мы добавим еще один метод уведомления, у нас будет 2 в степени 4 различных подкласса, или 16 различных классов. Становится очевидным, что такой подход приведет к экспоненциальному раздуванию кода.

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

  • Наследование статично. Мы не можем изменить поведение существующего объекта во время выполнения. Мы можем только заменить экземпляр на другой, созданный другим подклассом.
  • Подклассы могут иметь только один родительский класс. В C# наследование не позволяет классу наследовать поведение от нескольких классов одновременно.

Одним из способов преодоления этих недостатков является использование агрегации или композиции вместо наследования. Обе эти альтернативы практически идентичны. Экземпляр имеет ссылку на другой экземпляр и делегирует некоторую работу, в то время как при наследовании экземпляр сам выполняет эту работу, наследуя поведение от своего родителя.

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

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

Структурирование шаблона декоратора

На следующей схеме показано, как работает паттерн «Декоратор».

  1. Приложение делает запрос, а класс-декоратор перехватывает его.
  2. Класс-декоратор может предварительно обработать запрос, прежде чем передать его обернутому классу.
  3. Обернутый класс выполняет свои функции, как обычно, не зная о классе-декораторе.
  4. Класс-декоратор может обработать ответ перед тем, как передать его приложению.
  5. Декоратор возвращает результат исходному вызывающему классу.

В своей базовой реализации паттерн «Декоратор» имеет четырех участников:

  • Компонент: Компонент декларирует общие интерфейсы для обёрток и обёрнутых объектов.
  • Бетонные компоненты: Конкретный компонент — это класс объектов, которые будут обернуты. Он определяет исходное поведение, которое может быть изменено декораторами.
  • Базовый декоратор: Базовый декоратор ссылается на обернутый объект. Базовый декоратор делегирует все операции обернутому объекту.
  • Конкретные декораторы: Декоратор Concrete Decorator определяет дополнительное поведение, которое может быть добавлено к Concrete Components динамически.
  • Клиент: Клиент может обернуть компоненты в несколько слоев декораторов, если он работает с объектами через общий интерфейс.

Чтобы продемонстрировать, как работает паттерн «Декоратор», мы откроем шикарный ресторан «от фермы до вилки».

Идея ресторана заключается в том, чтобы готовить блюда из ингредиентов, приобретенных непосредственно у производителя. Это означает, что некоторые блюда могут быть помечены как распроданные, поскольку ферма может производить только такое количество ингредиентов.

Для начала мы реализуем нашего участника Component, который является нашим абстрактным классом Dish:

namespace Decorator.Components
{
    /// <summary>
    /// The abstract Component class
    /// </summary>
    public abstract class Dish
    {
        public abstract void Display();
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Нам также понадобится пара классов ConcreteComponent, представляющих отдельные блюда, которые может подавать наш ресторан. Эти классы заботятся только об ингредиентах блюда, а не о количестве доступных блюд. За это отвечает Декоратор.

namespace Decorator.Components
{
    /// <summary>
    /// A ConcreteComponent class
    /// </summary>
    public class Salad : Dish
    {
        private readonly string _veggies;
        private readonly string? _cheeses;
        private readonly string? _dressing;

        public Salad(string veggies, string? cheeses, string? dressing)
        {
            _veggies = veggies;
            _cheeses = cheeses;
            _dressing = dressing;
        }

        public override void Display()
        {
            Console.WriteLine("nSalad:");
            Console.WriteLine($" Veggies: {_veggies}");
            Console.WriteLine($" Cheeses: {_cheeses}");
            Console.WriteLine($" Dressing: {_dressing}");
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима
namespace Decorator.Components
{
    /// <summary>
    /// A ConcreteComponent class
    /// </summary>
    public class Pasta : Dish
    {
        private readonly string _pasta;
        private readonly string _sauce;

        public Pasta(string pasta, string sauce)
        {
            _pasta = pasta;
            _sauce = sauce;
        }

        public override void Display()
        {
            Console.WriteLine("nPasta: ");
            Console.WriteLine($" Pasta: {_pasta}");
            Console.WriteLine($" Sauce: {_sauce}");
        }
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

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

namespace Decorator.Decorators
{
    /// <summary>
    /// The Abstract Base Decorator
    /// </summary>
    public abstract class AbstractDecorator : Dish
    {
        protected Dish _dish;

        protected AbstractDecorator(Dish dish)
        {
            _dish = dish;
        }

        public override void Display()
        {
            _dish.Display();
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, нам нужен участник ConcreteDecorator, чтобы отслеживать, сколько блюд было заказано. Это роль AvailabilityDecorator.

namespace Decorator.Decorators
{
    public class AvailabilityDecorator : AbstractDecorator
    {
        public int AvailableItems { get; set; }
        protected List<string> customers = new();

        public AvailabilityDecorator(Dish dish, int available) : base(dish)
        {
            AvailableItems = available;
        }

        public void OrderItem(string name)
        {
            if (AvailableItems > 0)
            {
                customers.Add(name);
                AvailableItems--;
            }
            else
                Console.WriteLine($"nNot enough ingredients for {name}'s dish");
        }

        public override void Display()
        {
            base.Display();

            foreach(string customer in customers)
                Console.WriteLine($"Ordered by {customer}");
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Последний шаг — это настройка метода Main. Сначала мы определим набор блюд, а затем украсим их, чтобы они могли отслеживать свою доступность. Наконец, мы закажем блюда.

Salad caesarSalad = new("Crisp Romaine Lettuce", "Parmesan Cheese", "Homemade Caesar Dressing");
caesarSalad.Display();

Pasta fetuccine = new("Homemade Fetuccine", "Creamy Garlic Alfredo Sauce");
fetuccine.Display();

Console.WriteLine("nChanging availability of the dishes");

AvailabilityDecorator caesarAvailability = new(caesarSalad, 3);
AvailabilityDecorator pastaAvailability = new(fetuccine, 4);

caesarAvailability.OrderItem("Marion");
caesarAvailability.OrderItem("Thomas");
caesarAvailability.OrderItem("Imogen");
caesarAvailability.OrderItem("Jude");

pastaAvailability.OrderItem("Marion");
pastaAvailability.OrderItem("Thomas");
pastaAvailability.OrderItem("Imogen");
pastaAvailability.OrderItem("Jude");
pastaAvailability.OrderItem("Jacinth");

caesarAvailability.Display();
pastaAvailability.Display();

Console.ReadLine();
Вход в полноэкранный режим Выход из полноэкранного режима

Результат работы нашего приложения будет следующим:

Проблема толстых декораторов

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

Хотя существуют некоторые инструменты разработки, такие как ReSharper, которые могут помочь с обработкой всех этих методов, ответственность за поддержку все равно лежит на разработчике.

Плюсы и минусы паттерна «Декоратор

✔ Мы можем расширить поведение объекта, не изменяя его исходный код ❌ Трудно удалить обертку из стека оберток
✔ Мы можем расширить поведение объекта без создания нового подкласса. ❌ Трудно реализовать декораторы таким образом, чтобы не зависеть от порядка декораторов в стеке оберток.
✔ Мы можем комбинировать несколько декораторов, чтобы добавить несколько моделей поведения к объекту. ❌ Толстые декораторы могут реализовать несколько методов, которые не будут использоваться
✔ Мы можем разделить монолитный класс на несколько классов, каждый со своим поведением, удовлетворяя тем самым принципу единой ответственности ❌ Первоначальная конфигурация слоев может выглядеть довольно уродливо и может быть сложной для поддержки

Отношения с другими паттернами

  • Адаптер и Декоратор работают схожим образом. Однако адаптер изменяет интерфейс существующего объекта, а декоратор улучшает объект без изменения его интерфейса. Кроме того, Декоратор поддерживает рекурсивную композицию, что невозможно при использовании Адаптера. Наконец, Адаптер предоставляет другой интерфейс для обернутого объекта, в то время как Декоратор предоставляет ему улучшенный интерфейс.
  • Chain of Responsibility и Decorator имеют схожую структуру классов. Оба интерфейса полагаются на рекурсивную композицию. Однако есть и некоторые различия. Обработчик CoR может выполнять произвольные операции без зависимости от других обработчиков в цепочке. С другой стороны, декоратор может расширять поведение объекта, сохраняя его в соответствии с базовым интерфейсом. Более того, декораторам не разрешается нарушать поток запроса.
  • Паттерны Composite и Decorator имеют схожую структуру, поскольку оба опираются на рекурсивную композицию. Декоратор похож на композит, но имеет только один дочерний компонент. Кроме того, Decorator добавляет дополнительные обязанности к обернутому объекту, в то время как Composite просто суммирует результаты своих дочерних компонентов.
  • Декоратор и прокси имеют схожую структуру, но совершенно разные цели. Оба паттерна построены на принципе композиции. Однако Proxy обычно самостоятельно управляет жизненным циклом своего сервисного объекта, в то время как композиция Decorator всегда контролируется клиентом.

Заключительные размышления

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

Стоит отметить, что паттерн «Декоратор», как и остальные паттерны проектирования, представленные «бандой четырех», не является панацеей или универсальным решением при разработке приложения. И снова инженеры должны сами решать, когда использовать тот или иной паттерн. В конце концов, эти шаблоны полезны, когда используются как точный инструмент, а не как кувалда.

Оцените статью
devanswers.ru
Добавить комментарий