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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 
×
%d такие блоггеры, как: