Builder Pattern

Seungjae·2022년 4월 19일
0

디자인 패턴 공부

목록 보기
3/4

복잡한 객체를 단계별로 구성할 수 있는 방안을 제시하는 디자인 패턴이다. 또한 해당 패턴을 통해 동일한 구성 코드를 사용하여 객체의 다양한 유형과 표현을 생성할 수 있다.

Problem


많은 필드와 중첩 객체를 힘들게 단계별로 초기화해야 한다고 가정해보자. 이런 복잡한 코드는 또한 클라이언트 코드 전체에 흩어져 있을 수도 있다. 예를 들어 House 객체를 생성한다고 생각하면, 기본적인 창문, 문, 지붕, 벽이 필요할 것이다. 하지만 경우에 따라서 앞마당이 있는 집, 차고가 있는 집도 있을 것이다. 이런 모든 상황을 어떻게 다 고려할 수 있을까?

가장 간단한 방법으로는 상속을 통한 해결이다. 하위 클래스에서 House를 확장하여 앞마당이 있는 하위클래스, 차고가 있는 하위 클래스를 만드는 것이다. 하지만 이렇게 될 경우 상당한 수의 하위 클래스가 만들어지게 된다. 또한 확장 요소가 추가될 때마다 하위 클래스를 추가로 만들어야한다.

다른 방법으로는 그냥 필요한 모든 것을 House에 넣고 그에 대한 생성자를 만들어주는 것이다. 이 방법으로 하위 클래스의 필요성을 제거할 수 있다. 그러나 생성자 호출이 매우 보기 흉해지고 앞마당이 있는 집을 거의 사용하지 않는 다면 앞마당 필드는 10번 중 9번을 필요가 없는 필드가 될 것이다.

Solve


Builder 패턴은 자체 클래스에서 객체 생성 코드를 추출해 Builder라는 별도의 객체로 이동시킨다. 객체를 생성하기 위해서는 Builder의 일련의 과정을 실행시키면 된다. 이때 포인트는 모든 단계를 호출할 필요가 없다는 것이다. 필요한 단계만 호출하여, 필요한 것들로만 구성하여 객체를 만들 수 있다.

또한 동일한 빌드 단계여도 다른 방식으로 여러 다른 빌더 클래스를 작성할 수 있다. 그 후 적합한 빌더를 사용하여 다양한 종류의 객체를 생성하는 것이다.

더 나아가 Director라는 별도의 클래스를 두어 빌더 단계에 대한 일련의 호출 부분을 뽑아낼 수도 있다. Dircetor는 빌드 단계를 실행하는 순서를 정의하고, 반면 Builder는 이러한 단계에 대한 구현을 제공한다. 물론 Director 클래스는 꼭 필요한 클래스는 아니다. 그냥 클라이언트 코드에서 직접 특성 빌드 순서를 호출해도 된다. 하지만 Director 클래스는 다양한 구성 루틴을 포함하여 프로그램 전체에서 재사용할 수 있는 좋은 장소이다. 그 뿐만 아니라 클라이언트 코드에서 제품 구성의 세부 정보를 완전히 숨길 수 있다는 장점도 있다.

Code


Builder

package builder

import java.time.LocalDate

interface Builder {
  fun seatCount(count: Int)
  fun carType(carType: CarType)
  fun engine(engine: Engine)
  fun dateOfManufacture(date: LocalDate)
}

CarBuilder

package builder

import java.time.LocalDate

class CarBuilder : Builder {
  var seatCount: Int? = null
  var carType: CarType? = null
  var engine: Engine? = null
  var dateOfManufacture: LocalDate? = null

  override fun seatCount(count: Int) {
    this.seatCount = count
  }

  override fun carType(carType: CarType) {
    this.carType = carType
  }

  override fun engine(engine: Engine) {
    this.engine = engine
  }

  override fun dateOfManufacture(date: LocalDate) {
    this.dateOfManufacture = date
  }

  fun build() = Car(seatCount, carType, engine, dateOfManufacture)
}

ManualBuilder

package builder

import java.time.LocalDate

class ManualBuilder : Builder {
  var seatCount: Int? = null
  var carType: CarType? = null
  var engine: Engine? = null
  var dateOfManufacture: LocalDate? = null

  override fun seatCount(count: Int) {
    this.seatCount = count
  }

  override fun carType(carType: CarType) {
    this.carType = carType
  }

  override fun engine(engine: Engine) {
    this.engine = engine
  }

  override fun dateOfManufacture(date: LocalDate) {
    this.dateOfManufacture = date
  }

  fun build() = Manual(seatCount, carType, engine, dateOfManufacture)
}

Car

package builder

import java.time.LocalDate

data class Car(val seatCount: Int?, val carType: CarType?, val engine: Engine?, val dateOfManufacture: LocalDate?)

Manual

package builder

import java.time.LocalDate

data class Manual(val seatCount: Int?, val carType: CarType?, val engine: Engine?, val dateOfManufacture: LocalDate?)

CarType

package builder

enum class CarType {
  SPORTS_CAR, SUV
}

Engine

package builder

data class Engine(val mileAge: Double, val volume: Double)

Director

package builder

import java.time.LocalDate

class Director {
  fun makeSportsCar(builder: Builder) {
    builder.seatCount(2)
    builder.carType(CarType.SPORTS_CAR)
    builder.engine(Engine(3.0, 0.0))
    builder.dateOfManufacture(LocalDate.of(1998, 2, 25))
  }

  fun makeSUV(builder: Builder) {
    builder.seatCount(4)
    builder.carType(CarType.SUV)
    builder.engine(Engine(2.5, 0.0))
    builder.dateOfManufacture(LocalDate.of(1998, 2, 25))
  }
}

Main

package builder

fun main() {
  println("which car is better?")
  println("1. SportsCar")
  println("2. SUV")

  val carBuilder = CarBuilder()
  val manualBuilder = ManualBuilder()
  val director = Director()

  when (readLine()) {
    "1" -> {
      director.makeSportsCar(carBuilder)
      director.makeSportsCar(manualBuilder)
    }
    "2" -> {
      director.makeSUV(carBuilder)
      director.makeSUV(manualBuilder)
    }
    else -> {
      println("Error!")
      return
    }
  }

  println("My Car is ${carBuilder.build()}")
  println("This Manual is ${manualBuilder.build()}")
}

구조


적용가능성


  • 선택적 매개변수가 매우 많은 생성자에 빌더 패턴을 적용할 수 있다. 패턴을 사용하면 더 이상 수십 개의 매개변수를 생성자에 집어넣을 필요가 없다.
  • Builder 패턴은 제품의 다양한 표현을 구성하며 세부 사항만 다른 유사한 단계를 포함할 때 적용할 수 있다. 기본 빌더 인터페이스는 가능한 모든 구성 단계를 정의하고 구체적인 빌더는 이러한 단계를 구현하여 제품의 특정 표현을 구성한다. Director는 빌드의 순서를 안내한다.
  • 빌더를 사용하여 제품을 단계별로 구성할 수 있다. 빌더는 빌드 단계 중 미완성인 제품을 노출하지 않는다. 즉 클라이언트 코드가 불완전한 결과를 가져오는 것을 방지할 수 있다.

장점﹒단점


장점

  • 개체를 단계별로 구성 가능
  • 제품의 다양한 표현을 작성할 때 동일한 구성 코드를 재사용
  • SRP => 제품의 비즈니스 로직에서 복잡한 구성 코드를 분리

단점

  • 패턴을 위해 여러 개의 새 클래스를 생성 -> 코드의 전반적인 복잡성이 증가

Git


https://github.com/oh980225/DesignPattern/tree/main/src/main/kotlin/builder

Ref


https://refactoring.guru/design-patterns/builder

profile
코드 품질의 중요성을 아는 개발자 👋🏻

0개의 댓글