Builder
Builder — это порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово. Builder даёт возможность использовать один и тот же код строительства для получения разных представлений объектов.
Проблема
Представьте сложный объект, требующий пошаговой инициализации множества полей и вложенных объектов. Такой код обычно инициализируется внутри перегруженного конструктора с десятком параметров, либо ещё хуже — размазан по всему клиентскому коду.
Например, давайте подумаем о том, как создать объект House. Чтобы построить простой дом, нужно возвести четыре стены, установить двери, вставить пару окон и положить крышу. Но что если вы хотите дом побольше и светлее, с садом и бассейном?
Самое простое решение — расширить базовый класс House и создать набор подклассов для покрытия всех комбинаций параметров. Но в итоге получится множество подклассов. Любой новый параметр, например, тип материала для крыши, заставит вас растиражировать это иерархию классов.
Есть и другой подход. Можно не распылять логику создания по классам, а вынести её в один огромный конструктор базового класса House, который будет принимать все возможные параметры и управлять созданием объекта. Хотя такой подход и избавляет от необходимости создавать подклассы, он создаёт другую проблему.
Решение
Паттерн Builder предлагает вынести конструирование объекта за пределы его собственного класса, поручив это дело отдельным объектам, называемым строителями.
Структура
Builder
├── reset(): void
├── setSeats(number): void
├── setEngine(engine): void
├── setTripComputer(): void
└── setGPS(): void
ConcreteBuilder
├── product: Product
├── reset(): void
├── setSeats(number): void
├── setEngine(engine): void
├── setTripComputer(): void
├── setGPS(): void
└── getProduct(): Product
Director
├── builder: Builder
├── setBuilder(builder): void
└── constructSportsCar(): void
Product
└── parts: string[]
Пример реализации
JavaScript
// Продукт
class Car {
constructor() {
this.parts = [];
}
addPart(part) {
this.parts.push(part);
}
listParts() {
console.log(`Части автомобиля: ${this.parts.join(', ')}`);
}
}
// Интерфейс строителя
class CarBuilder {
reset() {
throw new Error('Метод reset должен быть переопределен');
}
setSeats(number) {
throw new Error('Метод setSeats должен быть переопределен');
}
setEngine(engine) {
throw new Error('Метод setEngine должен быть переопределен');
}
setTripComputer() {
throw new Error('Метод setTripComputer должен быть переопределен');
}
setGPS() {
throw new Error('Метод setGPS должен быть переопределен');
}
}
// Конкретный строитель
class ConcreteCarBuilder extends CarBuilder {
constructor() {
super();
this.car = null;
}
reset() {
this.car = new Car();
}
setSeats(number) {
this.car.addPart(`Сиденья: ${number}`);
}
setEngine(engine) {
this.car.addPart(`Двигатель: ${engine}`);
}
setTripComputer() {
this.car.addPart('Компьютер поездки');
}
setGPS() {
this.car.addPart('GPS навигация');
}
getProduct() {
const result = this.car;
this.reset();
return result;
}
}
// Директор
class Director {
constructor() {
this.builder = null;
}
setBuilder(builder) {
this.builder = builder;
}
constructSportsCar() {
this.builder.reset();
this.builder.setSeats(2);
this.builder.setEngine('V8');
this.builder.setTripComputer();
this.builder.setGPS();
}
constructSUV() {
this.builder.reset();
this.builder.setSeats(7);
this.builder.setEngine('V6');
this.builder.setGPS();
}
}
// Использование
const director = new Director();
const builder = new ConcreteCarBuilder();
director.setBuilder(builder);
// Создаем спортивный автомобиль
director.constructSportsCar();
const sportsCar = builder.getProduct();
sportsCar.listParts();
// Создаем SUV
director.constructSUV();
const suv = builder.getProduct();
suv.listParts();
// Создаем автомобиль вручную
builder.reset();
builder.setSeats(4);
builder.setEngine('I4');
builder.setGPS();
const manualCar = builder.getProduct();
manualCar.listParts();
TypeScript с интерфейсами
// Продукт
class Computer {
private parts: string[] = [];
addPart(part: string): void {
this.parts.push(part);
}
listParts(): void {
console.log(`Компоненты компьютера: ${this.parts.join(', ')}`);
}
}
// Интерфейс строителя
interface ComputerBuilder {
reset(): void;
setCPU(cpu: string): void;
setRAM(ram: string): void;
setStorage(storage: string): void;
setGPU(gpu: string): void;
}
// Конкретный строитель
class GamingComputerBuilder implements ComputerBuilder {
private computer: Computer;
constructor() {
this.computer = new Computer();
}
reset(): void {
this.computer = new Computer();
}
setCPU(cpu: string): void {
this.computer.addPart(`CPU: ${cpu}`);
}
setRAM(ram: string): void {
this.computer.addPart(`RAM: ${ram}`);
}
setStorage(storage: string): void {
this.computer.addPart(`Storage: ${storage}`);
}
setGPU(gpu: string): void {
this.computer.addPart(`GPU: ${gpu}`);
}
getProduct(): Computer {
const result = this.computer;
this.reset();
return result;
}
}
// Директор
class ComputerDirector {
private builder: ComputerBuilder;
setBuilder(builder: ComputerBuilder): void {
this.builder = builder;
}
buildGamingPC(): void {
this.builder.reset();
this.builder.setCPU('Intel i9-12900K');
this.builder.setRAM('32GB DDR5');
this.builder.setStorage('1TB NVMe SSD');
this.builder.setGPU('RTX 4080');
}
buildOfficePC(): void {
this.builder.reset();
this.builder.setCPU('Intel i5-12400');
this.builder.setRAM('16GB DDR4');
this.builder.setStorage('512GB SSD');
this.builder.setGPU('Integrated Graphics');
}
}
// Использование
const director = new ComputerDirector();
const builder = new GamingComputerBuilder();
director.setBuilder(builder);
director.buildGamingPC();
const gamingPC = builder.getProduct();
gamingPC.listParts();
director.buildOfficePC();
const officePC = builder.getProduct();
officePC.listParts();
Когда использовать
Используйте Builder, когда:
- Нужно создавать сложные объекты с множеством параметров
- Хотите создавать разные представления одно
- го объекта
- Нужно избежать перегруженных конструкторов
- Объект должен быть создан пошагово
- Нужна гибкость в процессе создания объекта
Преимущества
- Позволяет создавать объекты пошагово
- Позволяет использовать один и тот же код для создания разных объектов
- Изолирует сложный код создания объекта от его основной бизнес-логики
- Дает полный контроль над процессом создания объекта
Недостатки
- Усложняет код из-за введения дополнительных классов
- Может быть избыточным для простых объектов
Отличия от других паттернов
- Factory Method — создает один тип продукта
- Abstract Factory — создает семейства связанных объектов
- Prototype — создает объекты путем клонирования
Реальный пример
// Строитель для создания HTML элементов
class HTMLElementBuilder {
constructor(tagName) {
this.element = document.createElement(tagName);
this.attributes = {};
this.children = [];
}
addClass(className) {
this.element.classList.add(className);
return this;
}
setAttribute(name, value) {
this.element.setAttribute(name, value);
return this;
}
setText(text) {
this.element.textContent = text;
return this;
}
addChild(child) {
this.children.push(child);
return this;
}
build() {
this.children.forEach(child => {
this.element.appendChild(child);
});
return this.element;
}
}
// Использование
const button = new HTMLElementBuilder('button')
.addClass('btn')
.addClass('btn-primary')
.setAttribute('type', 'submit')
.setText('Отправить')
.build();
const form = new HTMLElementBuilder('form')
.setAttribute('action', '/submit')
.setAttribute('method', 'POST')
.addChild(button)
.build();
Builder — это идеальный паттерн для создания сложных объектов пошагово!