В наших решениях очень часто встречаются сложные объекты. Объекты, у которых есть несколько полей, и каждое поле сложно построить. Кому знаком этот тип объекта? Думаю, все. Иногда у нас есть гигантские конструкторы для создания объекта. Давайте посмотрим на пример:

public class Order
{
    public Buyer Buyer { get; set; }
    public Seller Seller { get; set; }
    public Product Product { get; set; }
    public double Price { get; set; }
    (...)
    public Order(Buyer buyer, Seller seller, 
                 Product product, double price, (...))
    {
            Buyer = buyer;
            Seller = seller;
            Product = product;
            Price = price;
            (...)
    }
}

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

Почему строитель - это решение?

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

Наиболее важные преимущества:

  • Более удобный в сопровождении код
  • Более читаемый код
  • Уменьшите количество ошибок при создании объекта

Но будьте осторожны, я не советую использовать этот шаблон для всех типов объектов. Чтобы построить простой объект, не нужно так много усилий. Вы должны проверить преимущества, которые вы получите при применении этого шаблона. Мы хотим, чтобы вы получали время, а не тратили его зря на создание классов построителей для всех ваших объектов.

Реализация OrderBuilder

public class OrderBuilder
{
    private Order _order = new Order();
    private readonly IBuyerRepository _buyerRepository;
    private readonly ISellerRepository _sellerRepository;
    private OrderBuilder(
                     IBuyerRepository buyerRepository, 
                     ISellerRepository sellerRepository)
    {
        _buyerRepository = buyerRepository;
        _sellerRepository = sellerRepository;
    }
    public static OrderBuilder Init(
                               IBuyerRepository buyerRepository, 
                               ISellerRepository sellerRepository)
    {
        return new OrderBuilder(buyerRepository, sellerRepository);
    }
    public Order Build() => _order;
    public OrderBuilder SetBuyer(int buyerId)
    {
        _order.Buyer = _buyerRepository.GetById(buyerId);
        return this;
    }
    public OrderBuilder SetSeller(int sellerId)
    {
        _order.Seller = _sellerRepository.GetById(sellerId);
        return this;
    }
    
    (...)
}

Это пример файла OrderBuilder. Чтобы все было еще проще, мы поместили логику для получения сложных объектов со стороны разработчика, как классы Buyer и Seller в приведенном выше блоке кода. Этим мы помогаем тем, кто хочет создать Order, поскольку нам просто нужны идентификаторы из каждого сложного класса. Чтобы разрешить эту функцию, нам нужно добавить некоторые зависимости репозиториев. Для этой цели был создан Init статический метод. Этот метод также позволяет нам использовать построитель без явного создания экземпляра. Однако, если вы предпочитаете, у вас может быть общедоступный конструктор, который получает необходимые репозитории.

Использование без застройщика

Buyer buyer = _buyerRepository.GetById(buyerId);
Seller seller = _sellerRepository.GetById(sellerId);
Product product = _productRepository.GetById(productId);
Order order = new Order(buyer, seller, product, price);

Использование со сборщиком со статическим инициированием

Order order = OrderBuilder.Init(buyerRepository, sellerRepository)
                          .SetBuyer(buyerId)
                          .SetSeller(sellerId)
                          .Build();

Использование со строителем с публичным конструктором

OrderBuilder orderBuilder = new OrderBuilder(buyerRepository,
                                             sellerRepository);
Order order = orderBuilder.SetBuyer(buyerId)
                          .SetSeller(sellerId)
                          .Build();

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

Что делать, если у меня несколько строителей? Как я могу организовать свой код?

Вы можете создать интерфейс, который будет реализован любым строителем.

В универсальном интерфейсе IBuilder есть только Build метод для возврата типа, который определяется каждым классом, реализующим его.

Конструкторы с необязательными параметрами

Иногда нам нужен объект, поддерживающий несколько комбинаций. Представим себе конструктор бургеров:

public Burger(int numPatties, bool cheese, bool bacon, 
              bool pickles, bool letuce, bool tomato)
{ ... }

Создание простого Fluent Builder

Мы можем создать класс строителя с плавным подходом. У нас есть простой класс Burger, который определяет количество пирожков по умолчанию.

public class Burger
{
    public int NumPatties { get; set; }
    public bool Cheese { get; set; }
    public bool Bacon { get; set; }
    public bool Pickles { get; set; }
    public bool Letuce { get; set; }
    public bool Tomato { get; set; }
    public Burger(int numPatties = 1)
    {
        NumPatties = numPatties;
    }
    public Burger(bool cheese, bool bacon, bool pickles, 
              bool letuce, bool tomato, int numPatties = 1)
    { ... }
}

Один из возможных вариантов - иметь конструктор, в котором все логические параметры передаются с некоторым значением. Результат будет примерно таким:

Burger uglyBurger = new Burger(true, true, false, false, false);

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

public class BurgerBuilder
{
    private Burger _burger = new Burger();
    public Burger Build() => _burger;
    public BurgerBuilder WithPatties(int num)
    {
        _burger.NumPatties = num;
        return this;
    }
    public BurgerBuilder WithCheese()
    { 
        _burger.Cheese = true;
        return this;
    }
    public BurgerBuilder WithBacon()
    {
        _burger.Bacon = true;
        return this;
    }
    (...)
}

В этой реализации у нас может получиться что-то вроде:

BurgerBuilder burgerBuilder = new BurgerBuilder();
Burger awesomeburger = burgerBuilder.WithCheese()
                                    .WithBacon()
                                    .Build();

Результат? Вкусный бургер с сыром и беконом… такой хороший и такой простой в приготовлении.

Заключение

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