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

Основные концепции

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

Пример применения

Рассмотрим создание объекта Car, у которого много настроек, таких как тип двигателя, количество дверей, цвет и т.д.

Без использования паттерна «Билдер» мы можем столкнуться с такой проблемой: необходимо создавать различные конструкторы, что ухудшает читаемость и поддержку кода. Паттерн «Билдер» помогает избежать этого, при этом используя fluent API стиль — подход, при котором методы возвращают сам объект билдера, позволяя вызывать их цепочкой. Это делает код более выразительным и легким для чтения.

public class Car {
    private String engine;
    private int doors;
    private String color;
 
    private Car(CarBuilder builder) {
        this.engine = builder.engine;
        this.doors = builder.doors;
        this.color = builder.color;
    }
 
    public static class CarBuilder {
        private String engine;
        private int doors;
        private String color;
 
        public CarBuilder setEngine(String engine) {
            this.engine = engine;
            return this;
        }
 
        public CarBuilder setDoors(int doors) {
            this.doors = doors;
            return this;
        }
 
        public CarBuilder setColor(String color) {
            this.color = color;
            return this;
        }
 
        public Car build() {
            return new Car(this);
        }
    }
    
}

Использование паттерна:

Car car = new Car.CarBuilder()
    .setEngine("V8")
    .setDoors(4)
    .setColor("Red")
    .build();
 
System.out.println(car);

Преимущества

  1. Чистый код. Конфигурация объектов становится ясной и понятной, даже если у объекта множество параметров.
  2. Гибкость в создании объектов. Можно не указывать все параметры сразу, а добавлять их по мере необходимости, что делает процесс сборки более гибким.
  3. Поддержка иммутабельности. Объекты могут быть неизменяемыми после создания, так как параметры устанавливаются только в процессе сборки.
  4. Минимизация перегрузок конструкторов. Это позволяет избежать множества конструкторов для различных комбинаций параметров.

Недостатки

  • Усложнение кода. Добавление класса-билдера может увеличить объем кода, особенно если объект не настолько сложен, чтобы оправдать использование паттерна.
  • Многословность. Если объект требует только нескольких параметров, то использование билдера может казаться излишним и создавать ненужную многословность.

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

  • Когда объект имеет множество конфигураций и параметров.
  • Когда нужен гибкий процесс создания объектов с возможностью пошагового добавления параметров.
  • Когда объект должен быть неизменяемым, и после сборки его состояние не должно меняться.

Продвинутый билдер

Обязательные поля

Одной из распространенных проблем является отсутствие явного указания на обязательные поля при использовании билдера. Если объект имеет поля, которые обязательны для заполнения (например, идентификатор или имя), но их установка не контролируется билдером, это может привести к созданию некорректных или невалидных объектов.

Решение:

  • Обязательные поля можно передавать через конструктор билдера, чтобы их указание было обязательным. Остальные параметры можно указывать через цепочку методов.

Пример:

public class Car {
    private final String engine;  // Обязательное поле
    private final String model;   // Обязательное поле
    private int doors;            // Необязательное поле
    private String color;         // Необязательное поле
 
    // Приватный конструктор для сборки объекта через билдер
    private Car(CarBuilder builder) {
        this.engine = builder.engine;
        this.model = builder.model;
        this.doors = builder.doors;
        this.color = builder.color;
    }
 
    // Статический метод для создания билдера с обязательными полями
    public static CarBuilder builder(String engine, String model) {
        return new CarBuilder(engine, model);
    }
 
    public static class CarBuilder {
        private final String engine;  // Обязательное поле
        private final String model;   // Обязательное поле
        private int doors = 4;        // Значение по умолчанию
        private String color = "Black";  // Значение по умолчанию
 
        // Конструктор билдера с обязательными полями
        public CarBuilder(String engine, String model) {
            if (engine == null || engine.isEmpty()) {
                throw new IllegalArgumentException("Engine is required");
            }
            if (model == null || model.isEmpty()) {
                throw new IllegalArgumentException("Model is required");
            }
            this.engine = engine;
            this.model = model;
        }
 
        // Методы для установки необязательных полей
        public CarBuilder setDoors(int doors) {
            this.doors = doors;
            return this;
        }
 
        public CarBuilder setColor(String color) {
            this.color = color;
            return this;
        }
 
        // Метод для сборки объекта Car
        public Car build() {
            return new Car(this);
        }
    }
}

Теперь обязательные поля передаются при создании билдера:

Car car = Car.builder("V8", "Sedan")  // Передача обязательных полей через статический метод
    .setDoors(2)                      // Опциональные поля
    .setColor("Red")
    .build();

Валидация создания объекта

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

Решение:
Добавляйте в билдер логику валидации и проверки состояния перед созданием объекта. Это позволит убедиться, что все параметры совместимы и объект корректен.

Пример:

public Car build() {
    if (doors < 2 || doors > 6) {
        throw new IllegalArgumentException("Invalid number of doors");
    }
    return new Car(this);
}

Многократный вызов методов

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

Предположим, что у нас есть билдер для создания объекта Car, и метод для установки количества дверей (setDoors) был вызван дважды:

Car car = new Car.CarBuilder("V8")
    .setDoors(4)
    .setDoors(2)  // Этот вызов перезапишет предыдущее значение
    .setColor("Red")
    .build();

В результате объект car будет создан с двумя дверями, хотя программист мог ожидать, что будет 4 двери (из-за первого вызова). Такая ситуация особенно распространена, когда объект конфигурируется динамически, или когда несколько разработчиков работают с билдером, не зная всех деталей.

Возможные решения проблемы

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

public static class CarBuilder {
    private String engine;
    private int doors;
    private String color;
    private boolean doorsSet = false;  // Флаг, указывающий на то, что метод setDoors уже был вызван
 
    public CarBuilder(String engine) {
        this.engine = engine;
    }
 
    public CarBuilder setDoors(int doors) {
        if (doorsSet) {
            throw new IllegalStateException("Doors can only be set once");
        }
        this.doors = doors;
        doorsSet = true;
        return this;
    }
 
    public CarBuilder setColor(String color) {
        this.color = color;
        return this;
    }
 
    public Car build() {
        return new Car(this);
    }
}

Логирование перезаписи параметров. Если необходимо разрешить многократные вызовы методов, но при этом важно отслеживать перезапись параметров, можно добавлять логирование, чтобы было видно, когда параметр перезаписывается новым значением.

public CarBuilder setDoors(int doors) {
    if (this.doors != 0) {
        System.out.println("Warning: Doors value is being overwritten from " + this.doors + " to " + doors);
    }
    this.doors = doors;
    return this;
}

Использование Fluent API Step building. Позволит конфигурировать объект в определенной последовательности.

public class Car {
    private final String engine;  // Обязательное поле
    private final int doors;      // Обязательное поле
    private final String color;   // Обязательное поле
 
    // Приватный конструктор для сборки через пошаговую сборку
    private Car(String engine, int doors, String color) {
        this.engine = engine;
        this.doors = doors;
        this.color = color;
    }
 
    // Интерфейс для первого шага: выбор двигателя
    public interface EngineStep {
        DoorsStep setEngine(String engine);
    }
 
    // Интерфейс для второго шага: выбор дверей
    public interface DoorsStep {
        ColorStep setDoors(int doors);
    }
 
    // Интерфейс для третьего шага: выбор цвета
    public interface ColorStep {
        BuildStep setColor(String color);
    }
 
    // Интерфейс для финального шага: завершение сборки
    public interface BuildStep {
        Car build();
    }
 
    // Класс, который реализует пошаговую сборку
    public static class Builder implements EngineStep, DoorsStep, ColorStep, BuildStep {
        private String engine;
        private int doors;
        private String color;
 
        @Override
        public DoorsStep setEngine(String engine) {
            this.engine = engine;
            return this;
        }
 
        @Override
        public ColorStep setDoors(int doors) {
            this.doors = doors;
            return this;
        }
 
        @Override
        public BuildStep setColor(String color) {
            this.color = color;
            return this;
        }
 
        @Override
        public Car build() {
            return new Car(engine, doors, color);
        }
    }
 
    // Метод для запуска пошаговой сборки
    public static EngineStep builder() {
        return new Builder();
    }
}

Мета информация

Область:: 00 Разработка
Родитель:: Порождающий паттерн проектирования
Источник::
Создана:: 2024-10-04
Автор::

Дополнительные материалы

Дочерние заметки