Builder

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

builder-class-diagram

  • Builder

    Product 객체의 요소들을 생성하기 위한 추상 인터페이스를 제공

  • ConcreteBuilder

    Builder에 정의된 인터페이스를 구현한다.

  • Director

    Builder 인터페이스를 사용하여 Product를 생성, 구성하여 반환한다.

  • Product

    Director가 Builder를 사용해서 생성한 결과물

1. 문제 상황

MazeGame의 createMaze는 미로를 생성하는 클래스이다.

class MazeGame {
  createMaze() {
    const maze = new Maze();
    const room1 = new Room(1);
    const room2 = new Room(2);
    const Door = new Door(r1, r2);

    maze.addRoom(room1);
    maze.addRoom(room2);

    room1.setSide("all", new Wall());
    room1.setSide("West", door);

    room2.setSide("all", new Wall());
    room2.setSide("East", door);

    // 미로 생성 추가 작업...

    return maze;
  }
}

createMaze는 미로를 생성하고, 1번 2번 방을 생성하고, 문을 생성하여 1번방과 2번방을 문으로 연결한다.

현재 createMaze는 미로를 어떻게 생성하는지미로가 어떻게 구성되어있는지에 대한 정보가 모두 들어있다.

미로를 어떻게 생성하는지에 대한 정보를 빌더패턴을 이용해 분리해보자.

2. 빌더 패턴 적용

createMaze의 미로를 생성하는 로직을 분리하여, createMaze는 단지 미로의 요소를 표현하도록 수정해보자.

미리 Builder를 사용하는 코드를 보면 이해가 편할것 같다.

createMaze(builder: MazeBuilder) {
  builder.buildMaze();
  builder.buildRoom(1);
  builder.buildRoom(2);
  builder.buildDoor(1, 2);
  return builder.getMaze();
}

위 코드는 Director의 코드이며, builder를 사용하여 미로가 어떻게 구성되어있는지만 표현한다.

  • Builder & ConcreteBuilder

    // Builder
    interface MazeBuilder {
      buildMaze: () => void;
      buildRoom: (num: number) => void;
      buildDoor: (room1: number, room2: number) => void;
    
      getMaze: () => Maze;
    }
    // ConcreteBuilder
    class StandardMazeBuilder implements MazeBuilder {
      private maze: Maze;
      buildMaze() {
        this.maze = new Maze();
      }
      buildRoom(num: number) {
        const room = new Room(num);
        this.maze.addRoom(room);
        room.setSide("all", new Wall());
      }
      buildDoor(roomNum1: number, roomNum2: number) {
        const room1 = this.maze.getRoom(roomNum1);
        const room2 = this.maze.getRoom(roomNum2);
        const door = new Door(room1, room2);
        room1.setSide(getCommonWallDirection(room1, room2), door);
        room1.setSide(getCommonWallDirection(room1, room2), door);
      }
      getMaze() {
        return this.maze;
      }
    }
    

    MazeBuilderBuilder Interface이며 미로를 생성하는 인터페이스를 정의한다.

    미로의 요소를 생성하는 로직은 MazeBuilder를 구현하는 ConcreteBuilder StandardMazeBuilder에 구현된다.

  • Director

    class MazeGame {
      createMaze(builder: MazeBuilder) {
        builder.buildMaze();
        builder.buildRoom(1);
        builder.buildRoom(2);
        builder.buildDoor(1, 2);
        return builder.getMaze();
      }
    
      createComplexMaze(builder: MazeBuilder) {
        builder.buildMaze();
        builder.buildRoom(1);
        // ...
        builder.buildRoom(100);
    
        return builder.getMaze();
      }
    }
    

    MazeGameDirector이며 Builder를 이용하여 미로를 완성시킨다.

    Director는 미로가 어떻게 생성되는지, 방은 어떻게 초기화되고 벽은 어떻게 생성되는지, 문은 어떻게 생성되는지 등 미로가 어떻게 생성되어 있는지 전혀 알 방법이 없다.

    단지 미로에 필요한 요소를 builder에게 요청하며 미로가 어떻게 구성되어있는지 표현할 뿐이다.

  • 사용

    const mazeGame = new MazeGame();
    const standardMazeBuilder = new StandardMazeBuilder();
    const standardMaze = mazeGame.createComplexMaze(standardMazeBuilder);
    

장점

  • Product에 대한 내부 표현을 다양하게 변화시킬 수 있다.

    Product를 조립할 때에는 Builder에 정의된 추상 인터페이스만을 통해 동작하기 때문에 새로운 재료를 쓰는 미로가 추가되거나, 기존 재료의 사용법이 변경되었을 때 Builder만 수정하면 된다.

  • 생성과 표현에 필요한 코드를 분리한다.

    Builder에 특정 Product의 부품을 생성하는 생성하는 방법이 구현되어 있기 때문에 재사용하여 다양한 Product를 찍어낼 수 있다.

  • 객체를 생성하는 절차를 세밀하게 나눌 수 있다.

    Builder패턴은 Director의 통제 아래 하나씩 내부의 구성요소를 만들어나간다.

    Product를 되돌려 받을 때 까지 Product를 조립할 수 있고 Director를 통해 세밀하게 나눠진 구성 과정을 확인할 수도 있다.

또다른 Builder 패턴

위의 Builder패턴을 보면 뭔가 어색하다고 느낄 수도 있다. 특히 Java에서 빌더패턴을 사용했다면 더 어색할것 같다.

Builder패턴은 객체 생성을 깔끔하고 유연하게하기 위해서도 사용된다.

class UserBuilder {
  public birthday: Date;
  public name: string;

  setBirthday(birthday: Date) {
    this.birthday = birthday;
  }

  setName(name: string) {
    this.name = name;
  }

  build() {
    return new User(this);
  }
}

class User {
  private name: string;
  private birthday: Date;
  constructor(userBuilder: UserBuilder) {
    this.name = userBuilder.name;
    this.birthday = userBuilder.birthday;
  }
  getUser() {
    return this.name + this.birthday;
  }
}

const user = new UserBuilder().setBirthday(new Date()).setName("hojin").build();

console.log(user.getUser());

장점

  • 객체 생성과 표현이 분리된다.

    객체를 생성하는 setter들이 있는 UserBuilder와 객체가 표현되어져있고 사용되는 User로 나뉘어진다.

  • property 수정에 유연하다.

    새로운 property가 추가되면 기존 로직은 그대로 두고, 새로운 setter를 정의하여 사용하면 되기 때문에 수정에 유연하다.