DesignPatternSmalltalkCompanion:Builder

From 흡혈양파의 번역工房
Jump to: navigation, search

BUILDER (DP97)

의도

내부 구현체로부터 복잡한 객채의 생성자를 분리해내면 동일한 생성과정을 통해 다른 구현체를 생성하도록 할 수 있다

구조

Dpsc chapter03 builder 01.png

논의

문제의 컨텍스트ㅡ두 패턴을 적용할 수 있는 상황ㅡ와 패턴 해법과 관련해 여러 가지의 상호간 문제가 있기 때문에 아래 내용을 계속하기 이전에 추상 팩토리 패턴을 (31) 읽기를 권한다.

애플리케이션에서 사용자 선택을 바탕으로 한 복합 객체 또는 다른 객체로부터의 명세(specification)를 구축하도록 요구한다고 가정한다. 추상 팩토리에 설명된 것과 비슷한 자동차 조립 애플리케이션을 예로 들어보자. 애플리케이션은 서로 다른 제품, 구체적으로 말해 포드, 토요타 또는 포르쉐 자동차 제품을[1] 구축할 수 있어야 한다. 이에 당신은 생성해야 할 제품 유형을 명시하기 위해 애플리케이션에 플래그(flag)를 설정할 수 있을 것이다. 매번 전체 제품의 컴포넌트를 (예: 엔진) 생성할 필요가 있으며, 그 플래그를 (다음 페이지 참고) 바탕으로 조건문을 실행할 것이다.

AnApplication>>createEngine
	"Without Builder"
	manufacturer == #Ford
		ifTrue: [^FordEngine new].
	manufacturer == #Toyota
		ifTrue: [^ToyotaEngine new].
	^PorscheEngine new

모든 자동차와 자동차 컴포넌트마다 동일한 종류의 논리가 필요할 것이다. 또한 이 객체에게 하위컴포넌트 부품을(엔진, 차체, 변속기) 일관된 최종 제품(자동차)으로 조립하는 방법을 전송해야 한다. 이는 단일 클래스에겐 너무 많은 행위이다. 게다가 그다지 확장이 가능하지도 않다. 현재 알려진 자동차 브랜드가 3개라고 해서 사용자 또는 애플리케이션 도메인이 앞으로 다른 컴포넌트 타입을 필요로 하지 않을 것이란 보장이 없다 (예: 새로운 자동차 계열, 새턴Saturn이 시장에 소개됨). 우리는 주요 애플리케이션 자체를 변경하지 않고 애플리케이션의 기능성을 확장시킬 수 있는ㅡ즉 새로운 컴포넌트 타입을 추가하는 것과 같은ㅡ설계가 필요하다.

Builder 패턴이 이에 대한 해법을 제시한다. 이 패턴은 애플리케이션을 대신해 컴포넌트를 생성하고 조립하는 책임을 지는 별도의 Builder 객체를 만든다. 애플리케이션은 포드를 한데 모으기 위해 Ford Builder를, 토요타를 조립하기 위해선 Toyota Builder를, 그리고 포르쉐를 조립하기 위해 Porsche Builder를 사용할 수 있다.

Builder는 구축되는 것과 그것이 구성되는 과정에 대한 상세 내용을 그것의 클라이언트로부터 분리시킨다. 구축되는 것을 제품(Product)이라 부르는데, 우리 예제에선 전체 차량이 되겠다.; 클라이언트는 전체적인 생성 과정을 책임지므로 지시자(Director)라고 부른다. 지시자는 제품에 어떠한 일반적 하위컴포넌트가 (본 예제에선 엔진, 차체 등을 의미) 들어가는지 알지만 서로 다른 제품의 타입에서 인스턴스화시키기 위해 어떤 컴포넌트 클래스를 사용하는지는 알지 못한다 (예: 포드를 구성하는데 엔진이 필요 시 FordEngine 클래스를 인스턴스화시켜야 한다). 또한 지시자는 제품을 어떻게 조립하는지 알지 못하기 때문에 외부 헬퍼(helper), 즉 Builder 객체의 도움을 요청한다. Builder는 하위컴포넌트 부품을 추가하고 최종 제품을 검색할 수 있는 메시지 인터페이스를 제공한다.

우리는 여러 타입의 Builder가 필요하며 (포드, 토요타, 포르쉐 Builder) 애플리케이션은 사용자 선호 또는 프로그램의 상황에 따라 사용할 하나의 Builder를 런타임에서 선택할 것이다. 이를 가능하게 하기 위해 다수의 Builder 클래스를 정의하고 이 클래스들이 동일한 빌딩 메시지 집합에 대해 다형적으로 응답하도록 시킬 것이다. 따라서 Director에서 코드가 구축된 Builder 프로토콜을 따르기만 한다면 그것이 어떤 유형의 Builder에게 이야기하는지는 신경쓰지 않는다. 따라서 다른 종류의 Builder를 Director가 함께 플러깅(plugging) 함으로써 Director 객체를 변경할 필요 없이 매우 다른 제품들을 생성할 수 있는 것이다.

Director는 Builder 내에 캡슐화된 제품의 내부 표현에 대해 또는 제품을 어떻게 조립하는지에 대한 직접적 지식이 없기 때문에 Builder 패턴은 Director 클래스의 구현을 매우 단순화시킨다. 또한 Builder들은 주로 하나의 하위계층구조에 위치하는 구분된 클래스로 분리되기 때문에 Builder의 행위를 바꾸거나 새로운 타입의 구축를 애플리케이션으로 추가하는 것은 쉽다.

협력

다음 상호작용 다이어그램은 Builder 패턴의 협력을 설명한다. 이는 [디자인 패턴]편 DP 99 페이지에 실린 협력 다이어그램과 다른데, 그곳에 실린 다이어그램에는 Director 객체와 그 Director에 대한 클라이언트가 모두 포함되어 있으며, Director에 대한 클라이언트는 애플리케이션에 중요한 객체로 추정된다. 우리는 굳이 둘 다 필요하지 않은 것으로 생각된다; Director가 애플리케이션과 Builder 사이를 방해할 이유가 없다. 다시 말하지만 패턴을 그런 식으로 구현하도록 선택할 수는 있지만 개인의 미학에 관한 문제이다. 애플리케이션 자체가 빌딩 과정을 책임져야 하는가, 아니면 별도의 Director에 태스크를 위임해야 하는가? 본 패턴 예제에서는 애플리케이션이 Director의 역할을 맡도록 한다.

Dpsc chapter03 builder 02.png

본 예제의 다이어그램이 다른 이유는 스몰토크에서는 인스턴스 생성을 위해 클래스 객체로 전송되는 런타임 메시지를 설명할 필요가 있기 때문이다. Builder 클래스가 선택되어 인스턴스화되고 나면 Director는 Builder 객체에게 새 부품을 구축할 것을 반복 요청한다 (addPartA, addPartB, addPartC 메시지). 이에 응해 Builder는 개발 중인 제품에 부품을 추가한다. 하위컴포넌트 부품이 추가되면 Builder는 어떤 이해관계든 응답하지 (반환하지) 않는다 (모든 메서드는 무언가를 반환하기 때문에 수신자(receiver) 객체인 Builder는 자동적으로 반환된다). 빌딩 과정이 완료되면 Director는 Builder에게 getResult를 전송함으로써 최종 제품을 요청한다.

구현

부품추가하기

Builder는 내부 상태에 대한 몇 가지 양식을 가진다: 하위컴포넌트 명세를 수신하면 Builder는 캡슐화된 제품에 새 부품을 추가한다. 사실 Builder는 Director가 "어떠어떠한 부품을 추가하라" 라거나 "어떠어떠한" 명세를 말할 때마다 하위컴포넌트를 인스턴스화하면서 결과 받기 메시지가 수신될 때 전체적 최종 제품을 구축할 수 있는 선택을 가진다. 그럼에도 불구하고 어떤 경우든 최종 제품이 클라이언트에 의해 검색될 때까지 일부 데이터는 Builder 내에서 캡슐화된다.

"부품 추가" 주제에는 최소 3개의 변형이 있다.

  1. 양방향 다이어그램은 Director가 Builder에게 제품에 특정 부품 유형을 추가하라고 말해주는데 그친다는 사실을 보여준다ㅡA형 컴포넌트를 추가하기, B형 부품을 추가하기, 4개의 실린더형 엔진을 추가하기, 2개문 쿠페 차체 추가하기를 예로 들 수 있다. Builder는 적절한 클래스를 인스턴스화하고 개발 중인 제품에 새 부품을 추가한다.
  2. Director는 Builder에게 "원재료"를 건네주면서 Builder에게 어떤 원재료 부품을 변형시켜 그 결과를 제품에 추가하라고 말하기도 한다. [디자인 패턴]에서는 하나의 양식으로 (예: Rich Text 양식) 된 파일 내 정보를 다른 양식으로 (예: TeX) 변환시키는 예제를 보여준다. 그러므로 "부품을 추가하라" 는 요청은 "여기 RTF 폰트변경 토큰이 있다; 이것을 텍스트 폰트변경 명세로 변환한 후 너의 제품에 추가한다" 는 뜻을 암시하는지도 모른다. 따라서 Builder는 적절한 클래스를 단순히 인스턴스화시키기보다는 특별한 지식과 경험으로 구현되어야 한다.
  3. Director는 Builder에게 컴포넌트에 대한 추상 사양을 제공하며, 이 사양을 해석하고 이를 바탕으로 객체를 생성한 후 이렇게 만들어진 객체를 제품으로 추가하는 일은 Builder에게 맡긴다. 이는 꼭 비주얼웍스의 UIBuilder에서 이루어져야 한다: 비주얼웍스 UIPainter는 사용자가 사용자 인터페이스(UI) 윈도우의 위젯을 배열하도록 하고 이 레이아웃을 바탕으로 추상 사양을 생성한다. 여기에는 모든 윈도우 위젯에 대한 명세를 포함한다: 유형, 위치, 크기, 내용 (예: 라벨에 텍스트 표시) 등이 포함된다. 이러한 명세는 UI 윈도우와 관련된 애플리케이션 클래스의 메서드에 저장된다. 애플리케이션이 시작할 때 이에 해당하는 “윈도우 사양”은 각 위젯을 생성하고 전체 윈도우로 추가시키는 UIBuilder로 전달된다.

다수의 제품군

제품군은 주로 다수이지만 Builder가 하나의 제품군으로부터 부품을 생성하길 원한다. 우리 예제에서는 포드, 토요타, 포르쉐 계열 중 하나로부터 자동차와 하위부품을 인스턴스화하고자 했다. 추상 팩토리에서는 팩토리 객체 내 코드가 어떻게 제품군을 선택하는지 논하였다. 구현과 관련된 문제들은 Builder의 경우에도 마찬가지이다. 당신은 다수의 Builder 클래스를 정의할 수 있으며, 각 Builder는 인스턴스화할 컴포넌트 클래스를 하드코딩할 수 있다; 부품 카탈로그를 사용할 수도 있다; 팩토리 메서드 패턴을 적용시킬 수도 있다; 심지어 서로 다른 제품군에서 부품을 생성하는 서로 다른 메서드들로 단일 Builder 클래스를 정의할 수도 있다. 추상 팩토리에서 이 모든 선택권을 어떻게 구현하는지 보였지만 지금은 Builder에 대한 팩토리 메서드 해법을 상세히 알아보고 이전에 고려하지 않았던 대안적 해법을 알아보고자 한다.

팩토리 메서드 (63) 사용하기. 서로 다른 Builder가 동일한 메시지로 동일한 컴포넌트를 추가한다; 예를 들어, 문이 2개인 세단 차체를 추가하려면 Builder에게 add2DoorSedanBody를 전송한다. 이 메서드는 실제 인스턴스화되는 클래스를 제외하곤 모든 Builder 클래스에 동일하게 나타난다.

CarBuilder>>add2DoorSedanBody
	"Do nothing. Subclasses will override."
	
FordBuilder>>add2DoorSedanBody
	self car
		addBody: Ford2DoorSedanBody new
		
ToyotaBuilder>>add2DoorSedanBody
	self car
		addBody: Toyota2DoorSedanBody new
		
PorscheBuilder>>add2DoorSedanBody
	self car
		addBody: Porsche2DoorSedanBody new

위를 대신하는 방법으로는, 코드의 중복을 회피하고 각 구체적 Builder 클래스에 팩토리 메서드를 구현하는 방법이 있다:

CarBuilder>>add2DoorSedanBody
	"Define this once for all subclasses. SubClasses will
	override the 'create2DoorsedanBody' factory method."
	self car
		addBody: self Create2DoorSedanBody
	
CarBuilder>>create2DoorSedanBody
	"Factory method; This should only be implemented
	by my concreate subclasses."
	self implementedBySubclass	"Visual Smalltalk"
	"self subclassResponsibility"	"VisualWorks, IBM, others"

FordBuilder>>create2DoorSedanBody
	"Factory method; Return an instance of the Ford
	2-door-sedan body class."
	^Ford2DoorSedanBody new
		
ToyotaBuilder>>create2DoorSedanBody
	"Return an instance of the Toyota 2-door-sedan
	body class."
	^Toyota2DoorSedanBody new 
		
PorscheBuilder>>create2DoorSedanBody
	"Return an instance of the Porsche 2-door-sedan body object."
	^Porsche2DoorSedanBody new

추상 팩토리 (31) 사용하기. 제품군을 선택하는 또 다른 방법이다. 본 접근법을 비주월웍스에서 사용한 예를 살펴보자.

UIBuilder가 윈도우 위젯을 구축할 것을 요청받았다. 비주얼웍스는 다수의 운영체제에 대한 인터페이스 룩앤필을 지원하기 때문에 다수의 위젯 군이 있다: Motif 군 (MotifRadioButton과 같은 클래스), OS/2 CUA 군 (CUARadioButton) 등이 있다. UIBuilder는 최근 선택된 룩앤필 가족(family)으로부터 위젯을 생성해야 한다. 예를 들어, 사용자가 OS/2 CUA 모양을 선택했다면 UIBuilder는 라디오 버튼이 생성될 때 CUARadioButton을 인스턴스화시켜야 하는 것이다.

이 문제를 앞서 설명한 바와 같이 해결할 수도 있다: 여러 개의 Builder 클래스를 정의하여 우리가 원하는 하나를 인스턴스화시키는 방법이다. 여기서 UIBuilder를 추상 클레스로 정의하고 MotifUIBuilder, CUAUIBuilder, MacUIBuilder와 같은 구체적 서브클래스를 둘 수도 있다. 대신 비주얼웍스는 추상 팩토리 패턴을 2차 헬퍼 패턴으로 적용한다.

UIBuilder는 위젯 생성 태스크를 위임하는 것으로 UILookPolicy 객체로 구성된다; 즉, Builder는 그 모양 정책으로 윈도우 내에 모든 위젯의 인스턴스화할 것을 요청하는 것이다. UILookPolicy 추상 클래스의 각 구체적 서브클래스마다 동일한 위젯 생성 메시지를 구현하지만 특정 룩앤필에 따라 위젯을 생성한다: MacLookPolicy는 매킨토시 화면에 나타내는 것과 동일한 라디오 버튼을 생성하고, MotifLookPolicy 인스턴스는 모티프와 같은 버튼을 생성한다. 원하는 룩앤필에 따라 서브클래스들 중 하나는 UIBuilder의 모양 정책 객체로 인스턴스화된다. UIBuilder는 어떠한 유형의 모양이 생성되었는지 또는 어떠한 유형의 모양 정책 객체에 이야기하는지 전혀 알지 못한다; 단지 추상적 UILookPolicy 프로토콜에 정의된 위젯 생성 메시지를 전송할 뿐이다.

따라서 UILookPolicy 객체는 UIBuilder를 위한 팩토리의 역할을 하며, UIBuilder/UILookPolicy 프레임워크는 추상 팩토리 패턴의 인스턴스이다. UIBuilder라는 단일 Builder 클래스가 있지만 이는 다수의 제품(위젯)군으로부터 제품을 생성할 수 있다.

예제 코드

이제 앞서 다룬 자동차 조립 애플리케이션을 확장시켜 예를 하나 살펴보자. 사용자가 일반 자동차판매 쇼룸으로 들어갈 수 있게 해주는 애플리케이션이 있고, 당신이 간이상자에 위치한 컴퓨터로 다가가 자동차의 순서대로 조립한다고 치자. 이는 웹에서 가상 자동차 판매 쇼룸으로 사용할 수도 있을 것이다. 쇼품은 특정 회사에서만 제작된 자동차를 판매하는 곳이 아니라고 가정하자; 고객은 어떤 브랜드 자동차든 선택할 수 있다. 그녀가 원하는 옵션을 선택할 수도 있다. 사용자가 혼다 자동차를 선택하고 2개문 세단 차체, 4개 실린더 엔진, 자동차 변속기, 에어컨, 고급스러운 오디오 패키지, 고급 페인트칠과 같은 옵션을 선택한 후 Order라는 메뉴 항목을 선택한다고 상상해보자.

당신은 이 애플리케이션에 모든 자동차와 옵션에 대해 공통된 사용자 인터페이스가 있길 원하며, 사용자의 선택을 바탕으로 가상 자동차를 조립하길 원한다고 가정하자. 즉 추상 팩토리 예제와 같이 애플리케이션이 화면에 3D 탐색이 가능한 이미지로 자동차를 만들어 고객이 자동차를 관찰하고 "투어"하도록 해주는 것이다. 자동차를 실제로 주문하기 전에 자동차의 구성을 변경하고 다시 볼 수 있다. 마지막으로 애플리케이션은 사용자가 설명한대로 자동차의 주문을 생성한다.

추상 팩토리에 사용된 제품 클래스가 비슷하다고 가정하되, 클래스 수는 이 경우가 더 많을 것이다. 옵션의 선택을 허용하기 때문에 FordEngine 클래스 뿐만 아니라 다음과 같이 더 많은 클래스가 있을 것이다.

Dpsc chapter03 builder 03.png

ToyotaEngine, PorscheEngine, FordBody, ToyotaBody, PorscheBody 아래에도 서로 비슷한 하위계층구조가 있을 것이고 기타 모든 컴포넌트에도 마찬가지다. 이 애플리케이션에서 Builder 부분과 관련해서는 CarBuilder 계층구조를 정의함으로써 시작한다.

Dpsc chapter03 builder 04.png

Object subclass: #CarBuilder
	instanceVariableNames: 'car'
	classVariablesNames: ''
	poolDictionaries: ''
	
CarBuilder class>>new
	^self basicNew initialize 
	
CarBuilder>>car
	"getter method"
	^car
	
CarBuilder>>car: aCar
	"setter method"
	car := aCar
	
CarBuilder subclass: #FordBuilder
	instanceVariableNames: ''	
	classVariablesNames: ''
	poolDictionaries: ''
	
CarBuilder subclass: #ToyotaBuilder
	...
	
CarBuilder subclass: #PorscheBuilder
	...

CarBuilder 의 car 인스턴스 변수는 Builder의 제품을 참조한다. Builder가 처음 인스턴스화되고 나면 car가 적절한 Car 서브클래스의 인스턴스로 (비어있는 셸; a.Car에는 아직 하위컴포넌트가 없음) 초기화된다.

FordBuilder>>initialize
	self car: FordCar new 
	
ToyotaBuilder>>initialize
	self car: ToyotaCar new
	
PorscheBuilder>>initialize
	self car: PorscheCar new

Builder가 컴포넌트를 추가하라는 요청을 수신하면 car로 컴포넌트를 수신한다. "하위컴포넌트를 추가하라" 는 메시지를 정의한다고 가정하자: 우리는 어떤 일도 하지 않기 위해 추상 상위클래스에서 이 메시지를 정의하고, 필요 시 구체적 Builder 서브클래스로 오버라이드한다.

CarBuilder>>add4CylinderEngine
	"Do nothing. Subclasses will override"
	
FordBuilder>>add4CylinderEngine
	self car addEngine: Ford4CylinderEngine new
	
ToyotaBuilder>>add4CylinderEngine
	self car addEngine: ToyotaCylinderEngine new
	
PorscheBuilder>>add4CylinderEngine
	self car addEngine: Porsche4CylinderEngine new
	


CarBuilder>>addStandard6CylinderEngine
	"Do nothing. Subclasses will override"
	
FordBuilder>>addStandard6CylinderEngine
	self car addEngine: FordStandard6CylinderEngine new
	
ToyotaBuilder>>addStandard6CylinderEngine
	self car addEngine: ToyotaStandard6CylinderEngine new
	
PorscheBuilder>>addStandard6CylinderEngine
	self car addEngine: PorscheStandard6CylinderEngine new

여기서 Car들은 addEngine:, addBody 등과 같은 메시지로 인해 자체로 하위컴포넌트를 추가하는 방법을 알고 있는 것으로 가정한다. 우리 예제 코드에선 구현되지 않았지만 이러한 메서드들은 만약 클라이언트가 엔진, 차체, 변속기, 또는 기타 부품을 하나 이상의 추가를 시도할 경우 오류를 신호로 보낼 수도 있다.

이제 예제 애플리케이션의 사용자 인터페이스에 대해 이야기해보자. 사용자는 메뉴로부터 자동차 제조업체를 선택한 후 해당 메뉴로부터 엔진 유형, 차체 유형, 기타 특성을 선택할 수 있을 것이다. 사용자가 여태까지 구축한 것을 보고 싶다면 Action 메뉴에서 Draw를 선택한다. 이에 대한 응답으로 애플리케이션은 그래픽 창에서 자동차를 표시한다 (메뉴 바 아래). 사용자가 자동차를 주문하고 싶어한다면 동일한 메뉴에서 Order를 선택한다. 사용자 인터페이스는 다음과 같은 모습일 것이다 (Audio System, Trim, 기타 하위컴포넌트에 풀다운 메뉴가 추가될 것이다).

Dpsc chapter03 builder 05.png

사용자 인터페이스가 CarAssemblerUI 객체에 의해 구현된다고 하자. CarAssemblerUI는 CarBuilder 인스턴스로 구성되고, CarAssemblerUI는 Builder에게 사용자 선택을 바탕으로 자동차 컴포넌트를 생성시켜 추가하라고 전한다. 비주얼 스몰토크(Visual Smalltalk)에서 이 애플리케이션을 구현하는 코드를 소개하겠다. 첫째, 우리는 사용자 인터페이스 애플리케이션을 ViewManager의 서브클래스로서 정의한다:

ViewManager subclass: #CarAssemblerUI
	instanceVariableNames: 'builder'
	classVariablesNames: ''
	poolDictionaries: ''

윈도우는 자동차가 그려질 하나의 그래픽 창과 과 함께 앞서 언급한 메뉴를 포함할 것이다. 여기서 질문해야 할 한 가지는, CarAssemblerUI가 어떻게 자동차 제조업체에 대한 사용자의 선택을 바탕으로 적절한 Builder 클래스를 인스턴스화할 것인가이다. 사용자가 이 선택을 한 Car 메뉴를 구축한 메서드를 살펴보자. 메서드는 스몰토크의 반영적 성능 중 하나를 활용한다: 이로 인해 한 클래스의 모든 서브클래스를 결정한다. 메뉴는 각각의 구체적 CarBuilder 서브클래스마다 하나의 메뉴 항목으로 구성된다. 각 메뉴 항목의 라벨은 해당하는 클래스로 manufacturer 메시지를 전송함으로써 얻을 수 있다. 각 항목과 관련된 실행은 CarAssemblerUI>userChoseBuilder: 메서드를 호출하는 메시지로서, 해당하는 Builder 클래스 객체를 argument로서 전달한다.

CarAssemblerUI>>CarMenu
	"Build the car-manufacturers menu."
	| menu |
	menu := Menu new
		title: 'Car';
		owner: self.
	CarBuilder subclasses do: [ :aClass |
		menu
			appendItem: aClass manufacturer	"the label"
			action: (Message 					"the action"
								receiver: self
								selector: #userChoseBuilder:
								arguments: (Array with: aClass)) ].
	^menu

사용자가 Car 메뉴로부터 제조업체를 선택 시 userChoseBuilder: 는 전달된 클래스를 메시지 argument로 인스턴스화시킴으로써 적절한 Builder를 생성한다. 따라서 이 Builder는 자동차의 모든 하위컴포넌트 부품을 생성하고 조립하는데 사용된다.

CarAssemblerUI>>userChoseBuilder: builderClass
	builder := builderClass new.
	...

이것을 실행하기 위해선 각 CarBuilder 클래스마다 manufacturer 메서드를 정의할 필요가 있다.

CarBuilder class>>manufacturer
	self implementedBySubclass
	
FordBuilder class>>manufacturer
	^'Ford'
	
ToyotaBuilder class>>manufacturer
	^'Toyota'
	
PorscheBuilder class>>manufacturer
	^'Porsche'

이제 자동차 하위컴포넌트에 대한 메뉴가 필요하다. 여기서 모든 Car 들을 동일한 일반 부품으로 구성한다는 단순화를 가정으로 한다; 예를 들어 모든 자동차는 4개 실린더, 표준 6개 실린더 또는 터보 충전식 6개 실린더 엔진 중 하나를 포함할 수 있는 것이다. 물론 Builder마다 각 엔진 유형에 대해 다른 클래스를 인스턴스화시킨다. 4개 실린더 엔진의 경우 애플리케이션은 Ford4CylinderEngine, Toyota4CylinderEngine, Porsche4CylinderEngine 중 하나를 인스턴스화해야 한다.

Engine 메뉴의 엔트리를 호출하는 메서드를 명시하는 코드가 있다:

CarAssemblerUI>>engineMenu
	^Menu new
		title: 'Engine';
		owner: self;
		appendItem: '4-Cylinder'
			selector: #engineIs4Cylinder;
		appendItem: '6-Cylinder Standard'
			selector: #engineIsStandard6Cylinder;
		appendItem: '6-Cylinder Turbocharged'
			selector: #engineIsTurbocharged6cylinder;
		yourself

이에 따라 사용자가 Engine의 풀다운 메뉴로부터 4-Cylinder를 선택하면 CarAssemblerUI>>EngineIs4Cylinder 메서드가 호출된다. 이 메서드는 단순히 Builder에게 일반 메시지인 add4CylinderEngine을 전송할 뿐이다.

CarAssemblerUI>>engineIs4Cylinder
	"The user has selected the '4-Cylinder' menu item
	from the 'Engine' pulldown menu. Tell by Builder."
	self builder add4CylinderEngine

각 Builder 클래스는 그 부품군에 따라 적절한 클래스를 인스턴스화시키기 위해 add4CylinderEngine을 구현한다 (이 메서드들은 앞서 언급한 바 있다). 그 외 컴포넌트에 대한 메서드도 이와 비슷한 방식으로 구현된다.

CarAssemblerUI>>engineIsStandard6Cylinder
	"The user has selected the 'Standard 6-Cylinder' 
	menu item from the 'Engine' pulldown menu."
	self builder addStandard6CylinderEngine
	
CarAssemblerUI>>engineIsTurbocahrged6Cylinder
	"The user has selected the 'Turbocharged 6-Cylinder' 
	menu item from the 'Engine' pulldown menu."
	self builder addTurbochargedCylinderEngine

사용자가 Action 메뉴에서 Order 또는 Draw 메뉴 항목을 클릭하면, 애플리케이션은 조립된 자동차를 바탕으로 그림을 그리거나 주문을 객체로 전달하기 전에 Builder의 제품을 (조립된 자동차) 검색해야 한다.

사용자가 Order를 선택 시 호출되는 메서드를 살펴보도록 하자:

CarAssemblerUI>>orderCar
	"The user has selected the 'Order' menu item, signaling
	all car/components selections have been made."
	| completeCar |
	"Get the assembled car from my Builder:"
	completeCar := builder assembledCar.
	completeCar isNil ifTrue: [^MessageBox message:
		'you haven''t finished assembling a complete car yet!'].
	...
	"Assemble and print an invoice for the assembled car:"
	CarInvoiceMaker new printInvoiceFor: completeCar

CarBuilder>>assembledCar는 Builder의 "결과 얻기" 메서드이다. 이는 자동차에 엔진이나 차체와 같은 필수 하위컴포넌트가 모두 있다는 것을 확인하고, 완성된 자동차를 반환한다.

CarBuilder>>assembledCar
	"Return my final Product after verifying there's
	a completed Product to return."
	car isNil ifTrue: [^nil].
	car engine isNil ifTrue: [^nil].
	...
	^car

구조적으로 우리 애플리케이션은 다음 다이어그램과 같은 모양이다:

Dpsc chapter03 builder 06.png

이와 유사한 추상 팩토리 다이어그램과 이 그림 간 차이를 찾아보자. 추상 팩토리에서 팩토리는 개별적 컴포넌트를 생성하도록 요청받을 수 있고, 적절한 객체를 반환시키기도 한다. 팩토리 클라이언트가 원한다면 이 부품들 각각을 더 큰 제품으로 추가할 수도 있지만 팩토리 자체는 이를 알지 못한다.

Builder의 경우 각 컴포넌트의 생성을 요청받을 수 있지만 요청이 있을 때마다 Builder는 어떠한 것도 반환하지 않는다; 대신 새로 생성된 컴포넌트가 Builder 내에 캡슐화된 제품에 추가된다. 이후에 모든 하위컴포넌트가 추가되면 Builder에게 최종 제품을 요청할 수 있다.

알려진 스몰토크 사용예

[디자인 패턴]에서는 비주얼웍스에서 (DP105) Builder 패턴의 알려진 사용예 3가지를 언급하였다. 이 중 한 가지인 ClassBuilder를 추가 예제와 함께 상세히 설명하고자 한다.

ClassBuilder

비주얼웍스에서 ClassBuilder 인스턴스는 새 클래스를 생성하거나 기존 클래스를 변경하기 위해 불러온다. 예를 들어, 일반적인 클래스 생성 메시지는 다음과 같다:

ASuperClass subclass: #ASubclass
	instanceVariableNames: 'var1 var2'
	classVariablesNames: 'ClassVar1'
	poolDictionaries: ''
	category: 'Companion Examples'

비주얼웍스 이미지로부터 이 메시지를 직접 구현하면 다음과 같다.

Class>>subclass: t instanceValiableNames: f classVariableNames: d 
	poolDictionaries: s category: cat
	"This is the standard initialization message for
	creating a new class as a subclass of an existing
	class (the receiver)"
	
	^self classBuilder
		superclass: self;
		environment: self environment;
		className: t;
		instVarString: f;
		classVarString: d;
		poolString: s;
		category: cat;
		beFixed;
		reviceSystem
	
	Behavior>>classBuilder
		^ClassBuilder new

첫 번째 메시지는 (self classBuilder) 팩토리 메서드를 사용해 ClassBuilder의 인스턴스를 생성한다. 다음 페이지에서 나타내는 바와 같이 Behavior>>classBuilder 메서드는 이와 같은 모습을 한다 (Class 클래스는 Behavior로부터 상속된다):

Behavior>>classBuilder
	^ClassBuilder new

따라서 생성 또는 변경되는 클래스의 다양한 하위컴포넌트 속성들을 설정하기 위해 이 ClassBuilder 인스턴스로 수 많은 메시지가 전송된다. 예를 들어, 클래스의 상위클래스가 set되고, 클래스 이름이 할당된다. 마지막으로 reviseSystem에서 ClassBuilder는 새로운 클래스 객체를 구성하거나 (기존에 클래스가 없을 경우) 기존 클래스를 변경하는 작업 중 하나를 수행한다. 이것은 Builder의 “결과 얻기” 메시지이다.

MenuBuilder

비주얼웍스에서 MenuBuilder는 각 메뉴 부품을 추가하기 위한 메서드를 구현한다 (메뉴 항목, 구분선, 하위메뉴). 각 부품에 관한 정보는 그 제품에 대한 MenuBuilder의 내부 표현에 첨부되지만 사실은 최종적으로 "결과 얻기"" 메시지가 수신될 때까지는 어떤 것도 구축하지 않는다. 모든 부품이 추가되고 나면 Director가 menu 메시지를 전송시킴으로써 완료된 제품에 대해 MenuBuilder를 요청한다. 이에 응해 MenuBuilder는 Menu 객체를 생성시켜 반환한다.

UIBuilder

비주얼웍스에서 UIBuilder는 사용자 인터페이스 창과 하위컴포넌트 위젯을 구성한다. 각 인터페이스 위젯에 대한 명세는 add: 메시지로 UIBuilder에 제공될 수 있다. 위젯 명세가 모두 추가되고 나면 UIBuilder에게 최종 제품 창열기를 요청할 수 있다. 비주얼웍스 이미지의 UIBuilder 클래스에서 수 많은 예제를 발견할 수 있을 것이다 (예제 범주의 클래스 메서드를 참고).

관련 패턴

Strategy

Builder 패턴은 전략 (339) 패턴과 유사하다; 사실 Builder는 전략의 구체화라고 말할 수도 있다. 두 패턴 간 차이점은 사용의 목적이다. Builder는 클라이언트를 대신해 새 객체를 하나씩 구성하는데 사용된다; 다른 타입의 Builder 객체들이 동일한 일반 빌딩 프로토콜을 구현하지만 사실상 서로 다른 클래스를 인스턴스화시킬 수도 있다. 전략은 알고리즘에 대해 추상 인터페이스를 제공하는데 사용된다; 즉 전략 객체는 객체로서 알고리즘의 구체화라고 할 수 있다; 서로 다른 전략 객체들이 동일한 일반 서비스에 대한 대안적 구현을 제공한다.

Abstract Factory

Builder와 추상 팩토리 (31) 생성 패턴이 서로 밀접한 관련이 있다는 사실은 이미 살펴보았다. 두 패턴 모두 여러 개의 제품군 중 하나로부터 제품을 인스턴스화하는 상황에 사용된다. 차이점이라고 하면, 추상 팩토리의 경우 모든 컴포넌트 부품을 인스턴스화하고 반환하기 위해 추상 팩토리를 불러오며 (호출될 때마다 팩토리는 제품을 반환), 그 클라이언트는 부품들을 좀 더 복잡한 하나의 객체로 조립해도 좋다. Builder는 최종 제품에 컴포넌트를 추가하기 위해 조금씩 불러오며 제품이 Builder 객체 내에 캡슐화된다. 여기서는 클라이언트 대신 Builder가 제품 조립자가 된다. 클라이언트가 필요한 모든 컴포넌트 부품을 추가하면 그는 Builder에게 최종 제품을 요청한다.

Notes

  1. 설명을 위해 지나친 단순화를 사용할 것이다. 물론 현실에선 자동차 제조업체의 수와 각 제조업체가 생산하는 자동차 모델 수는 더 많다.