Decorator C# | Паттерн Декоратор C#

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

Идея паттерна Декоратор

Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых структурных паттернов — Декоратор (Decorator).

Как я уже писал ранее, существует три вида паттернов проектирования:

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

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

Архитектура паттерна Decorator

Давайте рассмотрим диаграмму паттерна.

Декоратор (Decorator)

Component — базовый класс компонента, чье поведение будет расширяться декораторами

Client — работает с компонентом, не зная о существовании декораторов

ConcreteComponent — конкретная реализация компонента

Decorator — базовый класс декоратора, предназначенный для расширения поведения компонента

ConcreteDecoratorA, ConcreteDecoratorB — конкретный декоратор, который добавляет декорируемому объекту специфическое поведение.

Логика работы паттерна Декоратор

Рассмотрим основную логику работы паттерна Декоратор. Рассмотрим совершенно обычную и повседневную ситуацию, с которой сталкивался практически любой программист – он (ну или она) проголодался и пошел в ближайший ларек за шаурмой (ну или шавермой). В продаже имеется совершено обычная шаурма, но кроме того существуют дополнительные опции, сырный или арабский лаваш, добавки в виде сыра, грибов, имбиря и аджики. И вот представьте себе, как выглядело бы меню в этом ларьке, если бы были перечислены все виды шаурмы:

  • Обычная шаурма – 100 руб.
  • Шаурма с грибами – 110 руб.
  • Шаурма с имбирем – 110 руб.
  • Шаурма с сыром – 110 руб.
  • Шаурма с грибами и сыром – 120 руб.
  • Шаурма с грибами и имбирем – 120 руб.
  • Шаурма с грибами, имбирем и сыром – 130 руб.
  • Обычная шаурма в сырном лаваше – 110 руб.
  • и так далее…

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

Реализация паттерна проектирования Декоратор на языке C#

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

using System;

namespace Patterns.Decorator
{
    /// <summary>
    /// Базовый класс шаурмы.
    /// </summary>
    public abstract class ShaurmaBase
    {
        /// <summary>
        /// Название шаурмы.
        /// </summary>
        public string Name { get; protected set; }

        /// <summary>
        /// Стоимость.
        /// </summary>
        public int Price { get; protected set; }

        /// <summary>
        /// Создание нового экземпляра шаурмы.
        /// </summary>
        /// <param name="name">Имя клиента.</param>
        public ShaurmaBase(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException(nameof(name));
            }

            Name = name;
            Price = 100;
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns>Полное название шаурмы.</returns>
        public override string ToString()
        {
            return $"Шаурма для клиента по имени {Name}";
        }
    }
}

У базового базового класса шаурмы есть три конкретных реализации в зависимости от типа используемого лаваша. Это влияет на название и стоимость шаурмы.

namespace Patterns.Decorator
{
    /// <summary>
    /// Шаурма в обычном лаваше.
    /// </summary>
    public class StandardShaurma : ShaurmaBase
    {
        /// <summary>
        /// Создание нового экземпляра шаурмы в обычном лаваше.
        /// </summary>
        /// <param name="name">Имя клиента.</param>
        public StandardShaurma(string name) : base(name + " в обычном лаваше")
        {
        }
    }
}
namespace Patterns.Decorator
{
    /// <summary>
    /// Шаурма в сырном лаваше.
    /// </summary>
    public class CheeseShaurma : ShaurmaBase
    {
        /// <summary>
        /// Создание экземпляра шаурмы в сырном лаваше.
        /// </summary>
        /// <param name="name">Имя клиента.</param>
        public CheeseShaurma(string name) : base(name + " в сырном лаваше")
        {
            Price += 10;
        }
    }
}
namespace Patterns.Decorator
{
    /// <summary>
    /// Шаурма в арабском лаваше.
    /// </summary>
    public class ArabicShaurma : ShaurmaBase
    {
        /// <summary>
        /// Создание экземпляра шаурмы в арабском лаваше.
        /// </summary>
        /// <param name="name">Имя клиента.</param>
        public ArabicShaurma(string name) : base(name + " в арабском лаваше")
        {
            Price += 15;
        }
    }
}

Теперь рассмотрим базовый класс добавки шаурмы. Это и будет базовый класс декоратора.

using System;

namespace Patterns.Decorator
{
    /// <summary>
    /// Базовый класс добавки к шаурме.
    /// </summary>
    public abstract class ShaurmaAdditiveBase : ShaurmaBase
    {
        /// <summary>
        /// Шаурма в которую добавляются добавки.
        /// </summary>
        protected ShaurmaBase Shaurma { get; set; }

        /// <summary>
        /// Создание нового экземпляра шаурмы с добавкой.
        /// </summary>
        /// <param name="shaurma">Шаурма, в которую добавляется добавка.</param>
        public ShaurmaAdditiveBase(ShaurmaBase shaurma) : base(shaurma.Name)
        {
            Shaurma = shaurma ?? throw new ArgumentNullException(nameof(shaurma));
            Price = shaurma.Price;
        }
    }
}

И соответственно созданы необходимые конкретные реализации декораторов, которые вносят изменения в функционал основного класса шаурмы.

namespace Patterns.Decorator
{
    /// <summary>
    /// Сырная добавка к шаурме.
    /// </summary>
    public class CheeseAdditive : ShaurmaAdditiveBase
    {
        /// <summary>
        /// Создание экземпляра шаурмы с сырной добавкой.
        /// </summary>
        /// <param name="shaurma">Шаурма, в которую будет добавлен сыр.</param>
        public CheeseAdditive(ShaurmaBase shaurma) : base(shaurma)
        {
            Price += 10;
            Name += " с сыром";
        }
    }
}
namespace Patterns.Decorator
{
    /// <summary>
    /// Добавка имбиря в шаурму.
    /// </summary>
    public class GingerAdditive : ShaurmaAdditiveBase
    {
        /// <summary>
        /// Создать экземпляр шаурмы с добавкой имбиря.
        /// </summary>
        /// <param name="shaurma">Шаурма, в которую будет добавлен имбирь.</param>
        public GingerAdditive(ShaurmaBase shaurma) : base(shaurma)
        {
            Price += 15;
            Name += " с имбирем";
        }
    }
}
namespace Patterns.Decorator
{
    /// <summary>
    /// Грибная добавка в шаурму.
    /// </summary>
    public class MushroomAdditive : ShaurmaAdditiveBase
    {
        /// <summary>
        /// Создать новый экземпляр шаурмы с добавлением грибов.
        /// </summary>
        /// <param name="shaurma">Шаурма, в которую будут добавлены грибы.</param>
        public MushroomAdditive(ShaurmaBase shaurma) : base(shaurma)
        {
            Price += 10;
            Name += " с грибами";
        }
    }
}
namespace Patterns.Decorator
{
    /// <summary>
    /// Острая добавка.
    /// </summary>
    public class ChilliAdditive : ShaurmaAdditiveBase
    {
        /// <summary>
        /// Создать новый экземпляр шаурмы с острой добавкой.
        /// </summary>
        /// <param name="shaurma">Шаурма, в которую добавляется добавка.</param>
        public ChilliAdditive(ShaurmaBase shaurma) : base(shaurma)
        {
            Price += 5;
            Name += " острая";
        }
    }
}

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

Далее для удобства и удобочитаемости кода были добавлены несколько перечислений и метод расширения, позволяющий легко выводить названия элементов перечисления на русском языке. Подробнее об этом методе можно прочитать в статье Отображение значения Enum в C# на русском.

using System.ComponentModel;

namespace Patterns.Decorator
{
    /// <summary>
    /// Тип шаурмы.
    /// </summary>
    public enum ShaurmaType : int
    {
        /// <summary>
        /// В обычном лаваше.
        /// </summary>
        [Description("Шаурма в обычном лаваше")]
        Standard = 1,

        /// <summary>
        /// В сырном лаваше
        /// </summary>
        [Description("Шаурма в сырном лаваше")]
        Cheese = 2,

        /// <summary>
        /// В арабском лаваше.
        /// </summary>
        [Description("Шаурма в арабском лаваше")]
        Arabic = 3
    }
}
using System.ComponentModel;

namespace Patterns.Decorator
{
    /// <summary>
    /// Добавка к шаурме.
    /// </summary>
    public enum AdditiveType : int
    {
        /// <summary>
        /// Без добавок.
        /// </summary>
        [Description("Без добавок")]
        None = 0,

        /// <summary>
        /// Сыр.
        /// </summary>
        [Description("Сыр")]
        Cheese = 1,

        /// <summary>
        /// Имбирь.
        /// </summary>
        [Description("Имбирь")]
        Ginger = 2,

        /// <summary>
        /// Грибы.
        /// </summary>
        [Description("Грибы")]
        Mushroom = 3,

        /// <summary>
        /// Острая.
        /// </summary>
        [Description("Острая")]
        Spisy = 4
    }
}
/// <summary>
/// Приведение значения перечисления в удобочитаемый формат.
/// </summary>
/// <remarks>
/// Для корректной работы необходимо использовать атрибут [Description("Name")] для каждого элемента перечисления.
/// </remarks>
/// <param name="enumElement">Элемент перечисления</param>
/// <returns>Название элемента</returns>
static string GetDescription(Enum enumElement)
{
	Type type = enumElement.GetType();

	MemberInfo[] memInfo = type.GetMember(enumElement.ToString());
	if (memInfo != null && memInfo.Length > 0)
	{
		object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
		if (attrs != null && attrs.Length > 0)
			return ((DescriptionAttribute)attrs[0]).Description;
	}

	return enumElement.ToString();
}

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

using System;
 
namespace Decorator
{
    class Program
    {
        static void Main(string[] args)
        {
            // Начало работы программы. Просим представиться клиента.
            Console.WriteLine("Здравствуйте. Вы обратились в службу заказа шаурты. Представьтесь, пожалуйста.");
            Console.Write("Ваше имя: ");
            var clientName = Console.ReadLine();
            Console.WriteLine("Ваш заказ уже начал готовиться.");
            Shaurma shaurma = null;
 
            // Просим клиента выбрать тип шаурмы. 
            // Повторяем до тех пор, пока пользователь не введет корректное значение.
            do
            {
                shaurma = GetShaurmaType(clientName);
            }
            while (shaurma == null);
 
            // Просим клиента выбрать добавки к шаурме.
            // Повторяем вопрос до тех пор, пока пользователь не откажется от добавки.
            while(true)
            {
                shaurma = GetShaurmaAdding(shaurma, out bool finish);
                if(finish)
                {
                    break;
                }
            }
 
            // Выводим сообщение о готовности шаурмы.
            Console.WriteLine($"{shaurma} готова. Стоимость {shaurma.Price} рублей.");
            Console.ReadLine();
        }
 
        /// <summary>
        /// Запросить о необходимости добавки в шаурму.
        /// </summary>
        /// <param name="shaurma">Шаурма, в которую может быть добавлена добавка.</param>
        /// <param name="finish">Добавление не нужно.</param>
        /// <returns>Шаурма после добавления добавки.</returns>
        private static Shaurma GetShaurmaAdding(Shaurma shaurma, out bool finish)
        {
            // По умолчанию добавки еще нужны.
            finish = false;
 
            // Выводим пользователю все возможные виды добавок
            // и сохраняем его ответ.
            Console.WriteLine("Хотите добавить особую добавку в шаурму?");
            foreach (AddingType t in Enum.GetValues(typeof(AddingType)))
            {
                Console.WriteLine($"{(int)t} - {t.GetDescription()}");
            }
            var adding = Console.ReadLine();
 
            // Пытаемя привести ответ пользователя к требуемому типу.
            if (int.TryParse(adding, out int addingType))
            {
                switch ((AddingType)addingType)
                {
                    case AddingType.None:
                        // Добавки больше не нужны.
                        // Возвращаем шаурму без изменений.
                        finish = true;
                        return shaurma;
                    case AddingType.Cheese:
                        // Добавляем сыр.
                        return new CheeseAdding(shaurma);
                    case AddingType.Ginger:
                        // Добавляем имбирь
                        return new GingerAdding(shaurma);
                    case AddingType.Mushroom:
                        // Добавляем грибы.
                        return new MushroomAdding(shaurma);
                    case AddingType.Spisy:
                        // Добавляем перца.
                        return new SpicyAdding(shaurma);
                    default:
                        // Не удалось привести ответ пользователя к требуемому виду.
                        // Введено число отсутствующее в перечислении типов добавок.
                        Console.WriteLine("Вы ввели некорректное значение!");
                        return shaurma;
                }
            }
            else
            {
                // Не удалось привести ответ пользователя к требуемому виду.
                // Введено не целое число.
                Console.WriteLine("Вы ввели некорректное значение!");
                return shaurma;
            }
        }
 
        /// <summary>
        /// Запросить у пользователя тип шаурмы.
        /// </summary>
        /// <param name="clientName">имя клиента.</param>
        /// <returns>Шаурма.</returns>
        private static Shaurma GetShaurmaType(string clientName)
        {
            // Выводим пользователю все возможные типы шаурмы
            // и сохраняем его ответ.
            Console.WriteLine($"{clientName}, выберите, пожалуйста, тип лаваша из представленных:");
            foreach (ShaurmaType t in Enum.GetValues(typeof(ShaurmaType)))
            {
                Console.WriteLine($"{(int)t} - {t.GetDescription()}");
            }
            var type = Console.ReadLine();
 
            // Пытаемя привести ответ пользователя к требуемому типу.
            if (int.TryParse(type, out int shaurmaType))
            {
                switch ((ShaurmaType)shaurmaType)
                {
                    case ShaurmaType.Standart:
                        // Шаурма в обычном лаваше.
                        return new StandartShaurma(clientName);
                    case ShaurmaType.Cheese:
                        // Шаурма в сырном лаваше.
                        return new CheeseShaurma(clientName);
                    case ShaurmaType.Arabic:
                        // Шаурма в арабском лаваше.
                        return new ArabicShaurma(clientName);
                    default:
                        // Не удалось привести ответ пользователя к требуемому виду.
                        // Введено число отсутствующее в перечислении видов шаурмы.
                        Console.WriteLine("Вы ввели некорректное значение!");
                        return null;
                }
            }
            else
            {
                // Не удалось привести ответ пользователя к требуемому виду.
                // Введено не целое число.
                Console.WriteLine("Вы ввели некорректное значение!");
                return null;
            }
        }
    }
}

Проверим работу программы.

Исходный код программы можно посмотреть в репозитории https://github.com/shwanoff/Decorator

Рекомендую также изучить статью Паттерн Наблюдатель (Observer). А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.