Перейти к содержанию

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

Категория: структурный паттерн.

Проблема

Нужно добавить объекту дополнительное поведение (логирование, кеширование, подсчёт стоимости, шифрование), но: - через наследование это привело бы к комбинаторному взрыву подклассов на каждое сочетание "фич" (EspressoWithMilk, EspressoWithSyrup, EspressoWithMilkAndSyrup, ...); - поведение нужно добавлять не всем экземплярам класса, а только конкретным, и желательно динамически, в рантайме, а не жёстко на этапе компиляции через выбор подкласса.

Решение

  1. Выделяется общий интерфейс для базового объекта и всех "надстроек" над ним.
  2. Каждая дополнительная возможность оформляется отдельным классом-декоратором, который:
  3. реализует тот же интерфейс, что и оборачиваемый объект;
  4. хранит ссылку на этот объект;
  5. в своих методах вызывает методы обёрнутого объекта и добавляет к результату своё поведение.
  6. Декораторы можно комбинировать, оборачивая один в другой в произвольном порядке - каждый уровень обёртки ничего не знает о других уровнях.

Структура

  • Component - общий интерфейс базового объекта и декораторов.
  • ConcreteComponent - базовая реализация, которую оборачивают.
  • Decorator - абстрактный класс/интерфейс, хранящий ссылку на Component и делегирующий ему вызовы по умолчанию.
  • ConcreteDecorator - добавляет конкретное поведение до/после делегирования вызова обёрнутому объекту.

Когда применять

  • Нужно добавлять и убирать обязанности объекта динамически, в рантайме, без изменения его класса.
  • Расширение через наследование невозможно (класс sealed) или привело бы к взрывному росту числа подклассов на все комбинации возможностей.
  • Несколько независимых "сквозных" улучшений (логирование, кеш, ретраи, метрики) нужно применять к разным объектам в разных сочетаниях.

Плюсы

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

Минусы

  • Много маленьких, похожих друг на друга объектов - это усложняет отладку (нужно "размотать" цепочку оберток, чтобы понять итоговое поведение).
  • Порядок применения декораторов иногда имеет значение и может быть неочевиден при чтении кода.
  • Декоратор и оборачиваемый объект должны быть максимально похожи по интерфейсу - если декоратору нужно добавить новый метод, которого нет в базовом интерфейсе, паттерн перестаёт работать так гладко.

Отличие от Proxy

Оба паттерна оборачивают объект за тем же интерфейсом, но по назначению они разные: Decorator добавляет новое поведение (расширяет функциональность), Proxy контролирует доступ к объекту (может вообще не выполнять реальный вызов - например, при кешировании или проверке прав). Технически код может выглядеть похоже, разница - в цели использования.

Пример в .NET Framework / BCL

  • Иерархия Stream: GZipStream, CryptoStream, BufferedStream оборачивают базовый Stream (например, FileStream), добавляя сжатие, шифрование или буферизацию - классический учебный пример Decorator прямо из BCL.
  • Middleware в ASP.NET Core частично сочетает в себе идеи Decorator и Chain of Responsibility - каждый middleware оборачивает следующий в цепочке и может добавить поведение до/после вызова next().

Пример реализации на C#

Decorator.cs
using System;

namespace DesignPatterns.Structural.Decorator
{
    // Общий интерфейс для базового объекта и всех декораторов
    public interface ICoffee
    {
        string Describe();
        decimal GetCost();
    }

    // Базовая реализация (то, что декорируем)
    public sealed class Espresso : ICoffee
    {
        public string Describe() => "Эспрессо";
        public decimal GetCost() => 90m;
    }

    // Базовый декоратор хранит ссылку на оборачиваемый объект
    // и по умолчанию просто делегирует ему вызовы.
    public abstract class CoffeeDecorator : ICoffee
    {
        protected readonly ICoffee Inner;

        protected CoffeeDecorator(ICoffee inner)
        {
            Inner = inner;
        }

        public virtual string Describe() => Inner.Describe();
        public virtual decimal GetCost() => Inner.GetCost();
    }

    public sealed class WithMilk : CoffeeDecorator
    {
        public WithMilk(ICoffee inner) : base(inner) { }

        public override string Describe() => $"{Inner.Describe()} + молоко";
        public override decimal GetCost() => Inner.GetCost() + 30m;
    }

    public sealed class WithSyrup : CoffeeDecorator
    {
        private readonly string _flavor;

        public WithSyrup(ICoffee inner, string flavor) : base(inner)
        {
            _flavor = flavor;
        }

        public override string Describe() => $"{Inner.Describe()} + сироп ({_flavor})";
        public override decimal GetCost() => Inner.GetCost() + 20m;
    }

    public static class Demo
    {
        public static void Run()
        {
            // Декораторы можно комбинировать в любом порядке и количестве -
            // каждый добавляет своё поведение поверх предыдущего, ничего не зная об остальных.
            ICoffee order = new WithSyrup(new WithMilk(new Espresso()), "карамель");

            Console.WriteLine($"{order.Describe()} - {order.GetCost():C}");
        }
    }
}

Открыть Decorator.cs отдельно Скачать Decorator.cs