Программирование

Когда использовать паттерн Builder: примеры и преимущества

Пошаговое руководство по использованию паттерна Builder в программировании. Реальные примеры применения, преимущества перед Factory и сравнение паттернов.

5 ответов 1 просмотр

Когда следует использовать паттерн Builder в программировании? Приведите распространенные примеры из реальной практики и объясните преимущества этого паттерна по сравнению с паттерном Factory.

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


Содержание


Когда следует использовать паттерн Builder в программировании

Паттерн Builder становится незаменимым инструментом в нескольких ключевых сценариях. Во-первых, когда объект имеет множество необязательных параметров, которые могут быть заданы в произвольном порядке. Представьте, что вам нужно создать объект конфигурации сервера с десятками опций — многие из них могут быть необязательными или иметь значения по умолчанию. Вместо создания конструктора с 20+ параметрами (что делает код нечитаемым) или множества перегруженных конструкторов, Builder позволяет пошагово добавлять нужные свойства.

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

В-третьих, этот паттерн shines, когда вам нужно создавать неизменяемые объекты (immutable objects) с множеством параметров. Поскольку Builder позволяет собрать все параметры перед созданием объекта, вы можете гарантировать, что объект будет полностью инициализирован и неизменяемый после создания.

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

java
// Вместо конструктора с множеством параметров:
Person person = new Person("Иван", "Иванов", 30, "Москва", "ул. Ленина, 123", 
 "ivan@example.com", "+7(999)123-45-67", true, 
 "Manager", 50000, "Java", "Python");

// Используем Builder для более читаемого кода:
Person person = new Person.Builder()
 .firstName("Иван")
 .lastName("Иванов")
 .age(30)
 .city("Москва")
 .address("ул. Ленина, 123")
 .email("ivan@example.com")
 .phone("+7(999)123-45-67")
 .isActive(true)
 .position("Manager")
 .salary(50000)
 .skills("Java", "Python")
 .build();

Реальные примеры применения паттерна Builder

В реальной практике паттерн Builder нашел широкое применение во многих областях разработки. Давайте рассмотрим несколько распространенных примеров:

1. Конфигурационные объекты

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

java
@Configuration
public class DatabaseConfig {
 private String host;
 private int port;
 private String username;
 private String password;
 private String database;
 private boolean sslEnabled;
 private int connectionPoolSize;
 // Другие параметры...
 
 public static class Builder {
 // Все поля DatabaseConfig
 public Builder host(String host) { /* */ }
 public Builder port(int port) { /* */ }
 // Другие методы установки...
 public DatabaseConfig build() { /* */ }
 }
}

2. API запросы

При работе с REST API часто нужно строить запросы с различными параметрами, фильтрами и опциями. Builder позволяет создавать читаемые запросы.

java
HttpRequest request = HttpRequest.newBuilder()
 .uri(URI.create("https://api.example.com/data"))
 .header("Content-Type", "application/json")
 .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
 .timeout(Duration.ofSeconds(10))
 .build();

3. UI компоненты

В фреймворках для создания пользовательских интерфейсов Builder может использоваться для сложных компонентов. Например, в Android при создании диалоговых окон.

java
AlertDialog.Builder builder = new AlertDialog.Builder(context)
 .setTitle("Внимание")
 .setMessage("Вы уверены, что хотите удалить этот элемент?")
 .setPositiveButton("Да", (dialog, which) -> deleteItem())
 .setNegativeButton("Нет", null)
 .setNeutralButton("Отмена", null)
 .setIcon(R.drawable.warning_icon);

4. Строительство сложных документов

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

5. Объекты с множеством опций

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

java
Coffee coffee = new Coffee.Builder()
 .size("Большой")
 .type("Капучино")
 .withMilk(true)
 .withSugar(2)
 .withCinnamon(true)
 .build();

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


Преимущества паттерна Builder перед Factory

Паттерн Builder и Factory решают схожие задачи по созданию объектов, но подходят для разных сценариев. Давайте рассмотрим преимущества Builder:

1. Избегаем “конструкторов с большим числом аргументов”

Как отмечено в JavaRush, Builder избавляет от проблемы “многих аргументов” в конструкторе. Когда у объекта 10+ параметров, создание объекта через конструктор становится нечитаемым и подверженным ошибкам.

2. Читаемость и безопасность

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

3. Гибкость и контроль над процессом создания

В отличие от Factory, который обычно возвращает готовый объект, Builder дает полный контроль над последовательностью шагов создания. Как указано в Хабре, Builder позволяет создавать различные варианты объекта во время его создания, а не только один предопределенный тип.

4. Поддержка необязательных параметров

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

5. Возможность создавать неизменяемые объекты

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

6. Легкое расширение

Добавление новых опций в объект с Builder не требует изменения клиентского кода, в отличие от Factory, где добавление нового типа потребует создания новой фабричного метода.

Как хорошо описано в Метаните, Builder позволяет пошагово строить сложные объекты с множеством опциональных частей, что невозможно с простым Factory.


Сравнение Builder и Factory: когда что использовать

Выбор между Builder и Factory зависит от конкретных требований к создаваемым объектам. Давайте разберем ключевые различия:

Когда использовать Factory:

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

Пример Factory:

java
// Простая фабрика для создания объектов разных типов
public class ShapeFactory {
 public static Shape createShape(String type) {
 switch (type) {
 case "circle": return new Circle();
 case "square": return new Square();
 default: throw new IllegalArgumentException("Unknown shape type");
 }
 }
}

Когда использовать Builder:

  1. Когда объект имеет множество необязательных параметров, которые могут быть установлены в произвольном порядке.
  2. Когда процесс создания объекта состоит из нескольких этапов, каждый из которых может быть опциональным.
  3. Когда важна читаемость кода при создании сложных объектов.
  4. Когда нужно создавать неизменяемые объекты с множеством параметров.
  5. Когда объекты могут иметь различные представления в зависимости от контекста использования.

Пример Builder:

java
// Builder для сложного объекта с множеством опций
public class ComplexObject {
 private String requiredParam;
 private Optional<String> optionalParam1;
 private Optional<Integer> optionalParam2;
 // Другие параметры...
 
 public static class Builder {
 private String requiredParam;
 private Optional<String> optionalParam1 = Optional.empty();
 private Optional<Integer> optionalParam2 = Optional.empty();
 
 public Builder(String requiredParam) {
 this.requiredParam = requiredParam;
 }
 
 public Builder optionalParam1(String value) {
 this.optionalParam1 = Optional.ofNullable(value);
 return this;
 }
 
 public Builder optionalParam2(Integer value) {
 this.optionalParam2 = Optional.ofNullable(value);
 return this;
 }
 
 public ComplexObject build() {
 // Проверка обязательных параметров
 if (requiredParam == null) {
 throw new IllegalStateException("Required parameter is missing");
 }
 
 // Создание объекта
 ComplexObject obj = new ComplexObject();
 obj.requiredParam = this.requiredParam;
 obj.optionalParam1 = this.optionalParam1.orElse(null);
 obj.optionalParam2 = this.optionalParam2.orElse(null);
 return obj;
 }
 }
}

Комбинированный подход

Иногда можно комбинировать оба паттерна. Например, можно использовать Factory для создания экземпляров Builder, а затем использовать Builder для детальной настройки объекта.

java
// Фабрика создает Builder для конкретного типа объекта
public class ObjectBuilderFactory {
 public static ObjectBuilder createBuilder(ObjectType type) {
 switch (type) {
 case TYPE_A: return new TypeAObjectBuilder();
 case TYPE_B: return new TypeBObjectBuilder();
 default: throw new IllegalArgumentException("Unknown object type");
 }
 }
}

// Пример использования
ObjectBuilder builder = ObjectBuilderFactory.createBuilder(ObjectType.TYPE_A);
ComplexObject obj = builder
 .withRequiredParam("value")
 .withOptionalParam1("optionalValue")
 .build();

Как хорошо видно из Доки, Builder дает больше контроля над процессом создания, особенно для сложных объектов, в то время как Factory лучше подходит для простого создания объектов известных типов.


Практическая реализация паттерна Builder

Давайте рассмотрим практическую реализацию паттерна Builder на нескольких языках программирования.

Реализация на Java

Классическая реализация Builder на Java включает класс продукта, интерфейс строителя и конкретный строитель:

java
// Продукт
public class Computer {
 private final String cpu;
 private final String ram;
 private final String storage;
 private final String gpu;
 private final String powerSupply;
 
 private Computer(Builder builder) {
 this.cpu = builder.cpu;
 this.ram = builder.ram;
 this.storage = builder.storage;
 this.gpu = builder.gpu;
 this.powerSupply = builder.powerSupply;
 }
 
 // Геттеры...
 
 // Строитель
 public static class Builder {
 private String cpu;
 private String ram;
 private String storage;
 private String gpu;
 private String powerSupply;
 
 public Builder(String cpu, String ram) {
 this.cpu = cpu;
 this.ram = ram;
 }
 
 public Builder storage(String storage) {
 this.storage = storage;
 return this;
 }
 
 public Builder gpu(String gpu) {
 this.gpu = gpu;
 return this;
 }
 
 public Builder powerSupply(String powerSupply) {
 this.powerSupply = powerSupply;
 return this;
 }
 
 public Computer build() {
 return new Computer(this);
 }
 }
}

// Использование
Computer gamingPC = new Computer.Builder("Intel i7", "32GB")
 .storage("1TB SSD")
 .gpu("RTX 3080")
 .powerSupply("750W")
 .build();

Реализация на C++

В C++ можно использовать шаблоны для более гибкой реализации Builder:

cpp
#include <string>
#include <memory>

class Product {
private:
 std::string part1;
 std::string part2;
 std::string part3;
 
public:
 void setPart1(const std::string& part) { part1 = part; }
 void setPart2(const std::string& part) { part2 = part; }
 void setPart3(const std::string& part) { part3 = part; }
 
 void show() {
 std::cout << "Product parts: " << part1 << ", " << part2 << ", " << part3 << std::endl;
 }
};

class Builder {
public:
 virtual ~Builder() = default;
 virtual void buildPart1() = 0;
 virtual void buildPart2() = 0;
 virtual void buildPart3() = 0;
 virtual std::unique_ptr<Product> getResult() = 0;
};

class ConcreteBuilder : public Builder {
private:
 std::unique_ptr<Product> product;
 
public:
 ConcreteBuilder() : product(std::make_unique<Product>()) {}
 
 void buildPart1() override {
 product->setPart1("Part1");
 }
 
 void buildPart2() override {
 product->setPart2("Part2");
 }
 
 void buildPart3() override {
 product->setPart3("Part3");
 }
 
 std::unique_ptr<Product> getResult() override {
 return std::move(product);
 }
};

class Director {
private:
 Builder* builder;
 
public:
 void setBuilder(Builder* b) { builder = b; }
 
 void construct() {
 builder->buildPart1();
 builder->buildPart2();
 builder->buildPart3();
 }
};

// Использование
Director director;
ConcreteBuilder builder;
director.setBuilder(&builder);
director.construct();
std::unique_ptr<Product> product = builder.getResult();
product->show();

Реализация на Python

В Python можно реализовать Builder с использованием dataclasses и методов:

python
from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class Computer:
 cpu: str
 ram: str
 storage: Optional[str] = None
 gpu: Optional[str] = None
 power_supply: Optional[str] = None
 peripherals: List[str] = field(default_factory=list)

class ComputerBuilder:
 def __init__(self, cpu: str, ram: str):
 self.computer = Computer(cpu=cpu, ram=ram)
 
 def with_storage(self, storage: str) -> 'ComputerBuilder':
 self.computer.storage = storage
 return self
 
 def with_gpu(self, gpu: str) -> 'ComputerBuilder':
 self.computer.gpu = gpu
 return self
 
 def with_power_supply(self, power_supply: str) -> 'ComputerBuilder':
 self.computer.power_supply = power_supply
 return self
 
 def add_peripheral(self, peripheral: str) -> 'ComputerBuilder':
 self.computer.peripherals.append(peripheral)
 return self
 
 def build(self) -> Computer:
 return self.computer

# Использование
computer = (ComputerBuilder("Intel i7", "32GB")
 .with_storage("1TB SSD")
 .with_gpu("RTX 3080")
 .with_power_supply("750W")
 .add_peripheral("Keyboard")
 .add_peripheral("Mouse")
 .build())

print(computer)

Реализация на JavaScript

В JavaScript можно использовать классы для реализации Builder:

javascript
class Product {
 constructor() {
 this.parts = [];
 }
 
 addPart(part) {
 this.parts.push(part);
 }
 
 listParts() {
 console.log(`Product parts: ${this.parts.join(', ')}`);
 }
}

class Builder {
 constructor() {
 this.reset();
 }
 
 reset() {
 this.product = new Product();
 }
 
 buildPartA() {
 this.product.addPart('PartA');
 }
 
 buildPartB() {
 this.product.addPart('PartB');
 }
 
 getProduct() {
 const result = this.product;
 this.reset();
 return result;
 }
}

class Director {
 constructor(builder) {
 this.builder = builder;
 }
 
 constructMinimalViableProduct() {
 this.builder.buildPartA();
 }
 
 constructFullFeaturedProduct() {
 this.builder.buildPartA();
 this.builder.buildPartB();
 }
}

// Использование
const builder = new Builder();
const director = new Director(builder);

director.constructMinimalViableProduct();
const p1 = builder.getProduct();
p1.listParts();

director.constructFullFeaturedProduct();
const p2 = builder.getProduct();
p2.listParts();

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


Источники

  1. JavaRush — Кофе-брейк: Паттерн проектирования Builder: https://javarush.com/groups/posts/3822-kofe-breyk-124-pattern-proektirovanija-builder-kak-rabotaet-serializacija-i-deserializacija-v-j

  2. Хабр OTUS — Паттерн Builder: https://habr.com/ru/companies/otus/articles/552412/

  3. Metanit — Паттерн Builder: https://metanit.com/sharp/patterns/2.5.php

  4. Дока — Паттерны проектирования: https://doka.guide/tools/design-patterns-creational/


Заключение

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

Основные преимущества Builder перед Factory включают избегание “конструкторов с большим числом аргументов”, более читаемый код, гибкость в установке параметров в произвольном порядке, и возможность создавать различные варианты объектов.

В реальной практике Builder нашел применение в конфигурационных объектах, API запросах, UI компонентах и многих других областях, где требуется гибкость и читаемость при создании сложных объектов.

Выбор между Builder и Factory зависит от конкретных требований: Builder лучше подходит для сложных объектов с множеством опций, в то время как Factory эффективен для простого создания объектов известных типов.

Эллеонора Керри / Писатель

Паттерн Builder следует использовать, когда объект имеет много необязательных параметров, которые могут быть заданы в произвольном порядке, и когда нужно избежать «конструкторов с большим числом аргументов». Он позволяет пошагово строить объект, гарантируя, что все обязательные поля заданы, а необязательные могут быть пропущены. В реальной практике Builder часто применяют при создании сложных конфигурационных объектов (например, конфигураций сервера, запросов к API, UI-компонентов), а также в библиотеках, где есть множество опций, как в StringBuilder и StringBuffer. Паттерн Builder имеет явные преимущества над Factory: он избавляет от «многих аргументов» в конструкторе, позволяет более читаемо и безопасно задавать параметры, а также обеспечивает неизменяемость промежуточного состояния.

Хабр / Сообщество IT-специалистов

Паттерн Builder полезен, когда объект имеет множество параметров, которые можно задавать поэтапно, и когда важно контролировать порядок и условия сборки. Он позволяет отделить логику создания от представления, упрощая поддержку и расширение кода. Паттерн Builder состоит из компонентов Product, Builder, ConcreteBuilder и Director, которые вместе управляют процессом сборки. В отличие от Factory, который просто возвращает готовый объект, Builder даёт полный контроль над последовательностью шагов, позволяет создавать иммутабельные объекты и менять их представление без изменения кода клиента. Преимущества: читаемость, гибкость, возможность переиспользовать Builder для разных представлений, и более строгая проверка валидности объекта.

Е

Builder паттерн позволяет инкапсулировать создание объекта и разделить его на этапы. Он полезен, когда процесс создания нового объекта не должен зависеть от того, из каких частей он состоит и как эти части связаны, а также когда требуется получить различные варианты объекта во время его создания. В реальной практике примером служит сборка хлеба: класс Baker (директор) использует конкретные строители RyeBreadBuilder и WheatBreadBuilder, чтобы собрать ржаной и пшеничный хлеб, каждый со своими компонентами. Паттерн Builder имеет преимущество перед Factory в том, что позволяет пошагово строить сложные объекты с множеством опциональных частей и генерировать несколько вариантов, тогда как Factory обычно возвращает готовый объект без возможности детальной настройки.

С

Паттерн Builder следует использовать, когда объект создаётся в несколько этапов, каждый из которых может быть опциональным, и при этом важно сохранить читаемость кода и избежать громоздких конструкторов. Например, в реальном проекте при создании напитков в кофейном баре можно использовать Builder для последовательного добавления свойств с поддержкой цепочки вызовов. В отличие от фабрики, которая обычно возвращает один тип объекта и скрывает детали создания, Builder позволяет последовательно добавлять свойства, поддерживать цепочку вызовов и легко расширять набор опций без изменения существующего кода. Преимущества Builder по сравнению с Factory: чёткая граница между этапами сборки и объектом, возможность создавать неизменяемые объекты без перегрузки конструктора, лёгкое добавление новых шагов без изменения клиентского кода.

Авторы
M
Разработчик
Е
Технический писатель
С
Технический писатель
Источники
Хабр / Сообщество IT-специалистов
Сообщество IT-специалистов
Проверено модерацией
Модерация
Когда использовать паттерн Builder: примеры и преимущества