Skip to main content

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

  • 코드의 규모에서 알 수 있듯이 다소 복잡할 수 있다.
Related Links