Builder Pattern
View all Design Patterns
Subway에서 샌드위치를 주문할 때 옵션을 선택하는 과정처럼 복잡한 객체를 각 단계별로 생성하는 방법이다. 또한 **점층적 생성자 패턴(Telescoping constructor anti-pattern)**의 문제를 해결할 수 있다.
Class Diagram
Abstract overview
Concrete example
이렇게 구성하게 되면 클라이언트는 SandwichDirector
와 여기에 연결한 Concrete Class인 SandwichBuilder
에 의존하게 되며, 필요한 일련의 과정을 SandwichDirector
가 수행하게 된다.
Code
interface Ingredients {
bread: string;
lettuce: boolean;
tomato: boolean;
cucumber: boolean;
avocado: boolean;
}
class Sandwich {
ingredients: Ingredients;
constructor(builder: SandwichBuilder) {
this.ingredients = builder.getIngredients();
}
show() {
console.log(`Sandwich is made of ${JSON.stringify(this.ingredients)}`);
}
getIngredients() {
return this.ingredients;
}
}
interface SandwichBuilder {
addBread(bread: string): SandwichBuilder;
addLettuce(): SandwichBuilder;
addTomato(): SandwichBuilder;
addCucumber(): SandwichBuilder;
addAvocado(): SandwichBuilder;
build(): Sandwich;
getIngredients(): Ingredients;
}
class ClubSandwichBuilder implements SandwichBuilder {
bread: string;
lettuce: boolean;
tomato: boolean;
cucumber: boolean;
avocado: boolean;
constructor() {
this.bread = "Bread";
this.lettuce = false;
this.tomato = false;
this.cucumber = false;
this.avocado = false;
}
addBread(bread: string) {
this.bread = bread;
return this;
}
addLettuce() {
this.lettuce = true;
return this;
}
addTomato() {
this.tomato = true;
return this;
}
addCucumber() {
this.cucumber = true;
return this;
}
addAvocado() {
this.avocado = true;
return this;
}
build() {
const sandwich = new Sandwich(this);
this.reset();
return sandwich;
}
getIngredients() {
return {
bread: this.bread,
lettuce: this.lettuce,
tomato: this.tomato,
cucumber: this.cucumber,
avocado: this.avocado,
};
}
/**
* Or builder instance would be shared through all builders
*/
reset() {
this.bread = "Bread";
this.lettuce = false;
this.tomato = false;
this.cucumber = false;
this.avocado = false;
}
}
class SandwichDirector {
makeClubSandwich(builder: SandwichBuilder) {
return builder
.addBread("White Bread")
.addLettuce()
.addTomato()
.addCucumber()
.addAvocado()
.build();
}
makeClubSandwichWithoutCucumber(builder: SandwichBuilder) {
return builder
.addBread("White Bread")
.addLettuce()
.addTomato()
.addAvocado()
.build();
}
}
// when client creates an instance
const builder = new ClubSandwichBuilder();
const director = new SandwichDirector();
const clubSandwich = director.makeClubSandwich(builder);
clubSandwich.show();
이렇게 구성하게 되면, 하나의 ClubSandwich
라는 거대한 클래스를 통해 인스턴스를 생성하지 않아도 된다. 즉, 다음과 같은 경우(점층적 생성자 패턴 문제)를 피할 수 있다.
class ClubSandwich {
// even some ingredients wouldn't be used at most cases
constructor (bread, lettuce, tomato, bacon, turkey, mayonnaise, bread) {
...
}
}
만약 cucumber
를 빼고 싶다면, SandwichDirector
에서 이를 없앤 director 메서드를 생성하면 된다.
class SandwichDirector {
...
makeClubSandwichWithoutCucumber(builder: SandwichBuilder) {
return builder
.addBread("White Bread")
.addLettuce()
.addTomato()
.addAvocado()
.build();
}
}
Pros and Cons
Pros
- 추상 팩토리 패턴과 유사하지만, 추상 팩토리 패턴은 연관된 객체들의 패밀리를 구성하는 데에 초점이 있고, 빌더 패턴의 경우 객체를 생성할 때 단계별로 생성하는 데에 초점이 맞춰있다.
- 팩토리 패턴의 경우 완성된 객체를 곧바로 반환하지만, 빌더 패턴의 경우, 각 단계에 수정을 할 수 있다는 특징이 있다.
- 옵션이 다양할 경우 유용하게 사용할 수 있다.
- 일반적으로 빌더 메서드의 경우
.addLettuce().addTomato().addAvocado()
처럼 체이닝이 가능하다.
Cons
- 코드의 규모에서 알 수 있듯이 다소 복잡할 수 있다.