DesignPatternSmalltalkCompanion:Prototype
PROTOTYPE (DP 117)
의도
프로토타입 인스턴스를 이용하여 생성할 객체 종류를 상세히 설명하고 이 프로토타입을 복사하여 새로운 객체를 생성한다.
구조
[디자인 패턴] 편에서는 프로토타입이 복사한 결과를 반환하기 위해 clone 오퍼레이션을 구현함을 보였다. 이 다이어그램에서는 더 상세한 설명을 제공한다: 프로토타입 또는 클라이언트는 복사한 내용을 사용하기 전에 조정하고자 하는 경우가 있다ㅡ다시 말해 Clone의 상태로부터 새로운 인스턴스의 상태를 상속받고자 하지만 Clone의 상태와 조금 다른 상태를 원할 수도 있는 것이다.
이 다이어그램은 클라이언트가 어떻게 프로토타입을 복제하는지를 보여준다.
논의
프로토타입 패턴의 본질은 이렇다: 클래스의 견본적 인스턴스를 이용할 수 있도록 유지하고, 프로토타입의 상태를 기반으로 하는 클래스의 인스턴스가 필요할 때 클래스로 new를 전송하는 대신 견본적 인스턴스를 복제한다. 따라서 이 패턴에서는, 스스로를 복제함으로써 클래스의 새로운 인스턴스를 생성할 수 있는, 사전에 구축된 견본적 인스턴스가 있는 클래스가 주요 요소이다. 복제 과정에서 클래스는 인스턴스 생성의 책임을 지지 않는다; 견본적 인스턴스의 책임이다. 각 견본적 인스턴스는ㅡ프로토타입이라 불리는ㅡ잠재적으로 스스로의 복제 과정을 원하는대로 변경할 수 있으므로, 스스로 인스턴스를 생성할 때보다 초기 상태가 원하는 대로 조정된 새로운 인스턴스ㅡClone이라 불리는ㅡ를 생성할 수 있다. 그리고 나면 Clone은 프로토타입의 역할을 하고 스스로를 복제한다.
음악 편집기
[디자인 패턴] 편의 Motivation(동기) 부분에서는 패턴이 유용한 이유를 설명하고 있다. 음악 편집기에는 새로운 그래픽을 만들어내어 이를 편집 중인 문서에 추가할 수 있는 GraphicTool가 있다. GraphicTool은 사용자가 음계에 추가할 음표, 쉼표, 보표를 대해 전혀 알지 못하지만 수 많은 Graphic 객체들을 제한 없이 인스턴스화할 수 있다. GraphicTool은 모든 Graphic 객체에 공통된 속성을 이용하여 이를 달성하는데, 이것은 바로 스스로를 복사할 수 있는 능력을 의미한다. 사용자가 음악에 한 가지 요소를 추가하고자 할 경우 도구는ㅡ어떤 종류의 그래픽인지 모르지만ㅡ그 요소에 대한 프로토타입의 그래픽을 복사하여 복사내용을 문서에 추가한다. GraphicTool은 GraphicTool 이후에 정의된 서브클래스를 포함해 Graphic 서브클래스는 무엇이든 인스턴스화시킬 수 있다.
본 예제는 스몰토크에선 완벽하게 구현되지 않는다. 스몰토크 GraphicTool은 Graphic class>>new은 물론이고 Graphic에 대한 인스턴스 생성 프로토콜을 알 필요가 있다. 편집기는 문서에 그래픽을 추가하기 위해서 어떤 Graphic 서브클래스를 인스턴스화시킬 것인지 도구에게 말할 것이다. GraphicTool은 그 클래스를 선택해 인스턴스를 생성하고 문서에 추가할 것이다. 이러한 작업은 C++에선 불가능한데 그 이유는 변수가 클래스를 명시할 방법이 없기 때문이다. 그러나 스몰토크에서는 매우 간단하게 이루어진다.
스몰토크에서 프로토타입
[디자인 패턴] 편에서는, 메타클래스가 이미 프로토타입과 같은 역할을 하기 때문에 스몰토크나 Objective-C와 같은 메타클래스를 사용하는 언어에서는 프로토타입의 사용이 덜 유용하다고 언급한다 (항목 4, "서브클래스의 수를 줄인다," DP 120 참고). 메타클래스는 각 클래스가 그의 인스턴스 생성 과정을 각자 조절할 수 있도록 한다. 예를 들어, Point의 생성 메시지는 Point class>>new가 아니라 Point class>>x:y:가 된다 (생성자 메서드; Beck, 1997). 따라서 어떠한 클래스 쪽이든 프로토타입의 역할을 한다.
스몰토크에서 프로토타입 Prototype 패턴을 사용하는 이유에는 3가지가 있다:
- 객체의 초기 상태는 기존 객체와 다르다고만 알려져 있다. 이는 보일러 플레이트에 관한 설명에서 더 상세히 살펴보겠다.
- 초기 상태는 런타임 시 결정되며 컴파일 시간에 결정할 수 없다. 이것의 주요 발생 이유는 사용자가 주로 런타임 시 객체의 상태를 정의하기 때문일 것이다. 이번 경우는 사용자가 프로토타입을 구성하고 시스템은 다른 프로토타입을 생산하기 위해 그것을 복사한다. 알려진 스몰토크 사용예의 ThingLab 부분에서 관련 예제를 참조하라.
- 객체를 초기화하는 데에는 비용이 엄청나게 든다; 기존 인스턴스를 복사하는 편이 낫다. 이는 객체의 상태가 시간이 많이 소요되는 계산을 거쳐야 하거나 외부 데이터 소스로부터 상태를 얻어야 하는 경우 발생한다.
언어 비교 연구원들은 때때로 스몰토크의 메타클래스 모델을 간과하곤 한다. 그럼에도 불구하고 이 모델은 스몰토크로 프로그래밍하는 작업을 즐겁게 만드는 강력한 특징 중 하나이다. 스몰토크는 그 클래스가 추가 행위로 확장이 가능하고 다른 객체로 전달될 수도 있는 일급 객체라는 점에서 가장 많이 사용되는 객체지향 언어들 중에서도 특별한 언어라 할 수 있다. [디자인 패턴] 편에서는, 여러가지 경우에 이러한 특징이 스몰토크에서 많은 디자인 패턴의 구현을 변경한다고 언급하였는데, 이러한 패턴에는 추상 팩토리, 팩토리 메서드, 그리고 물론 프로토타입도 포함한다.
복사와 복제
[디자인 패턴]편에서는 copy로 구현되는 clone를 보여주고 있지만 여기서는 copy 이상을 수반하는 clone를 나타내고자 한다. 이유가 무얼까?
스몰토크에서 clone 메서드는 단순히 Object에 정의된 copy 메시지를 사용하기 위해 구현할 수도 있다. copy의 목적은 본래 객체를 정확하게 복사하기 위함이다. 하지만 프로토타입이 스스로를 복제할 경우 Clone의 상태는 프로토타입의 상태와 비슷하긴 하나 완전히 동일하진 않다. Clone은 두 지점에서 상태를 조정할 수 있다:
- 복제 도중: 프로토타입은 조정을 위해 initializeClone을 사용하며 이 때는 아마도 프로토타입의 상태를 무시할 것이다.
- 복제 완료 후: 클라이언트는 Clone 상태를 명시적으로 나타내기 위해 adjustClone: 와 같은 프로토타입 메서드를 사용한다.
initializeClone이 하는 일은 클라이언트가 통제할 수 없지만 adjustClone: 의 사용여부나 어떤 파라미터로 전달하는지는 통제를 할 수 있다는 점에서 initializeClone과 adjustClone: 는 다르다. 따라서 복사는 프로토타입의 정확한 복제인 Clone을 만든다. initializeClone을 이용한 copy는 프로토타입이 Clone의 상태를 조정하도록 해준다. adjustClone: 을 이용한 copy는 클라이언트가 Clone의 상태를 조정하도록 해준다.
보일러 플레이트 객체
때때로 사용자는 복합 객체의 전체 구성을 명시하지 않고 표준 구성으로부터 어떻게 다른지만 명시하고 싶을 때가 있다. 보험정책을 정의하는 시스템을 설계 중이라고 치자. 보험 대리점은 스크래치로부터 새로운 정책을 빌딩하기보다는 표준 정책부터 시작해 그로부터 자체적으로 조정한 정책을 도출할 것이다. 여기서 표준 정책을 템플릿 또는 보일러 플레이트(boilerplate)라 부른다. 이러한 종류의 편집은 문서가 서로 미미하게 다른 사업에서 발생한다.
스몰토크와 같은 객체지향 언어에서 이러한 종류의 프로세스를 모델링할 경우 보일러 플레이트로부터 개인화된 문서를 도출하는 두 가지 기본 전략이 있다.
- 프로토타입(Prototype): 원본 문서를 복사하여 복사본을 편집한다.
- 데코레이터(decorator): 원본 문서와 새 문서 간 다른 내용만 저장한다.
두 접근법의 비교는 예제 코드 단락에서 소개하고 있다.
복사 이해하기
스몰토크에서 Prototype 패턴을 사용하는 방법을 이해하려면 먼저 copy 메서드가 어떻게 작용하는지를 이해해야 한다. 세 가지 주요 스몰토크 구현 모두ㅡ비주얼웍스, 비주얼 스몰토크, IBM 스몰토크ㅡ동일한 복사 메서드를 정의한다:
- Object>>copyㅡ이 메서드는 "수신자와 똑같은" 또 다른 인스턴스를 반환한다 (Goldberg & Robson, 1989, p.97).
세 가지 방언(dialect) 모두 copy는 다음과 관련해 정의된다:
- Object>>shallowCopyㅡ이 메서드는 동일한 객체는 수신자의 인스턴스 변수를 가리키는 복사의 인스턴스 변수를 이용해 수신자의 복사결과를 반환한다.
비주얼 스몰토크와 IBM 스몰토크에는 한 가지 추가 메서드가 더 있다:
- Object>>deepCopyㅡ수신자의 인스턴스 변수 복사를 가리키는 복사의 인스턴스 변수를 이용해 수신자의 복사결과를 반환한다. deepCopy는 copy보다 한 단계 더 나아간다.
비주얼웍스에서는 copy의 구현에 deepCopy 대신 postCopy를 추가한다:
Object>>copy
^self shallowCopy postCopy
- Object>>postCopyㅡ이 메서드는 복사가 깊이 이루어지도록 copy를 확장시킨다. 각 클래스는 필요한 만큼 깊이 가도록 postCopy를 확장시킬 수 있다. 이런 방식으로 비주얼웍스에서 copy는 템플릿 메서드(355)로서 구현된다.
얕은 복사와 깊은 복사
Shallow Copy versus Deep Copy
여기서는 얖은 복사와 깊은 복사의 차이를 예제로 나타내고자 한다. 다음 코드를 이용해 3개의 OrderedCollection를 구축할 수 있다:
| original shallow deep |
original := OrderedCollection new.
original
add: 'abc';
add: 'def'.
shallow := original shallowCopy.
deep := original deepCopy
이 코드로 구축된 객체를 살펴보면 'abc' 와 'def' 스트링을 포함한 동일한 OrderedCollections을 볼 수 있다. 하지만 이 모음에 포함된 객체는 서로 다른 정체성을 가진다. 이 코드는 얕은 복사의 구조가 깊은 복사의 구조와 다름을 보여준다.
(original at: 1) == (shallow at: 1) "Returns true"
(original at: 1) == (deep at: 1) "Returns false"
그 원인은 shallowCopy의 경우 인스턴스 변수가 아니라 메시지의 수신자만 복사하는 반면 deepCopy는 둘 다 복사하기 때문이다. 어떤 경우든 반환된 객체는 수신자와 같지 않은 새로운 객체이다. 그러나 객체의 인스턴스 변수에서는 그렇지 않다. 다음 다이어그램이 차이를 보여준다.
도메인 객체 복사하기
당신이 어떤 목적을 가지든 shallowCopy의 사용으로 충분하며, 특히 객체가 String이나 Integer과 같은 간단한 객체만 포함한 경우 더 그러하다. 당신은 주로 이 객체를 수정하기보다는 복사에서 교체한다. 그러나 다음 클래스들은 shallowCopy의 한계를 보여준다:
Object subclass: #Person
instanceVariableNames: 'name address dateOfBirth'
classVariableNames: ''
poolDictionaries: ''
Object subclass: #Address
instanceVariableNames: 'street city state zipCode'
classVariableNames: ''
poolDictionaries: ''
Person의 인스턴스가 address 인스턴스 변수 내에 위치한 Address의 인스턴스를 포함하고 있다고 가정하자. shallowCopy를 Person에게 전송할 경우 원본과 동일한 name, address, dateOfBirth를 가리키는 인스턴스를 생성한다. name과 dateOfBirth 객체를 copy에서 교체할 경우 원본은 영향을 받지 않는다. 하지만 Address를 변경하려 할 경우 복사 뿐만 아니라 원본의 주소도 변경된다.
이 문제가 바로 deepCopy 메시지를 만들게 된 동기가 된다. deepCopy를 Person의 동일한 인스턴스에 전송할 경우 완전히 새로운 Address 또한 포함하는 Person 객체를 받을 것이다. 그리고 다른 인스턴스 변수마다 새로운 버전을 가질 것이다ㅡ이번 사례에서는 name과 dateOfBirth에 대해 각각 새로운 String과 Date 인스턴스를 가질 것이다.
하지만 deepCopyㅡ벤더(vendor)에 의해 구현된ㅡ로는 충분치 않다. Person 클래스에 dependents라는 또 다른 인스턴스 변수를 포함하고, 이 변수가 Person의 OrderedCollection을 포함한다고 가정하자. 이런 경우 deepCopy는 dependents 변수에ㅡ동일한 Person 인스턴스의 OrderedCollection을 원본으로 하는ㅡ새로운 OrderedCollection을 포함하는 새로운 Person을 반환한다. 사람들은 deepDeepCopy와 같은 새 메서드를 포함시킴으로써 deepCoppy의 한계점을 극복하고자 노력했지만 곧 실패하였다.
LaLonde와 Pugh(1994b)는 깊은 복사 문제에 대해 대부분 인스턴스에서 작동하는 일반적 해결책을 제시하였다. 이 해법은 veryDeepCopy라는 새로운 메서드를 소개하고 있는데 이는 deepCopy의 스몰토크-80 기존 구현의 순환 참조 문제를 해결한다. 그들의 접근법은 모든 방언에서 효과가 있다.
deepCopy의 또 다른 대안법으로, 각 스몰토크가 제공하는 바이너리 객체 저장기능(binary object storage facility)을 사용하는 방법이 있다. 비주얼 스몰토크는 ObjectFiler라 불리는 추가기능을 포함하고 있는데, 이것은 디스크 파일에 객체를 기록하는 성능을 가진다. 비주얼웍스에도 BOSS(Binary Objects Streaming Service)라는 이름으로 이와 거의 동일한 기능을 포함하고 있으며, IBM 스몰토크는 ENVY/Swapper라 불리는 추가기능을 제공한다. 각각의 기능은 순환참조를 정확히 처리하며 deepCopy의 위치에서 사용할 수 있다. 단순히 스트림에서 객체를 구성한 후에 완전히 독립적인 객체를 복사하기 위해 다시 읽어올 수 있다.
구현
[디자인 패턴] 편에서 제기된 구현 문제를 스몰토크에 적용시킨다:
- 프로토타입 관리자 사용하기. 클래스에 프로토타입 수가 정해져 있지 않는 경우가 있다. 클래스가 얼마나 많은 프로토타입을 가질 것인지 모르는 경우 프로토타입의 수가 얼마가 되든 관리를 준비해야만 한다. 예를 들어, 예제 코드 단락에 소개된 Policy 클래스는 런타입 시 소개된 Policy 프로토타입을 처리할 수 있어야 한다.
[디자인 패턴] 편에서는 이러한 프로토타입을 관리하기 위해 구분된 프로토타입 관리자 객체를 소개한다. 객체가 클래스의 인스턴스를 관리하는 다른 패턴들과 마찬가지로 스몰토크에서 그 객체는 주로 클래스 자체이다. 즉 Policy 클래스 스스로 Policy의 인스턴스들을 관리할 수 있는 것이다. 이 기법은 Flyweight (189)와 Singleton (91)에서 상세히 논하고자 한다.
- 복제 오퍼레이션 구현하기. [디자인 패턴] 편에서는 스몰토크에서 때로는 clone 메시지가 copy에 불과하다고 설명하고 있다. 그러나 스몰토크 구조 다이어그램에서 보이는 바와 같이 복제 과정에는 그 이상이 수반된다; 간단한 copy 이후 Clone의 상태를 조정하기 위해 initializeClone을 포함시킬 수도 있다. 클라이언트는 프로토타입을 복제하기 위해 단순히 copy를 전송할 수도 있지만 클래스가 정말로 프로토타입 클래스라면 clone을 구현해야 하며, clone을 어떻게 구현할 것인지에 대한 결정을 내려야 한다. 클라이언트가 프로토타입을 복제하고 싶을 때는 clone을, 단순히 복사하고 싶을 때는 copy를 이용하도록 해준다.
- 복제 초기화하기. [디자인 패턴] 편에서는 초기화 메서드와 같은 타입을 이용할 것을 제시하면서 프로그래머들이 복제 과정을 스스로 조정할 수 있도록 한다. 이러한 충고는 스몰토크에는 해당하지 않는다. 관례적으로 초기화 메시지는 인스턴스 생성의 일부로만 사용되어야 하는 특별한 메시지이다. 게다가 initializeWithData: (그다지 좋은 메서드 이름은 아니다)와 같은 이름으로 메서드를 부르기 위해서는 클라이언트가 파라미터를 제공할 필요가 있다. 이것이 바로 adjustClone: 또는 이와 같은 메서드의 목적이다. 이러한 메서드는 프로토타입이 실제로 스스로를 복제한 이후 클라이언트가 복제 과정을 알맞게 조정(customize)하도록 허용한다.
예제 코드
다시 한 번 앞에서 소개한 보험의 예제를 고려해보자. 수 많은 조직과 보험 회사 간 계약을 나타내는 Policy가 주요 관심이 되는 클래스이다. 보험 대리점은 3개의 기본 템플릿 중 하나에서 시작된다: 개인용, 중소기업용 (구성원이 100명 미만), 대기업용 3가지 템플릿이 있다. 이는 주로 보험 적용 범위에 따라 달라진다.
보험 대리점은 3가지 템플릿 정책 중 하나를 선택하여 다양한 방식으로 변경할 것이다. 먼저 적용되는 조직 이름이나 보험 관리자 이름과 같이 누락된 정보를 기입할 것이다. 그리고 나서 이 조직에 대한 정책을 쓰는 실제적 사업을 시작할 것이다. 우리 문제 영역에서는 보험에서 부담하는 의료절차를 재정의하는데 이른다. 이런 방식으로 그는 기존 Policy를 바탕으로 한 새로운 Policy을 정의하면서도 이는 자체적으로 권리를 소유한 유일한 객체가 될 것이다.
비주얼웍스에서 보험 정책 프로토타입을 구현하려면 몇 가지 인스턴스 변수를 이용해 간단한 정책 정의부터 시작한다.
Object subclass: #Policy
instanceVariableNames:
'policyNumber coverageStartDate lengthOfCoverage'
classVariableNames: ''
poolDictionaries: ''
category: 'Insurance'
프로토타입의 구현에서는 적절히 깊은 복사 메서드가 핵심이다. Policy이 포함하는 각 객체는 간단하면서 옅은 객체이다. 보일러 플레이트 정책으로부터 새로운 정책을 얻기 위해 클라이언트가 다음 메시지를 전송한다:
Policy>>derivedPolicy
"Return a new Policy derived from this Policy."
^self copy
이 간단한 예제에서 우리는 postCopy 메서드를 정의할 필요가 없다. Policy에서 간단한 객체를 변경하기보다는 교체하는 것이 낫기 때문에 copy로 충분하다.
이제 예제를 좀 더 복잡하게 만들어보자. Policy가 그것이 보장하는 조직을 알아야만 보험 회사는 보상청구에 대한 정책 번호를 조직의 번호와 일치시킬 수 있다. 보험업체 또한 보험청구서를 제출한 사람이 정책에서 보장되는지 확인할 수 있다. 이제 클래스는 다음과 같은 모습일 것이다:
Object subclass: #Policy
instanceVariableNames: 'policyNumber coverageStartDate
lengthOfCoverage organization'
...
Object subclass: #CoveredOrganization
instanceVariableNames: 'name address planAdministrator'
classVariableNames: ''
poolDictionaries: ''
category: 'Insurance'
추가된 복잡성은 Poilcy를 복사하는 방식을 변경할 필요성을 야기한다. 이는 앞서 설명한 바와 같이 비주얼웍스의 postCopy 메서드를 오버라이드함으로써 실행할 수 있다.
Policy>>postCopy
"Make an independent copy of this Policy's attributes"
super postCopy.
organization := organization copy
이러한 postCopy 버전은 교체 대신 수정될 다른 객체를 포함한 객체를 어떻게 복제하는지를 보여준다. 당신은 새 컨테이너에 기존 객체의 복사객체를 전송해야 한다.
이 접근법은 단순한 객체의 경우 효과적이지만 collection이나 다른 중첩(nested) 객체와 같이 좀 더 정교한 구현을 필요로 하는 경우는 적절치 않다. 예를 들어, 건강보험정책은 보장되는 의료절차부터 보험청구를 보상하는 방식까지 매핑을 포함한다. 우리 애플리케이션에서 매핑은 절차 코드부터 (의료절차를 나타내는 표준 숫자 코드) 절차를 어떻게 보상할 것인지 명시하는 ProcedureRule의 인스턴스까지를 그리고 있는 Dictionary로 나타나고 있다. (절차 코드 123.4는 맹장 수술에 해당할 수 있다.) 특정 정책은 맹장 수술 시 병원에서 부과한 금액의 80%로 보상할 것이라고 말할 수 있다. 우리 Policy 객체 각각은 코드부터 원칙 매핑까지를 설명하는 Dictionary를 포함할 것이다.
이제 Policy 클래스는 다음과 같을 것이다:
Object subclass: #Policy
instancevariableNames: 'policyNumber coverageStartDate
lengthOfCoverage organization procedureRules'
...
이것은 postCopy 메서드에 더 많은 변화를 촉구한다. 첫째, 우리는 절차 원칙을 지키기 위해 새로운 Dictionary를 생성해야 한다. 그리고 난 후 Dictionary에 위치시키기 전에 각 규칙을 copy한다.
Policy>>postCopy
"Make an independent copy of this Policy's attributes"
| newDictionary |
newDictionary := Dictionary new.
procedureRules keysAndValuesDo:
[:key :value | newDictionary at: key put: value copy].
procedureRules := newDictionary.
organization:= organization copy
이 접근법은 복사의 다른 측면들을 이해하고 나면 구현하기가 쉽기 때문에 유익하다. 그러나 단점 또한 존재한다. 특히 복잡성이 증가한다는 점이 증가가 치명적인 급소이다. 설계가 진화하면서 이 메서드는 반복된 수정을 필요로 하는 경향이 크다. 수정을 빼먹을 경우 감지하기 힘들고 예상치 못한 미묘한 실패가 발생할 수 있다. 게다가 엄청난 공간을 잡아 먹는다. postCopy는 새 객체에 필요하지 않을 수도 있는 기존 객체의 인스턴스 변수를 복사한다. 이보다 더 중요한 것은 이 접근법이 변화의 이력을 명시적으로 제공하지 않는다는 점이다. 변수별 비교를 하지 않고는 원본과 복사의 차이를 추적하기가 쉽지 않다. 많은 애플리케이션에서는 이러한 이력을 필요로 하기 때문에 모든 경우에 적절한 접근법은 아니라고 할 수 있겠다.
Decorator를 이용한 구현
일반적으로, 템플릿을 나타내는 클래스가 단순하여 그다지 깊이 중첩되지 않을 때마다 보일러 플레이트 객체에 대해 프로토타입 접근법을 사용하라. 좀 더 복잡한 경우, Decorator (161) 접근법의 사용을 고려해야 한다. 이것은 이전 객체와 복사한 객체 차이를 (또는 델타) 기록하는 클래스를 생성기 위함이다.
Decorator는 다른 객체를 캡슐화하는 decorator 객체를 어떻게 생성하는지 혹은 그 상태나 행위를 어떻게 효과적으로 증가시키는지, 혹은 둘 다 실행하는 방법을 설명한다. 다음 설계를 고려해보자.
Decorator는 Policy의 새로운 서브클래스, DecoratingPolicy를 소개하여 boiletplate 정책을 가리키는 새로운 basePolicy 인스턴스 변수를 추가한다. 보일러 플레이트에서 파생된 모든 정책은 Policy보다는 DecoratingPolicy의 인스턴스일 것이다.
이것이 어떻게 작용하는지 이해하려면 Policy와 DecoratingPolicy에서 모두 주요 메서드가 되는 ruleAt:의 구현을 살펴보도록 한다. ruleAt: 이러한 Policy 원본 클래스에서 어떻게 구현되는지 알아보자:
Policy>>ruleAt: aProcedureCode
^self procedureRules
at: aProcedureCode
ifAbsent: [self defaultRule]
여기서 ruleAt: 은 aProcedureCode의 키를 가지는 원칙을 반환하거나 그 키에 원칙이 없을 시에는 기본 원칙을 반환한다. setRuleFor:rule: 메서드도 이와 비슷하여 단순한 편이다:
Policy>>setRuleFor: aProcedureCode rule: aProcedureRule
self procedureRules
at: aProcedureCode
put: aProcedureRule
이제 decoratingPolicy 클래스에서의 ruleAt: 의 구현을 비교하자:
DecoratingPolicy>>ruleAt: aProcedureCode
^self
at: aProcedureCode
ifAbsent: [self basePolicy ruleAt: aProcedureCode]
DecoratingPolicy는 절차 코드에 저장된 원칙에 관련된 고유의 사전을 먼저 살펴본다는 사실을 명심하자. 이 메서드가 만약 일치하는 원칙을 찾을 경우 그 결과를 반환한다. 일치하는 원칙을 찾지 못한 경우 basPolicy로 검색을 계속된다. DecoratingPolicy가 setRuleFor:rule:을 오버라이드하지 않는 이유는 상속된 구현이 올바르기 때문이다.
또 다른 예제인 lengthOfCoverage: 메서드를 살펴보자:
Policy>>lengthOfCoverage
"Return the length of time, in years, this Policy
is in effect."
^lengthOfCoverage
Decorator는 메서드를 약간 다르게 구현한다:
DecoratingPolicy>>lengthOfCoverage
"If the value has not yet been set, return the
base policy's value."
^lengthOfCoverage isNil
ifTrue: [self basePolicy lengthOfCoverage]
ifFalse: [lengthOfCoverage]
Decorator는 이와 같은 다수의 취득 메서드(getter method)를 구현하여 새로은 정책에 값이 설정되지 않았을 시 보일러 플레이트에 있는 값을 반환한다.
보일러 플레이트 정책으로부터 새 정책을 얻고자 하는 경우 클라이언트는 다음 메시지를 전송한다:
Policy>>derivedPolicy
"Return a new Policy derived from this one."
^DecoratingPolicy new basePolicy: self
이러한 구현에는 몇 가지 이점이 있다. 첫째, 복사가 필요하지 않으므로 저장비용을 감소시킨다. 둘째, 데코레이터 자체가 템플릿 정책의 변경 이력을 구성한다. 설계의 부작용으로는 기반 정책(base policy)에 일어난 변경은 그로부터 파생된 모든 정책에 적용된다는 점이다.
이러한 구현에는 단점도 몇 가지가 있다. Decorator 서브클래스에 관리가 쉽지 않은 복잡한 코딩을 필요로 한다는 점이다. 기반을 변경하거나 좀 더 복잡한 객체에서 최고의 접근법이 될 것이다.
알려진 스몰토크 사용예
비주얼웍스 TextAttributes
비주얼웍스 텍스트 디스플레이 클래스는 간단하면서 프로토타입 패턴을 쉽게 이해할 수 있다. 클래스 TextAttributes의 인스턴스는 폰트 매핑에 대한 CharacterAttributes, 라인 그리드, 기준선, 배열, 탭의 위치, 행 들여 쓰기를 포함해 텍스트 일부의 디스플레이에 영향을 미치는 정보를 관리한다. 때때로 사용자는 이 인스턴스 변수들 중 하나 또는 일부를 변경하기 위해 TextAttributes를 변경하고 싶어 하지만 시스템에 제공된 기준선 TextAttributes 인스턴스부터 시작할 필요가 있다.
이러한 이유로 TextAttributes 메서드 styleNamed: 는 copy 메서드를 이용하여 참조 인스턴스에서 파생된 새 인스턴스를 만든다:
TextAttributes class>>styleNamed: aSymbol
"Answer the style named aSymbol from the text style dictionary."
^(TextStyles at: aSymbol) copy
비주얼웍스 데이터베이스 프레임워크
비주얼웍스 데이터베이스 프레임워크는 프로토타입의 흔하지 않은 사용을 포함한다. 데이터베이스 프레임워크는 관계적 데이터베이스 쿼리(query)로부터 읽어온 데이터 행을 객체로 해석한다. ExternalDatabaseSession 내에 bindOutput 인스턴스 변수의 값은 쿼리에 의해 반환된 결과 클래스를 결정한다. 변수의 값은 목표 클래스의 빈(empty) 인스턴스이다. 해당 세션은 이 인스턴스를 프로토타입으로 이용하고 새로운 데이터 행마다 그 인스턴스를 복사한다.
ThingLab
앞서 살펴본 두 가지 프로토타입 패턴의 사용 예에서는 클래스에 프로토타입이 있다는 사실을 클래스가 인지하고 있었다. 즉, 프로토타입 인스턴스에 대해, 인스턴스화할 클래스를 정확하게 부호로 쓰는 동안 이를 알고 있었음을 의미한다. 이 정보를 알고 있기 때문에 팩토리 메서드와 같은 경쟁 패턴을 대신해 사용할 수도 있겠다.
프로토타입의 더 강력한 사용은 아마도 프로그램 실행 도중에 원형 객체를 생성하여 추후 사용을 위해 저장하고자 할 때 발생할 것이다. 제약조건기반(constraint-oriented)의 그래픽 편집기인 ThingLab에서 이의 전형적인 예를 들 수 있다 (Borning, 1981, 1986). ThingLab은 사용자가 그래픽 객체를 구성하고 이에 지리적 제약을 적용하도록 해준다. 사용자가 새로운 그래픽 객체를 (또는 "부품" 이나 "것") 정의하고 나면 다른 부품을 구축하는데 사용할 수 있다. ThingLab에서는 새로운 것이 생성되면 시스템이 계획에 따라 새로운 클래스를 정의하고 이를 그 클래스에 견본적 인스턴스로 저장한다. 사용자가 그것을 재사용하고자 할 경우 복제를 검색하기 위해 prototype 메시지가 관련 클래스로 전송된다. 프로토타입 패턴은 사용자가 (프로그래머가 아닌) 런타임 시 임의로 복합객체를 정의하도록 해준다.
관련 패턴
Decorator (161) 패턴이 프로토타입의 대안으로 사용되는 예제는 이미 살펴보았다. 여기서는 GoF 패턴이 아닌 다른 관련 디자인 패턴을 언급하고자 한다.
프로토타입은 Type Object을 대신하거나 이와 함께 사용할 수 있다 (Johnson & Woolf, 1998). 이 두 패턴은 컴파일 시간에 알려지지 않은 객체를 인스턴스화하는 문제에 대안적 해결책을 제공한다. Type Object는 compilation 없이 런타임 시 새로운 "클래스"를 생성시킬 수 있는 반면 프로토타입은 스스로 복제가 가능한 원형 객체를 사용한다. 두 패턴 모두 사용자가 런타임 시 새 객체를 설명한 다음 그 설명을 바탕으로 객체를 생성할 수 있게 해준다. 프로토타입의 경우 "설명"이란 프로토타입 객체 자신 이상은 되지 않는다; Type Object의 경우 "설명"은 생성될 객체의 구조를 설명하기 위해 런타임 시 구축된 다른 객체들의 집합체를 의미한다.
두 패턴은 인스턴스화 작업을 처리하기 위해 같이 작동할 수 있다. 클래스는 initialize를 구현함으로써 그 인스턴스의 초기화를 원하는대로 조정한다. Type Object의 경우, 서로 다른 타입의 객체들은 동일한 클래스의 인스턴트들이므로 initialize를 타입별로 원하는대로 조정할 수가 없다. 그러나 각 타입은 다른 프로토타입과 상관 없이 상태가 설정되는 고유의 견본적 인스턴스를 가진다. 그리고 타입 객체가 그 타입에 초기화되는 인스턴스를 생성하고자 하는 경우, 그 타입 객체는 기본적(custom) 초기 상태로 새 인스턴스를 생성하기 위해 자신의 견본적 인스턴스를 복제한다.