Начало
В начале сделаем такое отступление, я хочу начать серию статей про паттерны проектирования. Думаю каждому хочется, чтобы его код был хорошо масштабируемым, не правда-ли. Все паттерны делятся на категории, и перед их рассмотренеим, лучше начать изучение с самих паттернов, иначе ты не поймешь почему Декоратор является составным паттерном, а Фабрика порождающим. После изучения уже интуитивно будет понятно, почему какой-либо паттерн относиться к той или иной категории.
Для самого изучения паттернов я постараюсь не погружаться в определения, а показать примеры, задать проблему и решение проблемы с использованием паттерна. Мы возьмем компанию «Сесла Моторс» и будем решать поставленные ею задачи. Пускай название не вызывает у вас чувства, что якобы вы где‑то уже слышали про нее. Это не так. Ведь компания занимается только производством электрокаров. А такое редко встретишь в реальном мире. Должно быть интересно. Погнали!.
Задача
У нашей компании есть множество моделей автомобилей. Основные из них «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