Menu
Coddy logo textTech

Паттерн Декоратор

Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy — урок 58 из 70.

Паттерн Декоратор — это структурный паттерн, который позволяет добавлять объектам новые обязанности, оборачивая их в специальные объекты-обертки, называемые декораторами. В отличие от наследования, которое добавляет поведение на этапе компиляции, декораторы расширяют функциональность динамически во время выполнения, не изменяя исходный класс.

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

public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Coffee";
    public decimal GetCost() => 2.00m;
}

public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;
    
    public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
    
    public virtual string GetDescription() => _coffee.GetDescription();
    public virtual decimal GetCost() => _coffee.GetCost();
}

public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }
    
    public override string GetDescription() => _coffee.GetDescription() + ", Milk";
    public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}

Сила декораторов заключается в их способности к наслоению. Вы можете обернуть декорированный объект еще одним декоратором:

ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee);  // Двойное молоко!

Console.WriteLine(coffee.GetDescription());  // Кофе, Молоко, Молоко
Console.WriteLine(coffee.GetCost());         // 3.00

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

challenge icon

Задание

Легко

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

Вы организуете свой код в трех файлах:

  • Pizza.cs: Определите интерфейс IPizza в пространстве имен PizzaShop с двумя методами: GetDescription(), возвращающим строку, и GetPrice(), возвращающим десятичное число (decimal). Затем создайте класс PlainPizza, который реализует этот интерфейс — он должен возвращать "Pizza" в качестве описания и 8.00m в качестве базовой цены.
  • ToppingDecorators.cs: Создайте абстрактный класс ToppingDecorator, который реализует IPizza и хранит ссылку на оборачиваемую пиццу (передаваемую через конструктор). Его виртуальные методы должны делегировать выполнение обернутой пицце. Затем создайте три конкретных декоратора:
    • CheeseTopping — добавляет ", Cheese" к описанию и прибавляет 1.50m к цене
    • PepperoniTopping — добавляет ", Pepperoni" к описанию и прибавляет 2.00m к цене
    • MushroomTopping — добавляет ", Mushrooms" к описанию и прибавляет 1.25m к цене
  • Program.cs: Объедините все вместе, начав с обычной пиццы и оборачивая ее топпингами на основе входных данных. Каждый декоратор топпинга оборачивает предыдущую пиццу, формируя окончательный заказ слой за слоем.

Вы получите одни входные данные:

  • Список топпингов для добавления, разделенных запятыми (например, cheese,pepperoni,cheese)

Начните с PlainPizza, затем оберните ее соответствующим декоратором для каждого топпинга в том порядке, в котором они появляются. Допустимые топпинги: cheese, pepperoni и mushrooms. После применения всех топпингов выведите описание на одной строке и общую стоимость на следующей.

Например, если на вход подается cheese,pepperoni,mushrooms, вывод должен быть следующим:

Pizza, Cheese, Pepperoni, Mushrooms
12.75

Обратите внимание, что один и тот же топпинг можно добавлять несколько раз! Если на вход подается cheese,cheese, вы получите:

Pizza, Cheese, Cheese
11

Это демонстрирует гибкость паттерна декоратор — каждый декоратор оборачивает любую пиццу, которую он получает, будь то обычная пицца или уже украшенная, что позволяет динамически наращивать функциональность!

Шпаргалка

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

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

public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Coffee";
    public decimal GetCost() => 2.00m;
}

public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;
    
    public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
    
    public virtual string GetDescription() => _coffee.GetDescription();
    public virtual decimal GetCost() => _coffee.GetCost();
}

public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }
    
    public override string GetDescription() => _coffee.GetDescription() + ", Milk";
    public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}

Декораторы можно накладывать друг на друга, оборачивая уже декорированный объект в новый декоратор:

ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee);  // Двойное молоко!

Console.WriteLine(coffee.GetDescription());  // Coffee, Milk, Milk
Console.WriteLine(coffee.GetCost());         // 3.00

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

Попробуйте сами

using System;
using PizzaShop;

class Program
{
    public static void Main(string[] args)
    {
        // Прочитать список топпингов, разделенных запятыми
        string input = Console.ReadLine();
        string[] toppings = input.Split(',');
        
        // TODO: Начать с PlainPizza
        
        // TODO: Пройтись циклом по каждому топпингу и обернуть пиццу
        // соответствующим декоратором на основе названия топпинга:
        // - "cheese" -> CheeseTopping
        // - "pepperoni" -> PepperoniTopping
        // - "mushrooms" -> MushroomTopping
        
        // TODO: Вывести итоговое описание и цену
    }
}
quiz iconПроверьте себя

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

Все уроки раздела Объектно-ориентированное программирование