Struchkov Mark
46e423c2db
All checks were successful
continuous-integration/drone/push Build is passing
319 lines
16 KiB
Markdown
319 lines
16 KiB
Markdown
---
|
||
aliases:
|
||
tags:
|
||
- maturity/🌱
|
||
date: 2024-10-04
|
||
zero-link:
|
||
parents:
|
||
linked:
|
||
---
|
||
Builder Pattern — это [[порождающий паттерн проектирования]], который используется для пошагового создания сложных объектов. Этот паттерн особенно полезен, когда объект может иметь множество конфигураций или параметров, которые делают его создание через конструкторы неудобным или даже невозможным.
|
||
|
||
Основные концепции
|
||
- **Разделение построения и представления**. Билдер позволяет отделить логику создания объекта от его конечной структуры. Это делает код более чистым и поддерживаемым.
|
||
- **Пошаговая сборка**. Позволяет добавлять параметры или части объекта последовательно, при этом сам процесс создания может контролироваться и меняться независимо от основной логики объекта.
|
||
- **Иммутабельность**. Паттерн билдер часто применяется для создания неизменяемых объектов. После сборки объекта его нельзя изменить, что улучшает предсказуемость и безопасность.
|
||
|
||
## Пример применения
|
||
Рассмотрим создание объекта `Car`, у которого много настроек, таких как тип двигателя, количество дверей, цвет и т.д.
|
||
|
||
Без использования паттерна «Билдер» мы можем столкнуться с такой проблемой: необходимо создавать различные конструкторы, что ухудшает читаемость и поддержку кода. Паттерн «Билдер» помогает избежать этого, при этом используя [[Fluent API|fluent API]] стиль — подход, при котором методы возвращают сам объект билдера, позволяя вызывать их цепочкой. Это делает код более выразительным и легким для чтения.
|
||
|
||
```java
|
||
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);
|
||
}
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
Использование паттерна:
|
||
```java
|
||
Car car = new Car.CarBuilder()
|
||
.setEngine("V8")
|
||
.setDoors(4)
|
||
.setColor("Red")
|
||
.build();
|
||
|
||
System.out.println(car);
|
||
```
|
||
|
||
**Преимущества**
|
||
1. **Чистый код**. Конфигурация объектов становится ясной и понятной, даже если у объекта множество параметров.
|
||
2. **Гибкость в создании объектов**. Можно не указывать все параметры сразу, а добавлять их по мере необходимости, что делает процесс сборки более гибким.
|
||
3. **Поддержка иммутабельности**. Объекты могут быть неизменяемыми после создания, так как параметры устанавливаются только в процессе сборки.
|
||
4. **Минимизация перегрузок конструкторов**. Это позволяет избежать множества конструкторов для различных комбинаций параметров.
|
||
|
||
**Недостатки**
|
||
- **Усложнение кода**. Добавление класса-билдера может увеличить объем кода, особенно если объект не настолько сложен, чтобы оправдать использование паттерна.
|
||
- **Многословность**. Если объект требует только нескольких параметров, то использование билдера может казаться излишним и создавать ненужную многословность.
|
||
|
||
**Когда применять?**
|
||
- Когда объект имеет множество конфигураций и параметров.
|
||
- Когда нужен гибкий процесс создания объектов с возможностью пошагового добавления параметров.
|
||
- Когда объект должен быть неизменяемым, и после сборки его состояние не должно меняться.
|
||
|
||
## Продвинутый билдер
|
||
### Обязательные поля
|
||
Одной из распространенных проблем является ==отсутствие явного указания на обязательные поля== при использовании билдера. Если объект имеет поля, которые обязательны для заполнения (например, идентификатор или имя), но их установка не контролируется билдером, это может привести к созданию некорректных или невалидных объектов.
|
||
|
||
**Решение**:
|
||
- Обязательные поля можно передавать через конструктор билдера, чтобы их указание было обязательным. Остальные параметры можно указывать через цепочку методов.
|
||
|
||
Пример:
|
||
```java
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Теперь обязательные поля передаются при создании билдера:
|
||
```java
|
||
Car car = Car.builder("V8", "Sedan") // Передача обязательных полей через статический метод
|
||
.setDoors(2) // Опциональные поля
|
||
.setColor("Red")
|
||
.build();
|
||
```
|
||
### Валидация создания объекта
|
||
Ещё одна частая проблема заключается в том, что во время процесса построения ==не проверяются ограничения на совместимость полей.== Например, не всегда проверяется корректность значений или логика взаимодействия между параметрами, что может привести к созданию некорректного объекта.
|
||
|
||
**Решение**:
|
||
Добавляйте в билдер логику валидации и проверки состояния перед созданием объекта. Это позволит убедиться, что все параметры совместимы и объект корректен.
|
||
|
||
Пример:
|
||
```java
|
||
public Car build() {
|
||
if (doors < 2 || doors > 6) {
|
||
throw new IllegalArgumentException("Invalid number of doors");
|
||
}
|
||
return new Car(this);
|
||
}
|
||
```
|
||
### Многократный вызов методов
|
||
Когда методы билдера вызываются несколько раз, каждый вызов может перезаписывать значение параметра, что может остаться незамеченным или вызвать непредсказуемое поведение.
|
||
|
||
Предположим, что у нас есть билдер для создания объекта `Car`, и метод для установки количества дверей (`setDoors`) был вызван дважды:
|
||
```java
|
||
Car car = new Car.CarBuilder("V8")
|
||
.setDoors(4)
|
||
.setDoors(2) // Этот вызов перезапишет предыдущее значение
|
||
.setColor("Red")
|
||
.build();
|
||
```
|
||
|
||
В результате объект car будет создан с двумя дверями, хотя программист мог ожидать, что будет 4 двери (из-за первого вызова). Такая ситуация особенно распространена, когда объект конфигурируется динамически, или когда несколько разработчиков работают с билдером, не зная всех деталей.
|
||
#### Возможные решения проблемы
|
||
**Введение проверок на повторный вызов.** Чтобы избежать многократных вызовов одного и того же метода, можно добавить логику проверки, которая будет отслеживать, был ли метод уже вызван ранее. Если метод вызывается повторно, можно выбросить исключение или проигнорировать повторный вызов.
|
||
|
||
```java
|
||
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);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Логирование перезаписи параметров.** Если необходимо разрешить многократные вызовы методов, но при этом важно отслеживать перезапись параметров, можно добавлять логирование, чтобы было видно, когда параметр перезаписывается новым значением.
|
||
|
||
```java
|
||
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|Fluent API Step building]]. Позволит конфигурировать объект в определенной последовательности.
|
||
|
||
```java
|
||
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();
|
||
}
|
||
}
|
||
```
|
||
|
||
***
|
||
## Мета информация
|
||
**Область**:: [[../../meta/zero/00 Разработка|00 Разработка]]
|
||
**Родитель**:: [[Порождающий паттерн проектирования]]
|
||
**Источник**::
|
||
**Создана**:: [[2024-10-04]]
|
||
**Автор**::
|
||
### Дополнительные материалы
|
||
-
|
||
|
||
### Дочерние заметки
|
||
<!-- QueryToSerialize: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->
|
||
|