Builder

빌더, 생성 패턴 중 하나

사용 의도

  • 복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리해서 서로 다른 표현이라도 생성할 수 있는 동일한 절차를 제공할 수 있도록 한다.
  • 복잡한 객체를 단계별로 구성할 수 있도록 한다.

구체적 예시

문제 상황

복잡하게 많은 것을 요구하는 객체를 생각해보면, 이 객체를 만들어낼 때는 복잡하고, 많은 파라메타가 있는 constructor가 요구된다. 예를 들어서, House 객체를 만드는 것을 가정해보자. 간단한 집을 짓기 위해서는 4면의 벽, 바닥, 문, 딱 맞는 창문들, 지붕을 만들어야 한다. 그런데 추가적으로 뒷마당, 다른 굿즈들 등을 설정하려면 어떻게 해야 할까? 단순히 기본이되는 House 클래스를 확장하는 것은 수 많은 수정사항을 야기할 것이다. (subclass들을 모두 수정해야 함) 다른 방법으로 constructor를 방대하게 짜는 것인데, 이것은 constructor call을 아주 못생기고 낭비스럽게 해야한다는 문제가 있다.

// 못 생긴 constructor 호출
// House(doors, windows, rooms, garage, swimPool, ...)
const house1 = new House(2, 4, 4, null, null, null, ...);
const house2 = new House(1, 3, 2, true, null, true, ...);

해결 방법

빌더 패턴은 객체를 구성하는 코드를 클래스에서 분리시키고 builder에 따로 옮기는 방법이다. 빌더 패턴에서는 원하는 객체를 만들기 위해서 몇 개의 단계를 빌더 오브젝트에서 실행해야 한다. 중요한 점은 모든 단계를 수행할 필요가 없다는 것이다.

참여자

  • Builder: Product 객체의 일부 요소들을 생성하기 위한 추상 인터페이스를 정의한다.
  • ConcreteBuilder: Builder 클래스에 정의된 인터페이스를 구현하며, 제품의 부품들을 모아 빌더를 복합한다. 생성한 요소의 표현을 정의하고 관리한다. 또한 제품을 검색하는데 필요한 인터페이스를 제공한다.
  • Director: Builder 인터페이스를 사용하는 객체를 합성한다. 이름 그대로 감독과 같은 느낌
  • Product: 생성할 복합 객체를 표현한다.

협력 방법

  • 사용자는 Director 객체를 생성하고, 생성한 객체를 자신이 원하는 Builder 객체로 합성해 나감.
  • 제품의 일부가 구축될 때마다 DirectorBuilder에 통보한다.
  • BuilderDirector의 요청을 처리하여 제품에 부품을 추가한다.
  • 사용자는 Builder에서 제품을 검색한다.

결과

:)

  • 제품 내부 표현을 다양하게 변화할 수 있다.

    • 빌더를 사용하면 제품이 어떤 요소에서 복합되는지, 각 요소들의 표현 방법이 무엇인지 가릴 수 있게 된다.
  • 생성과 표현에 필요한 코드를 분리한다.

    • 사용자가 빌더와 상호작용을 통해 필요한 복합 객체를 생성하게 된다.
  • 복합 객체를 생성하는 절차를 좀 더 세밀하게 나눌 수 있다.

:(

  • 전체적인 코드의 복잡성은 올라가게된다. 패턴 자체가 새로운 여러 클래스를 요구하기 때문에.

코드 (Typescript)

Code

interface HouseBuilder {
  produceDoor(): void;
  produceWindow(): void;
  produceBackyard(): void;
  produceSwimPool(): void;
}

class ConcreteHouseBuilder implements HouseBuilder {
  private house: House = new House();

  public reset() {
    this.house = new House();
  }

  produceBackyard(): void {
    this.house.parts = "Back yard";
  }

  produceDoor(): void {
    this.house.parts = "2 Doors";
  }

  produceSwimPool(): void {
    this.house.parts = "Swim Pool";
  }

  produceWindow(): void {
    this.house.parts = "4 Windows";
  }

  getHouse(): House {
    const result = this.house;
    this.reset();
    return result;
  }
}

class House {
  private _parts: string[] = [];

  get parts(): string {
    return `The House with ${this._parts.join(", ")}`;
  }

  set parts(part: string) {
    this._parts.push(part);
  }
}

class HouseDirector {
  private builder: HouseBuilder;
  constructor(builder: HouseBuilder) {
    this.builder = builder;
  }

  buildMinimalHouse(): void {
    this.builder.produceDoor();
    this.builder.produceWindow();
  }
  buildFullFeaturedHouse(): void {
    this.builder.produceBackyard();
    this.builder.produceSwimPool();
    this.builder.produceDoor();
    this.builder.produceWindow();
  }
}

const clientCode = (): void => {
  const builder = new ConcreteHouseBuilder();
  const director = new HouseDirector(builder);

  console.log("Full Featured House: ");
  director.buildFullFeaturedHouse();
  const FullFeaturedHouse = builder.getHouse();
  console.log(FullFeaturedHouse.parts);

  console.log("");
  console.log("Minial House: ");
  director.buildMinimalHouse();
  const minimalHouse = builder.getHouse();
  console.log(minimalHouse.parts);

  console.log("");
  console.log("Custom House: ");
  builder.produceBackyard();
  builder.produceDoor();
  const customHouse = builder.getHouse();
  console.log(customHouse.parts);
};

clientCode();

Output

Full Featured House:
The House with Back yard, Swim Pool, 2 Doors, 4 Windows

Minial House:
The House with 2 Doors, 4 Windows

Custom House:
The House with Back yard, 2 Doors
Done in 0.15s.

Ref