객체의 가능한 모든 구성에 대한 하위 클래스를 만드는 것은프로그램을 너무 복잡하게 만들 수 있다.
예를 들어 집을 짓기 위한 House
라는 객체를 생성하는 방법을 생각해보자. 간단한 집을 지으려면 네 개의 벽과 바닥을 만들고 문을 설치하고 한 쌍의 창문을 맞추고 지붕을 만들어야 한다. 하지만 뒤뜰과 기타 물품(난방 시스템, 배관 및 전기 배선 등)이 있는 더 크고 밝은 집을 원한다면 어떻게 될까?
가장 간단한 솔루션은 기본 House
클래스를 확장하고 매개변수의 모든 조합을 포함하는 하위 클래스 집합을 만드는 것이다. 그러나 결국엔 상당한 수의 하위 클래스를 만드는 것으로 끝날 것이다. 현관 스타일과 같은 새로운 매개변수를 사용하려면 이 계층을 훨씬 더 키워야 한다.
하위 클래스를 만들지 않는 또 다른 접근 방식이 있다. 집 House
객체를 제어하는 모든 가능한 매개 변수를 사용하여 기본 클래스에서 바로 거대한 생성자를 만들 수 있다. 이 접근 방식은 실제로 하위 클래스의 필요성을 없애지만, 또다른 문제를 야기한다.
매개변수가 많은 생성자는 단점이 있다. 모든 매개변수 각각이 항상 필요한 것이 아니기 때문이다.
대부분의 경우에, 매개변수 전부가 사용되지는 않아서 생성자가 보기 좀 흉할 것이다. 예를 들어, 몇몇 집에만 수영장이 있을 것이므로, 수영장과 관련된 매개변수는 대부분 쓸모가 없을 것이다.
Builder 패턴은 클래스 자체에서 객체 생성 코드를 추출하여 builders 라는 별도의 객체로 이동할 것을 제안한다.
빌더 패턴을 사용하면 복잡한 객체를 단계적으로 구성할 수 있다. 빌더는 빌드되는 동안 다른 객체가 제품에 액세스하는 것을 허용하지 않는다.
패턴은 객체의 구성을 어떠한 단계(buildWalls
, buildDoor
등)로 구성한다. 객체를 생성하려면 빌더 객체에 대해 일련의 이러한 단계를 실행한다. 중요한 점은 모든 단계를 호출할 필요는 없다는 것이다. 객체의 특정 구성을 생성하는 데 필요한 단계만 호출할 수 있다.
제품의 다양한 표현을 작성해야하는 경우 일부 구성 단계에서는 다른 구현이 필요할 수 있다. 예를 들어서, 오두막의 벽은 나무로 지을 수 있지만, 성벽은 돌로 지어야 한다.
이 경우 동일한 빌드 단계 세트를 구현하지만, 다른 방식으로 여러 다른 빌더 클래스를 작성할 수 있다. 그런 다음 구성 프로세스에서 이러한 빌더(즉, 빌딩 단계에 대한 순서화된 호출 집합)를 사용하여 다양한 종류의 객체를 생성할 수 있다.
다른 빌더는 다양한 방식으로 동일한 작업을 실행한다.
예를 들어, 나무와 유리로 모든 것을 짓는 건축가, 돌과 철로 모든 것을 짓는 건축가, 금과 다이아몬드를 사용하는 건축가들이 있다. 같은 단계를 호출해서 첫 건축가에서 일반 주택, 그 다음 건축가로부터 작은 성, 마지막 건축가로부터 궁전을 얻을 수 있다. 그러나 이것은 빌드 프로세스(단계)를 호출하는 클라이언트 코드가 모두 공통의 인터페이스를 사용하여 빌더와 상호작용할 수 있는 경우에만 작동한다.
++)더 나아가 제품을 director
라는 별도의 클래스로 구성하는 데 사용하는 빌더 단계에 대한 호출들을 추출할 수 있다. 이 클래스는 빌드 단계를 실행하는 순서를 정의하는 반면 빌더는 이러한 단계에 대한 구현을 제공한다.
제
프로그램에 director
클래스를 포함하는 것이 꼭 필요한 것은 아니다. 클라이언트 코드에서 직접 특정 순서로 빌드 단계를 항상 호출할 수 있다. 하지만 director
클래스는 다양한 구성 루틴을 배치하여 프로그램 전체에서 재사용할 수 있는 좋은 장소일 수 있다. 마치 배치 스크립트 처럼?
또한 director
클래스는 클라이언트 코드에서 제품 구성의 세부 정보를 완전히 숨긴다. 클라이언트는 건축업자를 감독과 연결하고 감독과 건설을 시작하고 건축업자로부터 결과를 얻기만 하면 된다.
Builder
인터페이스는 모든 유형의 빌더에 공통적인 제품의 구성 단계를 선언한다.ConcreteBuilder
는 건설 단계의 다양한 구현을 제공한다. 빌더는 공통 인터페이스에 더해 인터페이스에 정의되지 않은 메서드를 구현하여 다양한 제품을 생성하도록 할 수 있다.Product
는 빌더에 의해 생성된 객체이다. 다른 빌더에 의해 생성된 product는 동일한 클래스 계층 또는 인터페이스에 속할 필요가 없다.Director
클래스는 구성 단계를 호출하는 순서를 정의하므로 제품의 어떤 특정한 구성을 만들고 재사용할 수 있다.Client
는 빌더 객체 중 하나를 Director
와 연결해야 한다. 일반적으로는 생성자의 매개변수를 통해 주입받아 단 한번만 수행된다. 그런 다음에 Director
는 모든 추가 구성에 해당 빌더 객체를 사용한다. 그러나 Client
가 빌더 객체를 Director
의 프로덕션 메서드에 전달할 때를 위한 대체 접근 방식(위 사진에서는 changeBuilder)이 있다. 이 경우 이 메서드를 사용해서 Director
와 함께 무언가를 제작할 때마다 다른 빌더를 사용할 수 있다.Director
클래스를 만드느 것에 대해 고민해 본다. 동일한 빌더 객체를 사용하여 제품을 구성하는 다양한 방법을 캡슐화할 수 있다.Director
객체 모두를 생성한다. 구성을 시작하기 전에 클라이언트는 빌더 객체를 Director
에게 전달 해야 한다. 일반적으로 클라이언트는 의존성 주입을 통해, 즉 Director
의 생성자 매개변수를 통해 이 작업을 수행한다. Director
는 모든 추가 구성에서 빌더 객체를 사용한다. 아니면, 빌더가 Director
의 프로퍼티로 전달 될 수 있는 대체 접근 메서드를 구현할 수도 있다.Director
로부터 직접 구성 결과를 얻을 수 있다. 그렇지 않으면 클라이언트 코드는 빌더에서 결과를 가져와야 한다.Director
클래스는 어떻게 생성할지 생성 단계를 안내하는 역할을 한다.import XCTest
/// 빌더 인터페이스. 제품 객체를 생성하는 단계의 메서드를 기술한다.
protocol Builder {
func producePartA()
func producePartB()
func producePartC()
}
/// 구체 빌더 클래스. 빌더 인터페이스를 채택하고 구체적인 구성 단계의 구현을 기술한다.
/// 프로그램은 아마 몇몇 빌더의 바리에이션이 존재할 것인데, 이를 서로 다르게 구현할 수 있다.
class ConcreteBuilder1: Builder {
/// 갓 생성된 빌더 인스턴스는 텅빈 제품 객체를 가질 것이다.
private var product = Product1()
func reset() {
product = Product1()
}
/// 모든 생성 단계는 같은 제품을 가지고 이뤄진다.
func producePartA() {
product.add(part: "PartA1")
}
func producePartB() {
product.add(part: "PartB1")
}
func producePartC() {
product.add(part: "PartC1")
}
/// 구체적 빌더는 결과를 내보내는 자신만의 고유한 메서드를 제공해야 한다. 다양한 유형의 빌더가 동일한 인터페이스를 따르지 않는, 타입이 다른 제품을 만들 수 있기 때문이다.
/// 따라서 이러한 메서드는 기본 빌더의 인터페이스에서 선언할 수 없다.(적어도 정적으로 형식화된 프로그래밍 언어에서는)
/// 일반적으로, 최종 결과를 클라이언트에 반환한 후 빌더 인스턴스는 다른 제품 생산을 시작할 준비가 되어야 한다.
/// 이것이 아래 `getProduct` 메서드 본문의 끝에서 reset 메서드를 호출하는 것이 일반적으로 관행인 이유이다.
/// 그러나 이 동작은 필수가 아니며, 빌더가 이전 결과를 삭제하기 전에 클라이언트 코드에서 명시적인 리셋 메서드호출을 기다리게 할 수 있다.
func retrieveProduct() -> Product1 {
let result = self.product
reset()
return result
}
}
/// Director는 특정 순서에 따라 각 단계를 실행하는 것에 대해서만 책임이 있다.
/// 특정 주문이나 구성에 따라 제품을 생산해야 할 때 유용하다.
/// 엄밀히 말하자면, Director 클래스는 선택 사항이다. 클라이언트도 빌더를 직접 제어할 수 있기 때문이다.
class Director {
private var builder: Builder?
/// The Director works with any builder instance that the client code passes
/// to it. This way, the client code may alter the final type of the newly
/// assembled product.
func update(builder: Builder) {
self.builder = builder
}
/// Director는 동일한 구성 단계를 사용해서 여러 제품의 변형을 구성할 수 있다.
func buildMinimalViableProduct() {
builder?.producePartA()
}
func buildFullFeaturedProduct() {
builder?.producePartA()
builder?.producePartB()
builder?.producePartC()
}
}
/// 제품이 꽤 복잡하고, 광범위한 구성이 필요한 경우만 빌더 패턴을 사용하는 것이 좋다.
///
/// 다른 생성 패턴과는 달리, 다른 콘크리트 빌더는 다양한 제품을 생산할 수 있다.
/// 즉, 다양한 빌더의 결과는 항상 동일한 인터페이스를 따르지 않을 수 있다.
class Product1 {
private var parts = [String]()
func add(part: String) {
self.parts.append(part)
}
func listParts() -> String {
return "Product parts: " + parts.joined(separator: ", ") + "\n"
}
}
/// 클라이언트 코드는 빌더 객체를 만들면서 감독 객체로 의존성 주입을 한다.
/// 그리고 생성된 제품을 빌더 객체로부터 받는다.
class Client {
// ...
static func someClientCode(director: Director) {
let builder = ConcreteBuilder1()
director.update(builder: builder)
print("Standard basic product:")
director.buildMinimalViableProduct()
print(builder.retrieveProduct().listParts())
print("Standard full featured product:")
director.buildFullFeaturedProduct()
print(builder.retrieveProduct().listParts())
// 잊지마십쇼… 클라이언트는 director 객체 없이도 빌더를 사용할 수 있습니다.
print("Custom product:")
builder.producePartA()
builder.producePartC()
print(builder.retrieveProduct().listParts())
}
// ...
}
/// Let's see how it all comes together.
class BuilderConceptual: XCTestCase {
func testBuilderConceptual() {
let director = Director()
Client.someClientCode(director: director)
}
}
출처: Refactoring Guru
Builder in Swift