Давайте рассмотрим паттерн проектирования Декоратор C# — Decorator C#, для чего он нужен и какие проблемы он решает. Где можно применять данный шаблон и когда это будет излишним.
Идея паттерна Декоратор
Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых структурных паттернов — Декоратор (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-канал. Там еще больше полезного и интересного для программистов.