DesignPatternSmalltalkCompanion:TemplateMethod

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

TEMPLATE METHOD(DP 325)

의도

오퍼레이션에서는 알고리즘의 처리 과정만 정의하고 각 단계에서 수행할 구체적 처리는 서브클래스로 미룬다. Template Method 패턴은 알고리즘의 처리 과정을 변경하지 않고 알고리즘 각 단계의 처리를 서브클래스에서 재정의할 수 있게 한다.

구조

Dpsc chapter05 TempleteMethod 01.png

논의

Template Method 패턴은 Design Patter Pop Chart(디자인 패턴의 팝 차트) 중 아마 1위에 해당할 것이다. 그 구조와 사용은 객체지향 프로그래밍의 진수를 이른다. Template Method는 상속에 대한 객체지향의 기본 개념에 달려 있다. 이 패턴은 기존 클래스에 비해 약간 다른 클래스의 정의를 필요로 한다. 이는 상속된 메서드를 오버라이드함으로써 새 행위를 제공하는 클래스의 기능에 의지한다.

Template Method 패턴에 있어 핵심은 동일한 수신자에 여러 개의 좁은 범위의 메시지와 관련해 넓은 범위의 메시지를 구현하는 것이다. 넓은 범위의 메서드인 템플릿 메서드(template method)는 슈퍼클래스에 구현되고, 좁은 범위의 메서드인 프리미티브 메서드(primitive method)는 그 클래스 또는 그것의 서브클래스에 구현된다. 템플릿 메서드는 일반 알고리즘을 구현하고, 프리미티브 메서드는 세부 사항을 명시한다. 서브클래스가 알고리즘을 사용하되 세부 내용을 변경하고자 할 경우 전체 템플릿 메서드를 오버라이드할 필요 없이 구체적 프리미티브 메서드를 오버라이드하면 된다.

상속을 통한 재사용

상속을 통해 공유되는 코드와 속성은 객체지향 프로그래밍에서 가장 해로운 기법들 중 하나이다. 믿을 수 없을 만큼 구현이 쉽지만 잘 사용하기란 힘들다. 일부 권위자들은 혼란스럽고 불필요하다는 이유로 상속의 구현을 묵살한다. 첫 OOPSLA 시기에 여러 논문들은 상속과 관련된 잠재적 문제에 대해 다뤘다 (Snyder, 1986). 객체지향 언어 SELF는 심지어 클래스 상속을 지원조차 않는다; 대신 객체 위임만을 사용한다. [디자인 패턴] 편에서는 객체 상속보다 객체 구성을 선호한다 (DP 18-21). 많은 패턴들이 객체 구성을 사용하기 위해 클래스 상속에 의존하는 코드를 변형한다; State, Strategy, Decorator, Bridge, Abstract Factory 패턴이 이에 속한다.

그럼에도 불구하고 상속은 스몰토크를 포함한 대부분 객체지향 언어에서 가장 중요한 기본 특징 중 하나이다. 훌륭한 클래스 계층구조는 추상화, 디자인, 구현의 재사용을 허용한다. 상속으로 인해 이러한 계층구조들은 이해, 유지, 확장이 쉽고, 그들의 클래스를 재사용할 가능성이 훨씬 더 크다. 우리는 처음부터 다시 시작하기보다는 기존 클래스를 특수화시킴으로써 기존 클래스와 약간 다른 새로운 클래스를 정의할 수 있다. [디자인 패턴] 는 상속 대신 구성을 사용할 때가 더 낫다는 사실을 증명하는 예제가 불충분함에도 불구하고 여전히 인터페이스의 정의와 구현에 상속을 선호한다 (DP 14-18). 대부분 패턴들은 최소 하나의 클래스 계층구조에 최상위 추상 클래스가 있고 이는 인터페이스 및 그것을 구현하는 여러 개의 구체적 서브클래스를 정의하는 특성을 가진다: Adapter 패턴의 Target 계층구조, Composite 패턴의 Component 계층구조, State 패턴의 State 계층구조 등등.

Template Method 패턴은 상속의 적당한 사용을 장려한다. 패턴은 공통된 행위를 공유하는 서로 관련된 클래스들의 리팩토링에 유용하기도 하다 (DP 326). 재사용을 위한 팩토링에서 핵심은 변하지 않는 것과 변하는 것을 분리하는 것인데, Template Method 패턴은 재사용 가능한 코드를 서브클래스로 변경될 수 있는 세부 사항과 슈퍼클래스로 분해시킴으로써 이를 달성한다.

Template Method는 어떻게 작용하는가

추상 클래스와 Template Method 패턴은 거의 서로 관련되어 있다. 추상 클래스는 그 계층구조에 클래스의 행위를 정의하는 슈퍼클래스이면서 구현의 세부 내용은 서브클래스로 미룬다. 슈퍼클래스는 그것이 정의하는 모든 행위를 구현하지는 않으므로 추상적 클래스이다. 각 서브클래스는 슈퍼클래스로부터 행위를 상속받거나 스스로 행위를 구현함으로써 모든 행위를 구현하기 때문에 구체적 클래스이다.

Template Method는 클래스 수준 대신 메서드 수준에서 동일한 기법을 사용한다. Template Method는 일반적으로 다음과 같은 모습일 것이다:

AbstractClass>>algorithmA
	self
		algorithmAStep1;
		algorithmAStep2;
		algorithmAStep3

클래스는 자신이 실행할 수 있는 행위의 unit마다 algorithmA와 같은 메서드를 정의한다. 메서드는 그다지 많은 일을 하지 않는다; 대신 실행 단계를 열거하는 템플릿을 제공하고 각 단계를 수신자에게 (self) 위임한다. 이러한 방식으로 Template Method는 어떤 단계를 수행할지 결정하게 되지만 수신자는 각 단계를 어떻게 실행할 것인지 결정한다. 이를 통해 실행해야 할 단계와 실행하는 방법을 분리할 수 있다.

Template Method 패턴의 미는 추상 클래스가 각 단계가 어떻게 수행되는지 정의할 뿐 아니라 각 구체적 서브클래스는 이러한 결정을 오버라이드할 기회를 가진다는 점이다. 구체적 클래스가 일반적으로 algorithmAStep2가 아닌 algorithmA와 같이 작용한다면 algorithmA또는 슈퍼클래스의 구현 단계 1과 3을 변경하지 않고 algorithmAStep2을 하위구현(오버라이드)할 수 있다. 이러한 과정은 서브클래스가 나머지 상속 과정에 필요한 부분만 오버라이드할 수 있도록 허용하여 각 서브클래스가 스스로 구현해야만 하는 코드의 양을 최소화시킬 뿐 아니라 서브클래스와 슈퍼클래스의 차이를 강조할 수 있다.

메서드의 타입

추상 클래스가 구현하는 메서드 타입에는 4가지, 템플릿, 구체적, 추상적, 훅 메서드가 있다 (DP 327-328). 뒤에서 3가지 타입은 프리미티브 메서드에 속한다:

:#. 템플릿. 템플릿 메서드는 추상 클래스에 존재하며 구체적, 추상적, 그리고/또는 훅 메서드를 하나의 알고리즘에 통합한다. 서브클래스는 불러오는 메서드를 오버라이드할 수 있음에도 불구하고 주로 변경되지 않은 채로 상속한다. 이것이 템플릿 메서드 패턴의 핵심이다. 
:#. 구체적. 구체적 메서드는 추상 클래스가 정의하고 서브클래스가 오버라이드하지 않는 메서드이다. 
:#. 추상. 추상 메서드는 추상 클래스에 존재하며, 메시지를 "선언" 하지만 그 구현은 서브클래스로 미룬다. 각 서브클래스는 자신이 상속하는 추상 메서드를 오버라이드 해야만 한다. ([디자인 패턴] 편에서는 이를 "프리미티브 오퍼레이션" 이라 부른다. 이는 스몰토크에서 사용되는 "primitive" 용어와 완전히 다른 개념이므로 혼동되지 않도록 주의한다.)
:#. Hook. Hook 메서드는 추상 클래스 내에 존재하며, 메시지의 행위를 선언하고 그에 대한 기본 구현을 제공한다. 때로는 기본 구현이 아무 것도 하지 않을 때가 있다. 서브클래스는 그 기본 구현을 변경하기 위해 메서드를 오버라이드 할지도 모른다. 

[디자인 패턴] 편에서는 5번째 타입, 팩토리 메서드도 열거하고 있지만 이는 구체적 메서드나 훅 메서드의 특별한 사례일 뿐이다 (팩토리 메서드 (63) 패턴을 참조).

어떤 메서드가 구체적 메서드여야 하는지를 확인하는 것은 쉽다. 하지만 추상 메서드와 훅 메서드를 결정하는 것은 까다롭다. 여기서 스스로 질문해봐야 할 것은 이 메서드에 대한 타당한 기본 구현이 있는지 여부이다. 있을 경우 추상적 클래스는 훅 메서드로 구현되어야 한다. 기본 구현이 없을 경우 메서드는 추상적이어야 한다.

[디자인 패턴] (329)에 따르면 디자인의 한 가지 목표는 서브클래스가 오버라이드해야 하는 프리미티브 오퍼레이션의 수를 최소화하는 것이다. 프리미티브 오퍼레이션 수를 (추상 메서드) 감소시킬 수 있는 한 가지 방법으로는 이 메시지들을 대신 훅 메서드로 구현하는 방법이 있다. 추상적 메서드는 개발자로 하여금 서브클래스가 메서드를 오버라이드하도록 강요한다. 반대로 훅 메서드를 변경되지 않은 채 상속하길 선택할 수도 있다. 이 방식을 통해 훅 메서드는 추상적 메서드보다 상속을 (서브클래싱) 더 수월하게 만든다.

추상 메서드 대신 훅 메서드를 사용 시 발생할 수 있는 한 가지 문제는 메서드가 오버라이딩 가능성이 있다는 사실이 서브클래스 개발자에게 확실히 드러나지 않을 수도 있다는 점이다. 클래스나 메서드 문서화만이 이러한 수준의 정보를 제공한다. 서브클래스 개발자가 추상적 메서드의 오버라이드를 잊을 경우 그는 코드를 실행하여 subclassResponsibility 오류로부터 월백을 수신할 때 그가 한 실수를 발견할 것이다. 만일 그가 무심코 훅 메서드로부터 부적합한 행위를 상속했다면 그 코드는 실행이 될지는 몰라도 정확히 실행되리란 법은 없다.

"아무것도 하지 않는" HOOK

슈퍼클래스에 정의된 훅 메서드는 그 서브클래스에 대해 합당한 기본 오퍼레이션을 제공해야만 한다. 합당한 기본 오퍼레이션의 특별 사례로, 아무것도 하지 않고 self만 리턴하는 메서드의 구현을 들 수 있다. 이렇게 "아무것도 하지 않는" 메서드는 오퍼레이션의 발생 가능성이 있으나 모든 서브클래스가 그 오퍼레이션을 필요로 하지는 않는 경우에 유용하다. 이러한 메서드 종류의 두 가지 예로 비주얼웍스의 ApplicationModel>>preBuildWith: 와 postBuildWith: 메서드를 들 수 있다. 이 메서드들은 인터페이스 구축과정 도중에 두 개의 다른 시점에서 사용자 인터페이스 위젯의 행위를 변경할 필요가 있는 ApplicationModel 서브클래스들에 대해 훅 메서드를 제공한다. 그러나 ApplicationModel 자체는 두 단계에서 아무것도 하지 않으며, 어떤 일도 하지 말하는 메시지를 구현할 뿐이다. 이러한 기본적 구현은 대부분 서브클래스에서도 적절하다.

또 다른 예는 IBM 스몰토크에서 사용하는 Class>>initialize를 들 수 있다. 모든 클래스가 초기화를 필요로 하는 것이 아니기 때문에 기본 구현은 아무 일도 하지 않는 것이다. 이로 인해 어떠한 클래스의 초기화 메서드에서든 super initialize를 안전하게 사용하는 것이 가능하다. 하지만 비주얼웍스의 클래스 메서드에서는 이러한 일을 해서는 안 된다; 비주얼웍스에서 Object class>>initialize는 모든 종속자를 제거하는데 있어 부작용이 있다!

비주얼웍스와 비주얼스몰토크에 사용되는 Stream>>close도 이러한 훅 메서드의 예이다. 클라이언트가 사용 중인 스트림 유형을 클라이언트가 알지 못하도록 하는데 유용하며, 내부 집합체 또는 외부 객체에 위치한다 (socket이나 파일 처리와 같이). 따라서 close의 기본 구현은 아무 일도 하지 않는 것이다. 외부 스트림 서브클래스는 외부 자원을 삭제하기 위해 이 메서드를 재정의한다.

계층구조의 설계와 리팩토링

계층구조의 재사용을 위해 리팩토링을 하거나 처음으로 설계하는 경우, 행위를 계층구조의 상향으로 이동시키거나 상태를 하향으로 이동시키는 방법이 도움이 된다. Auer (1995)는 4개의 발견적 패턴에서 이 과정을 설명한다:

  1. . 상태가 아닌 행위에 따라 클래스를 정의하라. 먼저 구조를 무시하고 클래스를 구현하여 행위를 정의한다.
  2. . 추상적 상태로 행위를 구현하라. 상태 변수를 직접적으로 참조하는 대신 메시지를 통해 간접적으로 상태에 접근하는 데에 있어 상태 정보를 필요로 하는 행위를 구현한다.
  3. . 메시지 계층을 확인하라. 서브클래스에서 쉽게 오버라이드가 가능한 작은 커널 메서드 집합을 통해 클래스의 행위를 구현하라.
  4. . 상태 변수의 확인을 미루라. 추상적 상태 메시지는 커널 메서드가 되므로 상태 변수를 필요로 한다. 그러한 변수들을 슈퍼클래스에 선언하는 대신 서브클래스에서 선언하고, 커널 메서드의 구현을 서브클래스로 미룬다.

이러한 과정은 인터페이스와 구현부를 구분하는 계층구조로 발전시킨다. 슈퍼클래스는 계층구조의 행위를 추상적으로 정의하며, 하나 또는 그 이상의 서브클래스가 그 행위를 구현한다. 슈퍼클래스는 커널 메서드를 사용할 뿐더러 서브클래스로 하여금 필요 없는 상태를 상속하도록 강요하지 않기 때문에 슈퍼클래스의 서브클래싱이 쉽다.

이 과정은 템플릿 메서드와 프리미티브 메서드로 이어진다. 프리미티브 메서드는 Auer의 패턴에서 커널 메서드라 불린다. 템플릿 메서드는 커널 메서드를 사용하는 계층이다. 이러한 패턴들은 Template Method 패턴이 중첩 가능함을 보여준다; 각 메시지 계층은 자신만의 템플릿-프리미티브 관계를 가진다. 템플릿 메서드가 전송하는 프리미티브 메시지들 자체가 템플릿 메서드가 될 수 있으므로 다른 프리미티브 메시지를 전송할 수 있다.

비주얼웍스의 Collection 계층구조로부터 중첩된(nested) Template Method 패턴의 예를 아래에 소개하고자 한다:

Collection>>removeAll: aCollection
	"Remove each element of aCollection from the receiver.
	..."

	aCollection do: [:each | self remove: each].
	^aCollection

Collection>>remove: oldObject
	"Remove oldObject as one of the receiver's elements. ..."

	^self remove: oldObject ifAbsent: [self notFoundError]

Collection>>remove: oldObject ifAbsent: anExceptionBlock
	"Remove oldObject as one of the receiver's elements. ..."

	self subclassResponsibility

Collection이 removeAll: 을 수신할 때 프리미티브 메서드 remove: 로 위임하는 템플릿 메서드가 구현부가 된다. 하지만 remove: 의 구현부는 프리미티브 메서드가 아니다; remove:ifAbsent: 로 위임하는 또 다른 Template Method이다. 마지막으로 remove:ifAbsent: 는 실제로 프리미티브 메서드이자 자신의 서브클래스로 위임하는 추상적 메서드이다. iterative Template Method 구현의 예제는 알려진 스몰토크 사용예 단락의 WindowPolicy 예제에서 하나 더 소개하겠다.

무엇을 위임하는가

메서드가 잘 작성되었다면 모든 작업은 추상화와 같은 단계에서 실행되어야 한다. 즉, 그 단계는 "뭔가 일반적인 것을 하라; 뭔가 일반적인 것을 하라; 뭔가 매우 구체적인 것을 하라" 라는 형태여서는 안 된다는 뜻이다. 대신 구체적 코드가 다른 메서드에 속해 있어야 한다. 그러면 이 메서드의 모든 단계는 같은 수준의 일반성 또는 특수성에 위치한다.

특정 코드 세부 내용은 일반 알고리즘용으로는 너무 구체적으로 튀어나온다. 이러한 세부 내용에는 상수, 클래스, 구체적 알고리즘, 또는 이들 중 일부가 포함된다.

상수 위임하기. 간단한 템플릿 메서드 패턴이 명확히 드러나는 사례는 필요시마다 하드코딩 대신 상수의 "위임" 을 팩토링하는 데에서 관찰된다. 스몰토크에서는 주로 Pool 변수를 사용하거나 상수를 단순히 리턴하는 구분된 메서드를 정의함으로써 상수를 명시한다 (Constant Method, Beck, 1997). Pool dictionary에는 많은 결점이 있다. 가장 주요 쟁점은 언어와 클래스 라이브러리에서 지원이 불충분하고 재사용에 불리한 영향을 미친다는 점이다 (Ewing, 1994). 이러한 단점으로 인해 상수를 리턴하도록 메서드를 명시하는 편이 더 유연한 접근법이 되는데, 이는 서브클래스가 서로 다른 상수 값을 리턴하기 위해 메서드를 서로 다르게 구현할 수 있기 때문이다. 이렇게 하여 상수를 리턴하는 메서드는 이제 훅 메서드가 되므로 그 상수를 사용하는 메서드는 템플릿 메서드가 된다.

IBM 스몰토크의 EtBrowser에 위치하는 다양한 서브클래스에서 서로 다른 상수 값을 정의하는 예제를 볼 수 있다. 슈퍼클래스 EtBrowser는 nil을 리턴하기 위해 메서드 classDoubleClickSelector를 정의한다. 이는 브라우저의 클래스 리스트 중 더블클릭하여 선택할 때 아무런 일도 발생하지 않음을 의미한다. 서브클래스는 이 메서드를 재정의하여 메시지 선택기를 리턴한다. 이 메시지 선택기는 다른 목적으로 사용된다: 일부 클래스에서 더블 클릭은 "선택한 클래스를 탐색하기"를 의미한다: 또 일부 클래스에서는 "이 클래스에 루트된 클래스 계층구조를 확장 또는 축소시키기" 를 의미한다.

IBM 스몰토크에서는 OsObject 계층구조에 있는 메시지 fixedSize가 각 OsObject 서브클래스가 나타내는 구조의 크기를 (바이트) 리턴한다. 이러한 다형성의 사용으로 인해 copyToOsMemory와 같이 OsObject 에 정의된 Template Method가 실제 크기와 상관 없이 어떤 구조에서든 작동하도록 허용한다.

클래스 위임하기. 스몰토크 클래스는 상수에 대한 특별한 사례이다. Factory Method 패턴에서 스몰토크 메서드는 클래스를 리턴하며, 그 클래스는 이후에 인스턴스를 생성하는 데에 사용된다 (Factory Method (63) 참조). 대개는 당신이 Factory Method 패턴을 처음으로 사용하고 싶지 않다 하더라도 클래스 명을 자신의 메서드에 고립시키는 것은 후에 클래스 명이 변경되더라도 코드를 유지하기 수월하기 때문에 좋은 방법이다.

알고리즘의 하위부분 위임하기. 때로는 클래스마다 다양한 알고리즘의 특정 하위부분을 격리하고 싶을 때가 있다. 다시 말하지만 이는 Template Method 패턴에 숨겨진 주요 동기이다. 객체지향 디자인의 도전과제 중 일부는 알고리즘과 그 컴포넌트 서브태스크 간 관계를 찾아 시작하는 것이다ㅡ이러한 서브태스크는 상속과 위임이 부족하기 때문에 절차적 언어에서는 달성할 수 없다.

비주얼웍스에서 이러한 사용은 Controller 계층구조에서 찾아볼 수 있다. 각 Controller가 어떻게 마우스에 대한 제어력을 얻는지 정의하는 "제어 루프"의 개념은 비주얼웍스 윈도윙 시스템에서 기본이 되는 부분 중 하나이다. 아래 표시된 controlLoop란 이름의 템플릿 메서드는 다음 개념을 구현한다:

Controller>>controlLoop
	[self poll.
	self isControlActive]
		whileTrue: [self controlActivity]

controlActivity란 메시지는 Controller에 정의된 훅 메서드로서 컨트롤러의 다음 수준으로 제어력을 전달한다. Controller의 서브클래스는 이 메서드를 오버라이드하여 제어력을 수신 시 구체적 행동을 실행한다. Controller의 서브클래스에 해당하는 ControllerWithMenu가 이 메서드를 또 다른 템플릿 메서드로 오버라이드하는 점을 주목하는 것이 흥미롭다:

ControllerWithMenu>>controlActivity
	self sensor redButtonPressed & self viewHasCursor
		ifTrue: [^self redButtonActivity].
	self sensor yellowButtonPressed & self viewHasCursor
		ifTrue: [^self yellowButtonActivity].
	super controlActivity

ControllerWithMenu의 서브클래스는 "적색" 또는 "황색" 마우스 버튼을 누르면 구체적 행동을 실행하도록 redButtonActivity와 yellowButtonActivity 메시지를 각각 오버라이드한다.

리팩토링의 사례 연구

리팩토링의 사례 연구를 살펴보면 이러한 과정에서 템플릿 메서드가 어떻게 발생하는지, 위에서 논한 문제들이 어떻게 벌어지는지를 명확하게 알 수 있도록 도와준다. 스몰토크에서 가장 잘 알려지고 설명하는 리팩토링의 사용예로는 Objectworks\Smalltalk 2.5와 Objectworks\Smalltalk 4.0 사이에 Smalltalk-80 View 클래스의 리팩토링을 들 수 있다.

Johnson과 Foote (1988), 그리고 Rubin과 Liebs (1992)가 설명하였듯 Smalltalk-80에 원본 클래스 View는 극히 규모가 크고 제어가 힘들다. 모든 사람들을 위해 모든 일을 처리하느라 시달린 경우이다. 뷰는 가장자리 꾸미기(edge decoration) 기능이 있고 (경계 두께와 색상을 안다), 하위뷰의 포함과 관리가 가능했으며, 부모 뷰 안에서 자신의 레이아웃을 처리했다. 또한 모델이나 (Observer 패턴을 통해) 관련된 컨트롤러와 통신을 책임졌다. 그 결과 각 View는 수많은 인스턴스 변수를 포함하게 되었다; 하지만 각 인스턴스는 그러한 변수들의 하위집합만을 사용했다.

Objectworks\Smalltalk 4.0은 (비주얼웍스의 선도자) Rubin과 Liebs (1992)가 설명한 바와 같이 View 계층구조의 상당한 리팩토링을 통해 이 문제를 해결하였다. 앞서 설명한 View의 책임을 각각 View 계층구조 내에 서로 다른 클래스로 리팩토링하였다. 따라서 추상적 비주얼 슈퍼클래스인 VisualPart의 하위클래스 집합 3개가 구성되었다:

  1. View 계층구조는 모델과 컨트롤러를 가진 객체들로 구성
  2. Wrapper 계층구조는 Decorator 패턴을 사용해 다른 비주얼의 포지셔닝 또는 디스플레이를 수정하는 객체들로 구성
  3. CompositePart 계층구조는 Composite 패턴을 이용해 다른 비주얼을 통합하는 비주얼들로 구성

그 결과 클래스의 규모가 훨씬 줄었고, 개발자는 그가 필요로 하는 부분만을 상속하는 서브클래싱만 신중히 선택할 수 있게 되었다.

리팩토링 도중에 발생한 문제 중 하나는 추상적 슈퍼클래스에 행위의 거래(trading) 상태에 대한 규칙이었다. 특히 리팩토링으로 인해 발생한 서로 다른 타입의 비주얼에서 제어를 처리하는 도중에 문제가 발생했다.

리팩토링을 거친 계층구조 대부분의 용도에서는 위에 설명한 기본적 3가지로 충분했다. 대부분의 경우 View는 (Controller가 있는 비주얼) Composite와 Decorator로 구성된 비주얼 트리의 잎에 속한다. 하지만 이것이 실패하는 한 가지 경우가 있다. 때때로 컨트롤러를 갖고 있지만 다른 비주얼을 포함하는 View가 필요할 때가 있다; 이것이 CompositeView이다. CompositeView의 예는 비주얼웍스의 canvas editor인데, 이것은 UIPainterView의 인스턴스이다. UIPainterView 내에 각 뷰는 고유의 컨트롤러를 갖고 있는데 이는 컨트롤러를 선택하여 이동하면 어떻게 반응하는지를 설명한다. 그러나 UIPainterView 자체도 canvas에 관한 메뉴가 전체로서 작동하는 방법을 설명하는 컨트롤러가 필요하다. 언뜻 보면 View 행위를 CompositePart의 서브클래스로 전송하는 문제에서 다수의 상속을 요구하는 듯 보이지만 비주얼웍스는 Template Method 패턴을 기발하게 적용시킴으로써 이 문제를 해결한다.

추상적 슈퍼클래스 VisualPart는 메서드 objectWantingControl을 Template Method로 구현한다. 메서드 objectWantingControl은 제어를 원하는 수신자의 비주얼 트리 내에서 컨트롤러를 리턴한다. 이 메서드는 훅 메서드getController 를 사용한다. 비주얼웍스 이미지에서 사용되는 메서드는 다음과 같다:

VisualPart>>objectWantingControl
	"The receiver is in a control hierarchy and the
	container is asking for an object that wants control.
	If no control is desired then the receiver answers
	nil. If control is wanted then the receiver answers
	the control object."

	| ctrl |
	ctrl := self getController.
	ctrl isNil ifTrue: [^nil].
	"Trap errors occurring while searching for
	the object wanting control."
	^Object errorSignal
		handle: [...]	"Handle error"
		do: [ctrl isControlWanted
			ifTrue: [self]
			ifFalse: [nil]]

VisualPart에 정의된 바와 같이 getController의 기본 행위는 nil을 리턴하는 것인데, 이는 제어를 원하는 Controller가 없음을 의미한다. 하지만 이 메서드는 View와 CompositeView 서브클래스에서 상태로서 구현되어 각각 해당하는 컨트롤러를 리턴한다. 이렇게 두 계층구조의 객체들은 불필요한 코드 중복 없이 그들이 원하는 행위를 얻을 수 있다.

Template Method가 비주얼웍스의 비주얼 컴포넌트 계층구조에 사용되는 예는 많다. 사용예를 발견하고 이해하는 것은 실습의 기회가 될 것이다.

구현

복잡한 메서드를 템플릿 메서드로 구현하는 한 가지 방법을 소개하고자 한다:

  1. 단순 구현. 모든 코드를 하나의 메서드에 구현한다. 코드의 팩토링을 시도 시 리팩토링하기 전에 코드를 작성하는 (write) 것이 더 편리하다. 이 거대한 메서드는 템플릿 메서드가 될 것이다.
  2. 단계를 나누라. 메서드를 논리 단계로 나누기 위해 코멘트를 사용한다. 코멘트를 이용해 각 단계를 설명하도록 한다.
  3. 단계 메서드를 생성하라. 각 단계에서 구분된 메서드를 템플릿 메서드로 생성한다. 이 메서드들을 템플릿 메서드로서 동일한 클래스에 넣는다. 단계 2에서 설명한 코멘트를 바탕으로 각 단계 메서드를 명명하고 템플릿 메서드로부터 그 코드를 이동한다. 단계 메서드가 템플릿 메서드로부터 임시 변수를 필요로 하는 경우 변수를 파라미터로서 통과시킨다.
  4. 단계 메서드를 호출하라. 템플릿 메서드를 단순화시켜 각 단계에 해당하는 단계 메서드를 호출함으로써 단계를 실행한다. 템플릿 메서드에 남겨진 것은 전체 알고리즘의 윤곽이다. 이제는 이것이 진짜 템플릿 메서드이다.
  5. 상수 메서드를 생성하라. 템플릿 메서드가 여전히 문자열이나 클래스명, 또는 다른 상수를 포함하고 있다면 이 문자열을 구분된 메서드로 팩토링한다. 각 상수마다 상수를 리턴하는 일만 하는 메서드를 정의한다. 그리고 나서 상수를 하드코딩하는 대신 템플릿 메서드를 변경하여 이러한 메서드를 불러온다.
  6. 단계 1~5를 반복하라. 당신이 생성한 각 단계 메서드마다 이 과정을 반복하라. 이는 프리미티브 메서드를 호출하는 템플릿 메서드로 팩토링할 것이다. 각 메서드의 모든 단계가 같은 수준의 일반성을 가질 때까지, 그리고 모든 상수가 고유의 메서드로 팩토링될 때까지 계속한다.

예제 코드

좀 더 공통적인 Template Method (Auer가 설명, 1995) 패턴의 사용예는 지연 초기화 메서드에 기본 값을 격리시키는 경우이다. 다음 메서드를 살펴보자:

Circle>>radius
	radius == nil ifTrue: [radius := 10].
	^radius

이 메서드는 슈퍼클래스에서는 잘 작동하지만 서브클래스가 radius 에 대한 기본 값을 10이 아닌 값으로 대체하고자 할 때 문제가 발생한다. 이런 경우 상수를 제외한 위의 코드를 모두 포함하기 위해 radius 메서드를 구현해야만 한다. 이보다 더 나은 버전은 Template Method 패턴을 이용해 defaultRadius 라 불리는 훅 메서드를 정의하는 것이다. 서브클래스는 이 기본 값을 오버라이드하거나 슈퍼클래스의 기본을 다음과 같이 상속할 수 있다:

Circle>>radius
	radius == nil ifTrue: [radius := self defaultRadius].
	^radius

Circle>>defaultRadius
	"This can be overridden in subclasses."
	^10

그리고 나면 Circle의 서브클래스인 BigCircle는 지연 초기화를 재구현할 필요 없이 기본 반경(radius)을 쉽게 변경할 수 있다:

BigCircle>>defaultRadius
	"Override the inherited default."
	^100

지연 초기화를 사용하지 않는다 하더라도 똑같은 기법을 initialize 메서드에 적용할 수 있다. Template Method 를 사용하지 않는 접근법은 다음과 같다:

Circle>>initialize
	radius := 10.
	"other variable declarations"

BigCircle>>initialize
	"Have to re-set the value of variableName in the subclass"
	super initialize.
	radius := 100.

이전 구현보다 개선된 버전은 다음과 같은 모습일 것이다:

Circle>>initialize
	radius := self defaultRadius.
	"other variable declarations"

defaultRadius 메서드의 구현은 앞의 예제와 동일하다.

알려진 스몰토크 사용예

논의 단락에서는 여러 가지 스몰토크 방언에 사용되는 Template Method 패턴의 사용예를 충분히 보였다. 스몰토크의 기본 클래스 라이브러리의 많은 곳에서도 패턴이 사용되었다. 우리는 알려진 사용예일 뿐 아니라 예제 코드에 해당하는 예제를 몇 가지 더 제시하고자 한다.

WindowPolicy

비주얼 스몰토크에서는 모든 윈도우 창이 윈도우 정책 객체와 관련되어 있는데, 이 객체는 창에 어떤 풀다운 메뉴를 추가할지 결정할 때 불러온다. WindowPolicy는 모든 구체적 정책 클래스에 대한 인터페이스를 정의한다. 서브클래스 SmalltalkWindowPolicy의 인스턴스들은 스몰토크 풀다운 메뉴가 윈도우 창의 메뉴 바에 추가되도록 보장한다. StandardWindowPolicy의 인스턴스들은 표준 파일과 편집 풀다운 메뉴를 추가한다. WindowPolicy의 일반 Template Method는 모든 풀다운 메뉴 추가 시 호출되는데 이는 다음과 같다:

WindowPolicy>>addMenus
	"Private - add the menus for the window to the
	menu bar."
	...
	self
		addSystemMenus;
		addStandardLeftMenus;
		addApplicationMenus;
		addStandardRightMenus

모든 add…Menus 메시지는 빈 메서드로서 WindowPolicy에 구현된다. WindowPolicy의 구체적 서브클래스는 필요 시 이러한 구현부를 오버라이드한다. 예를 들자면 다음과 같다:

StandardWindowPolicy>>addStandardLeftMenus
	"Private - add the menus that are to be located
	on the menu bar before any application-specific
	menus (File & Edit)."

	self addFileMenu.
	self addEditMenu.

결국 이것은 또 다른 템플릿 메서드로서, 파일과 편집 풀다운 메뉴에 추가할 메뉴 항목을 스스로 결정하도록 더 낮은 수준의 서브클래스를 허용한다는 점을 주목하자.

Collection 예제

다양한 스몰토크 방언에 사용되는 Collection 계층구조는 추상 클래스에 발견되는 4가지 타입의 메서드에 대한 예제를 모두 포함한다. 비주얼웍스의 예를 먼저 살펴보자.

collection: 메서드는 여러 개의 프리미티브 메시지를 전송하는 템플릿 메서드이다:

Collection>>collect: aBlock
	"Evaluate aBlock with each of the values of the
	receiver as the argument. Collect the resulting
	values into a collection that is like the receiver.
	Answer the new collection."

	| newCollection |
	newCollection := self species new.
	self do: [:each |
		newCollection add: (aBlock value: each)].
	^newCollection

species메시지는 자신의 실제 클래스보다는 객체의 "종류(kind)"에 대한 클래스를 반환한다. Collection은 그것을 프리미티브 메시지로서 사용하기 때문에 Collection에 구현되어 있을 것이라 예상되겠지만, Collection은 그것을 사실상 Object로부터 상속받는다:

Object>>species
	"Answer the preferred class for reconstructing the
	receiver. ..."

	^self class

이는 오버라이딩을 허용하기 때문에 훅 메서드이나 대부분 서브클래스에 적합한 기본 구현을 제공한다. 한 가지 적합하지 않은 서브클래스가 있다면 Interval이다. 이는 다음과 같이 species를 오버라이드한다:

Interval>>species
	"Answer the preferred class for reconstructing the
	receiver, that is, Array."

	^Array

두 번째 프리미티브 메서드는 do: 이다. Collection에서 이루어지는 그 기본 구현은 다음과 같다:

Collection>>do: aBlock
	"Evaluate aBlock with each of the receiver's elements
	as the argument."

	self subclassResponsibility

이것은 기본 구현을 제공하지 않고 수많은 Collection 서브클래스에 의해 오버라이드되므로 추상적 메서드라 할 수 있다. 이는 기본 구현을 제공하지 않는데, 그 이유는 당신이 OrderedCollection 또는 Set을 반복하는 방식이 매우 다르기 때문이다.

세 번째 프리미티브 오퍼레이션은 add:이다. 실제로 수신자에게 (self) 전송되는 것은 아니지만 다른 Collection으로 전송되므로 더 큰 수집(collecting) 알고리즘의 원시적 부분이다. 그 기본 구현은 다음과 같다:

Collection>>add: newObject
	"Include newObject as one of the receiver's elements. ..."

	self subclassResponsibility

따라서 add: 는 모든 직접 서브클래스가 오버라이드해야 하는 또 하나의 추상적 메서드가 된다;

보기엔 간단하여 프리미티브 메서드처럼 보이지만 사실은 템플릿 메서드인 예로, Collection 내의 또 다른 메서드 remove: 가 있다.

Collection>>remove: oldObject
	"Remove oldObject as one of the receiver's elements. ..."

	^self remove: oldObject ifAbsent: [self notFoundError]

첫 번째 프리미티브 메시지는 remove:ifAbsent: 이다. add: 와 마찬가지로 이는 서브클래스로 미뤄진다:

Collection>>remove: oldObject ifAbsent: anExceptionBlock
	"Remove oldObject as one of the receiver's elements. ..."

	self subclassResponsibility

따라서 remove:ifAbsent: 는 추상적 메서드이다. remove: 는 아무 것도 제거하지 않는다는 점이 중요하다; 모든 작업을 하는 remove:ifAbsetn: 를 얻는다. 따라서 remove: 는 사실상 프리미티브 메서드가 아니라 Template Method이다.

remove: 가 전송하는 또 다른 프리미티브 메시지로 notFoundError: 가 있다:

Collection>>notFoundError
	"Raise a signal indicating that an object is not
	in the collection."

	^self class notFoundSignal raise

이는 모든 Collection 클래스에 작용하므로 어떤 Collection 클래스도 이 메서드를 오버라이드하지 않는다. 따라서 이것은 실제 행위를 구현하기 때문에 구체적 메서드이며, 서브클래스는 그 행위를 변경하지 않는다. 만일 어떤 서브클래스가 행위를 변경했다면 notFoundError는 훅 메서드가 될 것이다.

Magnitude 예제

템플릿 메서드를 찾아볼 수 있는 또 다른 장소는 Magnitude 계층구조이다. IBM 스몰토크의 추상적 클래스 Magnitude 를 예로 들어보자. 그것의 공통 언어 정의 (Common Language Definition) API 프로토콜은 >, <=, >=, between:and:, max:, min: 메시지를 공급한다. 첫 번째 메서드 >는 추상적 메서드로서, 모든 서브클래스가 이를 구현해야 한다. 나머지 메서드들은 템플릿 메서드들로서 >와 관련해 정의되며, >를 구현하는 새로운 서브클래스는 나머지 메서드를 모두 그냥 상속받는다. 당신이 DollarAmount를 Magnitude의 새 서브클래스로 구현한다고 가정하면 나머지 Magnitude 행위를 얻기 위해 >만 오버라이드하면 되는 셈이다.

이와 비슷하게 비주얼웍스에서도 서로 다른 Magnitude 서브클래스들이 < 또는 > 메서드의 논리 버전을 구현할 수도 있다. 다른 Magnitude 메서드들은 "self >" or "self <" 를 전송함으로써 이러한 메서드들을 불러온다. 예를 들어, >= 메서드를 정의하여 다음과 같이 <를 사용할 수 있다:

Magnitude>>>= aMagnitude
	"Answer whether the receiver is greater than or
	equal to the argument."

	^(self < aMagnitude) not

이 접근법에서 장점은 서브클래스가 < 를 재정의하고 나면 >= 를 재정의할 필요가 없다는 점이다.

Object>>printString

Object>>printOn: 는 합당한 기본 값을 제공하는 훅 메서드의 훌륭한 예제가 된다. Object에 정의되는 템플릿 메서드 printString는 다음 페이지에 설명하듯이 printOn:의 로컬 구현을 이용해 실행할 수 있다:

Object>>printString
	"Answer a String that is an ASCII representation
	of the receiver."

	|	aStream aString |
	aString := String new: 20.
	self printOn: (aStream := WriteStream on: aString).
	^aStream contents

아래와 같이 Object 내의 printOn: 의 정의는 메시지를 수신하는 객체에 관해 최소한의 정보만 전한다:

Object>>printOn: aStream
	"Append the ASCII representation of the receiver
	to aStream. This is the default implementation which
	prints 'a' ('an') followed by the receiver class name."

	| aString |
	aString := self class name.
	(aString at: 1) isVowel
		ifTrue: [aStream nextPutAll: 'an ']
		ifFalse: [aStream nextPutAll: 'a '].
	aStream nextPutAll: aString

하지만 대다수의 서브클래스의 경우 정보가 많을수록 유용하다. 예를 들어, String안에서 실제 문자는 따옴표 문자열로 표시된다. 이를 위해선 String이 printOn: 의 기본 구현을 오버라이드한다:

String>>printOn: aStream
	"Append the receiver as a quoted string
	to aStream doubling all internal single
	quote characters."

	aStream nextPut: $'.
	self do:
		[ :character |
		aStream nextPut: character.
		character = $'
			ifTrue: [aStream nextPut: character]].
	aStream nextPut: $'

이러한 방식으로 printString은 변경되지 않은 채로 남아 있으면서 각 클래스에 대한 그 행위는 다양하게 구현될 수 있다. 그 클래스들은 훅 메서드로서, printOn: 만 오버라이드하면 된다.

Notes