Начало

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

Для самого изучения паттернов я постараюсь не погружаться в определения, а показать примеры, задать проблему и решение проблемы с использованием паттерна. Мы возьмем компанию «Сесла Моторс» и будем решать поставленные ею задачи. Пускай название не вызывает у вас чувства, что якобы вы где‑то уже слышали про нее. Это не так. Ведь компания занимается только производством электрокаров. А такое редко встретишь в реальном мире. Должно быть интересно. Погнали!.

Задача

 У нашей компании есть множество моделей автомобилей. Основные из них «Model A» и «Model B». Сами по себе машины разного ценового сегмента. Но есть опции которые можно добавить в конфигурацию обоих машин. И компания «Сесла Моторс» хочет удобно получить финальную цену и информацию с собранной конфигурации электрокара. Как им это сделать?

Попробуем решить

Давайте начнем с того, что на самом деле паттерн декоратор это удобная «обертка» для основного класса, каждая обертка наделяет основной класс новыми свойствами. Чтобы можно было «оборачивать» основной класс, нужно иметь базовый класс как для обертки, так и для основного класса. Пусть у нас это будет абстрактный класс Car.

public abstract class Car
{
    private string Description;

    public Car()
    {
        Description = "Unknown car";
    }

    public Car(string description)
    {
        Description = description;
    }

    public virtual string GetDescription()
    {
        return Description;
    } 

    public abstract decimal Price();
}

Обратите внимание, что свойства Price и Description имеют разные идентификаторы доступа. Этим я хотел показать гибкость данного паттерна, поэтому не важно будем мы использовать abstract или virtual. Главное, чтобы была возможность переопределить свойство.

От Car будут наследоваться классы ModelA и ModelB. Напишем их реализацию. Пока все прекрасно и тривиально

public class ModelA : Car
{
    public ModelA() : base("Default ModelA") { }

    public ModelA(string description) : base(description) { }

    public override decimal Price()
    {
        return 40_000.034m;
    }
}

Реализацию ModelB можете написать самостоятельно.

По моему я что-то еще говорил про обертку. Хм... Вот здесь мне кажется и начнется основная логика декоратора. Давайте подумаем, как обернуть экземпляры класса ModelA или ModelB новой опцией. Я бы написал дополнительный абстрактный класс, в котором будет содержаться логика обертки

public abstract class CarPartsDecorator : Car
{
    public Car _car;

    public CarPartsDecorator(Car car)
    {
        _car = car;
    }
}

В будущем в этом классе можно будет написать абстрактный метод для реализации его в субклассах. То есть добавить дополнительные свойства для опций автомобиля.

Теперь реализуем одну из опций AutomaticParkingSystem (класс-наследник CarPartsDecorator)

public class AutomaticParkingSystem : CarPartsDecorator
{
    private readonly decimal _ownPrice;
    private readonly string _description;

    public AutomaticParkingSystem(Car car,
        string description, decimal ownPrice = 1_500m) : base(car)
    {
        _ownPrice = ownPrice;
        _description = description;
    }

    public override decimal Price()
    {
        return _car.Price() + _ownPrice;
    }

    public override string GetDescription()
    {
        return _car.GetDescription() + _description;
    }
}

Классы других опций также можете написать самостоятельно

Все, класс! Теперь посмотрим как в итоге выглядит наш сервис

Car car = new ModelA();

car = new WheelDisk(car, ownPrice: 2_300.4m);
car = new AutomaticParkingSystem(car, description: ", new automatic parking system");

Console.WriteLine($"ModelA price: {car.Price()}");
Console.WriteLine($"ModelB price: {car.GetDescription()}");

Вывод получился такой:

ModelA price: 43800,434
ModelB price: Default ModelA, simple wheel disk, new automatic parking system

Комментарии (17)


  1. a-tk
    24.06.2023 14:41
    +7

    Отличный способ вместо масштабирования получить комбинаторный взрыв.

    Шаблон декоратор вообще-то про другое.


  1. dopusteam
    24.06.2023 14:41
    +1

    идентификаторы доступа

    Что такое идентификатор доступа? Впервые слышу такой термин


  1. lair
    24.06.2023 14:41
    +9

    Во-первых, зачем нужна еще одна статья про паттерны проектирования?

    Во-вторых, прежде чем задаваться вопросом "Давайте подумаем, как обернуть экземпляры класса ModelA или ModelB новой опцией", нужно определиться, зачем что-то оборачивать, и почему нельзя обойтись обычным контейнером.

    В-третьих, для декоратора не нужен собственный базовый класс.

    Так что прежде чем писать еще одну статью про паттерны проектирования, неплохо бы сначала, гм, освоить эти самые паттерны. Потом может внезапно оказаться, что писать уже ничего не надо.


  1. lair
    24.06.2023 14:41
    +2

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

    Отдельный вопрос - а зачем рассматривать категории паттернов, и пытаться понять, почему один паттерн - "составной", а другой - "порождающий".


    1. nronnie
      24.06.2023 14:41
      +5

      почему Декоратор является составным паттерном

      Я сначала вообще не понял, что такое "составной паттерн", потому что в русская языка более как-то общепринято говорить "структурный" :)


  1. nronnie
    24.06.2023 14:41
    +10

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

    А вот за это:

    public Car _car;
    

    вас вообще когда-нибудь покалечат.


    1. a-tk
      24.06.2023 14:41

      Ну, есть языки, где нету интерфейсов, а декораторы иметь хочется... Но это явно не тот случай.


    1. a-tk
      24.06.2023 14:41

      Может быть человек пришёл с питона и тут осенило, что на типизации можно делать прикольные вещи.


  1. ryanl
    24.06.2023 14:41
    +2

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


    1. ilya-chumakov
      24.06.2023 14:41

      del


  1. ilya-chumakov
    24.06.2023 14:41
    +3

    По примерам кода выше уже проехались. Добавлю, что тема очень избитая, и сказать что-то новое (а иначе зачем писать статью, особенно на Хабр) достаточно сложно. Плюс, декоратор - сам по себе популярный и многим известный паттерн, и с его вариациями в DI, в сторонних библиотеках типа MediatR, или просто работая с middleware pipeline - сталкивались многие. Поэтому, опытному разработчику эта статья не нужна, а новичку я бы рекомендовал прочитать любой популярный гайд/курс/книгу по паттернам. Если очень хочется именно C# и на русском языке - есть прекрасная книга Сергея Теплякова. Да хотя бы статья на википедии.


  1. ioncorpse
    24.06.2023 14:41
    +2

    Car car = new ModelA();
    car = new WheelDisk..
    car = new AutomaticParkingSystem..

    Можно координаты автора? Я ему цветы на могилку принесу. Или, если коллеги не убили и ещё живой - торт.
    Я такой дичи за 20 лет работы не видел. Даже не знаю, плакать хочется, но и настолько оригинально!


    1. s207883
      24.06.2023 14:41
      +1

      Каждый день делаю системы из говен и палок. Этот пример идеально демонстрирует как можно сделать Candy candy = new Shit();

      Просто никто фишку не понял


      1. ryanl
        24.06.2023 14:41

        Наоборот надо, тип ссылки - какаха, а в рантайме фактически получаем конфетку.)


  1. Okunev_PY
    24.06.2023 14:41

    Это больше смахивает на композицию, но уж никак не на декортатор


    1. a-tk
      24.06.2023 14:41

      Но на ерунду гораздо сильнее.


  1. Weldervan
    24.06.2023 14:41

    Сильно упрощённо переписал, теперь видно наглядно, "простым языком без лишних слов"..
        public interface ICar
        {
            string GetDescription();
            decimal Price();
        }
    
        
        public class ModelA : ICar
        {
            public string GetDescription()
            {
                return "ModelA";
            }
    
            public decimal Price()
            {
                return 40_000.034m;
            }
        }
    
    
        
        public abstract class CarDecorator : ICar
        {
            protected ICar _car;
    
            public CarDecorator(ICar car)
            {
                _car = car;
            }
    
            public virtual string GetDescription()
            {
                return _car.GetDescription();
            }
    
            public virtual decimal Price()
            {
                return _car.Price();
            }
        }
    
    
        
        public class AutomaticParkingSystem : CarDecorator
        {
            public AutomaticParkingSystem(ICar car) : base(car)
            {
            }
    
            public override string GetDescription()
            {
                return $"{_car.GetDescription()}, new automatic parking system";
            }
    
            public override decimal Price()
            {
                return _car.Price() + 2_300.4m;
            }
        }