DesignPatternSmalltalkCompanion:Strategy

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

STRATEGY (DP 315)

의도

알고리즘군을 정의하고 각각의 알고리즘을 별도의 클래스로 캡슐화한 후 각 클래스를 동일한 인터페이스로 정의하여 교환 가능하게 만든다. Strategy 패턴은 이를 사용하는 클라이언트로부터 독립적으로 알고리즘을 다양하게 변경할 수 있게 한다.

구조

Dpsc chapter05 Strategy 01.png

논의

애플리케이션은 종종 자신을 대신할 특정 서비스, 시스템 기능 또는 알고리즘을 필요로 하지만 이러한 서비스를 실행하는 방법에는 여러 가지가 있다. 메인 애플리케이션 클래스에 여러 메서드를 부호화하여 다수의 알고리즘을 구현하는 대신 우리는 Strategy 패턴을 적용시켜 다수의 구분된 클래스에 다양한 변형체를 캡슐화할 수 있다. 알고리즘 선택에 따라ㅡ또는 전략에 따라ㅡ이 클래스 중 하나를 서비스 제공자로 인스턴스화된다. 이후 애플리케이션은 필요 시 이러한 외부 헬퍼(helper) 객체로 서비스 요청을 전달한다. 동일한 서비스에 대해 서로 다른 버전을 제공하는 클래스들은 모두 동일한 서비스-요청 인터페이스를 다형적으로 구현하여 서비스 요청자가 그들과 동일하게 상호작용할 수 있도록 한다.

이러한 일은 언제 해결해야 할까? 많은 시스템은 컨텍스트나 상황을 바탕으로 다수의 전략에서 선택해야 하며, 여러 대화형 애플리케이션들이 사용자로 하여금 여러 개의 알고리즘적 전략 중 하나를 선택할 수 있도록 한다. 예를 들어, 영상압축 프로그램들은 주로 여러 개의 가능한 압축 알고리즘들 중 공간-속도 또는 공간-화질 상쇄관계의 고려사항을 바탕으로 하여 사용자가 하나를 선택할 수 있도록 허용한다. 일부 알고리즘은 고화질 영상 무결성을 유지하지만 높은 대역폭의 재생기능을 요구하는 큰 데이터 파일로 압축되기도 하고, 파일 손실은 있지만 크기가 작고 재생 요구사항이 단순한 알고리즘도 있다.[1] 따라서 프로그램은 사용자가 그들의 요구에 맞는 알고리즘을 선택하도록 허용해야 한다.

아래는 다수의 서비스 제공자를 필요로 하는 애플리케이션 예제를 몇 가지 소개한다:[2]

Application General Service Ways of Accomplishing Service
Video Compression 비디오 압축 MPEG, AVI, QuickTime 형태
Car Assembly 자동차/제품에 부품 추가하기 Ford, Toyota, Porsche Builders
Product Visualization 조립된 자동차 그리기 Ford, Toyota, Porsche renderers
Drawing Editor 그림을 디스크로 저장하기 BMP, GIF, JPEG 형태
File Compression 디스크 파일 압축하기 Zip, Huffman, RLE 알고리즘
Business Graphics 수치 데이터 그리기 Line plot, bar chart, pie chart
Document Editor 화면 내용 배열하기 Diffrent line-break strategies

이 중에서 [디자인 패턴] 편 DP 315페이지에 소개된 예제를 본을 따서 만든 마지막 애플리케이션을 살펴보자. 여기서 문서 편집기는 텍스트와 그래픽스를 포함하는 composition 객체에서 작동한다; composition은 출력에 대해 스스로 포맷팅을 책임지고, 사용자 선호를 바탕으로 레이아웃 전략을 선택할 수 있다. 이러한 상황에서 여러 전략들을 구현하는 한 가지 방법으로, 개별적 메서드와 마찬가지로 클래스 안에 여러 가지 알고리즘을 구현함으로써 책임이 있는 클래스의 구현을 "부풀리는(bloat)" 방법이 있다; composition 예제에서 여러 가지의 레이아웃 알고리즘을 Composition 클래스 내에 구분된 메서드로 통합하는 것을 의미하겠다. 그리고 나면 이 기능이 필요할 때마다 composition 객체가 조건문 집합을 이용해 적절한 메서드를 호출할 것이다. 물론 확장성과 관련해서는, 추후 새 전략을 추가 시 조건문을 재방문(revisiting)한다는 뜻을 내포하고 있다.

위를 대신해 Strategy 패턴을 사용하여 좀 더 모듈식의 확장 가능한 해법을 제공할 수도 있다. 우선 각 포맷팅 전략 또는 알고리즘을 구분된 클래스에 구현하고, composition이 Strategy 클래스 중 하나의 인스턴스를 가리키도록 한 후 이 형식자 객체에게 레이아웃 기능을 수행하도록 요청하고자 한다. 따라서 사실상 포맷팅 기능은 외부의 헬퍼 객체가 실행한다. 포맷팅 알고리즘은 단순히 하나의 또는 다른 레이아웃/Strategy 클래스를 인스턴스화시켜 선택할 수 있다. 이는 그때그때 사용자에 의해 (예: 메뉴 선택을 통해) 또는 변하는 조건을 바탕으로 프로그램에 따라 변경될 수 있다. 여기에는 새로운 Strategy 객체의 인스턴스화를 비롯해 composition가 이를 가리키도록 방향 전환하는 과정을 수반한다. 어떤 메시지를 보낼지 결정하기 위해 조건문을 반복 실행하는 데에 반해 클라이언트는 (composition) 레이아웃을 계산할 때마다 형식자에게 단일 메시지를 전송한다.

이후 애플리케이션의 수명이 끝나기 전에 새로운 포맷팅 요구사항이 발생할 경우 새 알고리즘들이 구분된 클래스에 분리되어 있으므로 이들을 구현하여 통합하는 것이 수월하겠다. Composition 의 구현 또한 다양한 포맷팅 알고리즘을 모두 포함하는 것이 아니기 때문에 패턴의 이해와 유지가 쉽다. 허나 가장 전형적인 객체지향 디자인에서는 추상 인터페이스를 사용하는 라인 분리자 객체 몇 개와 통신할 뿐이며, 인터페이스를 따르는 한 어떤 유형의 라인 분리자 객체인지 신경 쓰지 않는다.

다음 경우에서 라인 분리 기능을 호출할 때 필요한 코드의 차이를 간단히 알아보고자 한다: (1) 여러 개의 라인 분리 알고리즘을 Composition 클래스 내에 구분된 메서드로 부호화할 경우, (2) Strategy 패턴을 사용할 경우. [디자인 패턴] 편에서와 같이 Composition 클래스는 문서의 전체적 레이아웃을 업데이트한 후 레이아웃 코드를 작동시킬 때 호출되는 repair 메서드를 구현한다.

Strategy 패턴을 사용하지 않을 때

여기서 Composition은 formattingStrategy라는 이름의 인스턴스 변수를 포함하는데, 이 변수는 호출할 알고리즘을 (즉, 메서드) 결정하는 일을 한다. 두 가지 구현법을 살펴보자:

Brute Force. 첫 번째 구현법은 사실상 어느 언어에서의 부호화와 마찬가지로 it…then… 등과 같은 문자열을 통합한다:

Composition>>repair
	"Without the strategy pattern."
	formattingStrategy == #Simple
		ifTrue: [self formatWithSimpleAlgorithm]
		ifFalse: [formattingStrategy == #TeX
			ifTrue: [self formatWithTeXAlgorithm]
			ifFalse: [...]

스몰토크 특유의 접근법. 스몰토크에서는 메시지 선택기에 대한 상징적 표현인 perform: 의 실행 기능에 중점을 둘 수 있다. 앞서 언급하였듯 이 접근법은 기발하지만 프로그램의 이해적 측면에서는 까다롭다. 코드 브라우저의 "발신자"와 "메시지"와 같은 정적 분석 도구들조차 이 코드를 실패한다. 예를 들어, 다음 메서드가 전송한 "메시지"를 요청할 경우, 결과 리스트는 formatWithSimpleAlgorithm, formatWithTeXAlgorithm, 또는 프로그램에 따라 구성된 다른 포맷팅 메서드들을 포함하지 않을 것이다:

Composition>>repair
	"Without the strategy pattern, but using perform:."
	| selector |
	"Construct the name of the method to invoke:"
	selector := ('formatWith', formattingStrategy,
			'Algorithm') asSymbol.
	self perform: selector

Strategy 패턴의 사용

여기서 Composition 는 레이아웃 객체를 가리키는 formatter 라는 이름의 인스턴스 변수를 가진다. 이러한 헬퍼 객체가 하는 일은 그저 format: 메시지에 응답하는 데에 그치기 때문에 Composition은 헬퍼 객체의 정확한 클래스를 신경 쓸 필요가 없다:

Composition>>repair
	"With the Strategy pattern."
	formatter format: self.

Strategy 패턴을 적용 후 애플리케이션의 구조는 다음과 같다:

Dpsc chapter05 Strategy 02.png

예제 코드

이전의 예제 중 하나를 가져와 자세히 살펴보자. 사용자와 상호작용이 가능한 사무 도표 애플리케이션이 있다고 가정하고, 이를 이용 시 사용자는 다양한 수치 또는 재정 데이터를 시각화할 수 있다고 치자. 사용자는 지난 4분기에 기업의 총소득과 같은 자료를 요청할 수 있고, 애플리케이션은 다양한 형식으로 자료의 시각화를 제공한다: 막대그래프, 선 그래프 또는 파이 그래프.

먼저 GraphVisualizer 클래스의 구현으로 시작해보자. 대화형 애플리케이션이기 때문에 ViewManager의 서브클래스로 GraphVisualizer 를 만들겠다 (우리 예제는 비주얼 스몰토크 코드로 설명하겠다). 시각화 창에 SQL 쿼리에 대한 텍스트 엔트리 필드가 있을 것이다; 이에 채워 넣을 자료는 사용자의 쿼리를 바탕으로 데이터베이스에서 검색할 것이다. 윈도우 창에는 데이터를 도표로 나타낼 GraphPane와 "Graph It" 버튼이 포함되어 있다. 사용자가 그래프 유형을 선택하고, SQL 문을 입력 후 애플리케이션의 버튼을 누르면 애플리케이션은 데이터베이스 쿼리를 바탕으로 정보를 검색하여 위치시킨다.

ViewManager subclass: #GraphVisualizer
	instanceVariableNames: 'graphPane data grapher'
	classVariableNames: ''
	poolDictionaries: ''

GraphVisualizer>>grapher: aGrapher
	grapher := aGrapher

grapher 인스턴스는 애플리케이션이 그래픽 시각화 그리기 업무를 위임하는 객체를 가리킬 것이다. 이 객체는 다음 Grapher 하위계층구조의 클래스 중 하나로부터 인스턴스화될 것이다:

Dpsc chapter05 Strategy 03.png

Grapher는 추상 슈퍼클래스로서 (현재) 3개의 구체적 서브클래스를 가진다. 사용자가 파이 차트로 정보를 표시하길 요청하면 GraphVisualizer는 PieChartGrapher를 인스턴스화하고 그 인스턴스를 자신의 grapher 객체로 설치한다. 선 그래프와 막대 도표도 마찬가지다. 그래프를 그려야 하는 상황이 오면 GraphVisualizer는 자신의 grapher에 업무를 수행하라는 메시지를 전송한다.

Grapher 계층구조의 추상적 슈퍼클래스의 정의부터 시작해보자:

Object subclass: #Grapher
	instanceVariableNames: 'pen'
	classVariableNames: ''
	poolDictionaries: ''

Grapher>>pen: aPen
	"Draw using aPen"
	pen := aPen

모든 Grapher 객체들은 그림을 그릴 때 Pen 인스턴스를 사용한다. GraphPane에 그래프를 그리는 우리 예제의 애플리케이션에서는 잘 작동하고 있다. Grapher의 pen 변수는 패인(pane)의 펜을 참조하도록 설정하였다. 이는 Bitemap이나 Printer와 같은 Pen을 가진 클래스를 수반하는 다른 애플리케이션에서 Grapher 클래스를 재사용할 수 있음을 의미한다.

비주얼라이저 창에는 사용자가 그래프 유형을 선택하게 해주는 메뉴가 있다. 메뉴에서 그래프 유형을 선택하고 나면 GraphVisualizer가 적절한 Grapher 서브클래스를 인스턴스화하여 grapher 변수에 인스턴스를 보관한다. 그래프 선택 메뉴는 다음과 같은 모습이다:

Dpsc chapter05 Strategy 04.png

이러한 메뉴 항목에 상응하는 메서드는 다음과 같다:

GraphVisualizer>>useBarChart
	"The user has selected 'Bar Chart' from the 'Graph
	Type' menu. Create a new BarChartGrapher."
	self grapher:
		(BarChartGrapher new
			pen: graphPane pen)

GraphVisualizer>>useLineGraph
	"The user has selected 'Line Graph'."
	self grapher:
		(LinePlotter new
			pen: graphPane pen)

GraphVisualizer>>usePieChart
	"The user has selected 'Pie Chart'."
	self grapher:
		(PieChartGrapher new
			pen: graphPane pen)

"Graph It" 버튼을 누르면 GraphVisualizer가 데이터베이스에서 적절한 데이터를 검색하여 grapher에게 검색된 결과를 표시하라고 말한다. 버튼을 누르면 호출되는 메서드는 다음과 같다; 여기서 Grapher (Strategy) 객체를 불러온다:

GraphVisualizer>>graphIt
	"First, make sure the user has selected a graph type:"
	grapher isNil ifTrue:
		[^MessageBox message: 'Please select a graph type'].
	"Pass the user's SQL query to the database and
	store the result in 'data':"
	data := ...
	"Now plot it:"
	grapher plot: data

이제 Grapher와 그 구체적 서브클래스의 외곽을 만든다:

Grapher>>plot: data
	"Draw the graph for the information in 'data'."
	self implementedBySubclass

Grapher subclass: #BarChartGrapher
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

BarChartGrapher>>plot: data
	"Draw a bar chart depicting the information
	contained in 'data'."
	...

Grapher subclass: #LinePlotter
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

LinePlotter>>plot: data
	"Draw a line plot depicting the information
	contained in 'data'."
	...

이제 애플리케이션은 구조적으로 다음과 같은 모습이다:

Dpsc chapter05 Strategy 05.png

이것을 일반화된 구조 다이어그램과 관련시키면 GraphVisualizer는 Context에 해당하고, Grapher는 추상적 Strategy에 해당하므로 모든 구체적 Grapher 서브클래스에 공통된 인터페이스를 정의한다. LinePlotter, BarChartGrapher, PieChartGrapher는 ConcreteStrategyA, B, C를 각각 매핑한다.

이 예제에서 한 가지 중점은 여러 가지 그래핑 전략을 구현하는 구분된 Strategy 객체가 있으며 (Grapher 클래스에서 인스턴스화된), 애플리케이션은 그래핑 기능을 스스로 구현하는 대신 이 헬퍼 객체에게 위임한다는 점이다.

구현

본문에서 다룬 예제를 비롯해 다른 사용예에서 제기되는 몇 가지 구현 문제를 설명하고자 한다:

Context 객체와 Strategy 객체 결합하기

GraphVisualizer가 자료를 그래프로 나타내야 하는 경우 그것은 자신의 Grapher 객체를 호출한다. 이 때 발생하는 구현 문제는 두 객체들 간 (일반적으로 Strategy 객체와 Context 객체) 인터페이스를 어떻게 구성하여 정보를 공유하도록 할 것인지가 된다. 사실 두 객체를 결합하여 정보 공유를 가능하게 하는 방식에 관한 문제는 우리가 사용한 많은 패턴에서 제기된다. 여기에는 [디자인 패턴]에서 논하듯 여러 가지 접근법이 있다.

GraphVisualizer의 경우, 공유해야 하는 정보는 Grapher가 그림을 그려야 하는 Pen과 표시할 자료가 된다. 이는 서로 다른 범주의 공유 정보를 나타낸다: 정적 정보ㅡ애플리케이션이 실행되는 도중에는 변경되지 않을 정보 (Pen), 동적 정보ㅡ시간이 지나면서 변할 정보 (자료). 정적 정보의 경우, 이번 예제의 Grapher와 같이 헬퍼 객체에게 한 번 통지하여 처리한다. 반대로 동적 정보의 경우, 헬퍼 객체를 불러올 때마다 헬퍼 객체가 동적 정보를 인지하도록 보장해야 한다.

메시지 아큐먼트. 또 한 가지 접근법으로, GraphVisualizer가 모든 정보를 메시지 아큐먼트 형태로 하여 Grapher 오퍼레이션으로 전달함으로써 두 개의 객체를 느슨하게 결합시키는 방법이 있다. 따라서 Grapher로 전송된 메시지는,

grapher plot: data.

가 아니라 다음과 같다:

grapher plot: data using: graphPane pen

물론 이번 사례에서 우리는 두 개의 아규먼트로 Grapher 클래스 내에 그래프 그리기 메서드를 정의할 것이다:

Grapher>>plot: data using: aPen
	...

그러나 Pen 객체는 공유된 자료의 정적인 부분이다. 따라서 이와 관련해 그래프를 그릴 때마다 Grapher 객체에게 알리는 것이 아니라 한 번만 알리면 되는 일이다. Pen을 인스턴스화 하자마자 Grapher로 전달하였다:

GraphVisualizer>>useBarChart
	self grapher:
		(BarChartGrapher new
			pen: graphPane pen)

Pen은 한 번만 구성하면 된다; graphIt 메서드는 동적인 data 객체를 따라 전달되고, 이 객체는 우리가 그래프를 그릴 때마다 전송되어야만 한다.

우리는 또 다음과 같이 Grapher에 new 대신 인스턴스 생성 메서드를 사용하여 시작할 수도 있다:

Grapher class>>using: aPen
	"Instance creation method."
	^self new pen: aPen

그리고 다음과 같이 인스턴스화 시점에 Pen을 Grapher 객체로 전달한다:

GraphVisualizer>>useBarChart
	self grapher:
		(BarChartGrapher using: graphPane pen)

자기 위임. 또 다른 접근법으로, GraphVisualizer가 자신을 argument로 하여 Grapher에게 전달하여 Grapher가 직접 메시지 전송을 통해 GraphVisualizer에서 필요로 하는 어떤 자료든 명시적으로 요청하도록 허용하는 방법이 있다. Beck (1997)은 이를 자기 위임(self-delegation)이라 부른다. 이 접근법을 이용 시 GraphVisualizer는 그래프를 그리고 싶을 때 다음을 전송한다:

GraphVisualizer>>graphIt
	...
	grapher plotFor: self

따라서 GraphVisualizer를 향한 포인터와 같이 제공되는 Grapher는 표시할 자료(data to plot)와 같은 정보를 요청하는 GraphVisualizer 메시지를 전송할 수 있다. 그래서 우리는 이번 예제에서와 같이, 하나의 아규먼트, GraphVisualizer로 Grapher 클래스에 그래프 그리기 메서드를 정의하였다:

BarChartGrapher>>plotFor: aGraphVisualizer
	| data |
	data := aGraphVisualizer data.
	"Draw the bar chart:"
	...

사실 인스턴스화 시점에 그리기 Pen으로 Grapher 객체를 구성하는 접근법을 취하지 않았다면 이 메서드는 다음과 같을 것이다:

BarChartGrapher>>plotFor: aGraphVisualizer
	| data pen |
	data := aGraphVisualizer data.
	pen := aGraphVisualizer penToDrawWith.
	"Now, draw the bar chart:"
	...

물론 GraphVisualizer 클래스는 이제 좀 더 정교한 인터페이스를 정의하여 내부 객체에 접근성을 제공한다 (예: data나 penToDrawWith와 같은 메서드). 그 결과, (1) GraphVisualizer의 캡슐화는 "약화되고" (그것이 선택한 전송 메시지에 스스로를 아규먼트로 제공하는 대신 내부 객체로 외부자의 접근을 허용하게 된다), (2) GraphVisualizer와 그 Grapher는 더 강하게 결합된다. 따라서 이번 예제에서는 덜 바람직한 해법이지만, 정보 공유가 많은 예제에서는 선호하는 방법이라 할 수 있다.

백 포인터. 자기 위임을 약간 변형한 방식으로, GraphVisualizer를 향하는 백 포인터를 유지하는 Grapher 객체를 인스턴스 변수의 형태로 하는 방법이 있다. 사실상 구현적 측면에서는 아주 조금 변형을 줬지만 개념적으로는 다르다. GraphVisualizer 자체가 Grapher 객체와 함께 공유할 정적 정보의 조각으로 간주할 수 있다. 따라서 Grapher 인스턴스화 시점에 이 정보를 한 번 공유할 수 있다. 그리고 나면 Grapher가 필요로 하는 어떠한 동적 정보든 그때그때 요청할 수 있다. 이에 따라 Grapher 클래스는 graphVisualizer 인스턴스 변수로 정의할 수 있고, 이는 관련된 setter 메시지와 새로운 인스턴스 생성 메서드를 구현할 것이다:

Object subclass: #Grapher
	instanceVariableNames: 'graphVisualizer'
	classVariableNames: ''
	poolDictionaries: ''

Grapher>>graphVisualizer: aGraphVisualizer
	graphVisualizer := aGraphVisualizer

Grapher class>>for: aGraphVisualizer
	"Instance creation"
	^self new graphVisualizer: aGraphVisualizer

물론 이러한 구현은 Grapher의 구체적 서브클래스에 의해 상속될 것이다. GraphVisualizer 내의 Grapher 인스턴스 생성은 다음과 같을 모습을 보이게 될 것이다:

GraphVisualizer>>useBarChart
	grapher := BarChartGrapher for: self

이제 GraphVisualizer가 자신의 Grapher로 전송한 메시지는 더 간단해진다: 아규먼트가 포함되어 있지 않다.

GraphVisualizer>>graphIt
	grapher plot

자기 위임 case와 마찬가지로 Grapher 객체는 관련된 GraphVisualizer로부터 정보를 직접 요청하지만 이제는 자신의 graphVisualizer 인스턴스 변수로 전송된 메시지로 요청할 것이다:

BarChartGrapher>>plot
	| data pen |
	data := graphVisualizer data.
	pen := graphVisualizer penToDrawWith.
	"Now, draw the bar chart:"
	...

이 접근법에 한 가지 결점이 있다면, Strategy 객체는 (Grapher) 단일 Context 객체를 (GraphVisualizer) 직접 가리키고 있기 때문에 Strategy 객체가 더 이상 Context들 사이에서 공유되지 않을 수도 있다. 따라서 영속적 백 포인터보다 자기 위임을 선호하는 애플리케이션이 있을지도 모른다.

동시에 다수의 Strategy 객체 연결, 한 번에 하나만 활성화

애플리케이션은 때때로 다수의 Strategy 객체를 동시에 연결하되 한 번에 하나만 활성화시킬 수가 있다. 즉 알고리즘이나 전략이 변경될 때마다 Strategy 객체를 새로 생성하는 것이 아니라, 애플리케이션이 시작 시 가능한 Strategy 인스턴스를 모두 생성하고 이를 교체할 수 있다는 말이다. 이는 추가 인스턴스를 보관하는 데에 드는 메모리와 관련해 비용이 많이 들지 않고 Strategy 객체를 인스턴스화하고 초기화하는 것이 시간이 많이 소요될 때 선호된다. 이러한 접근법에서는 인스턴스화된 Strategy 객체를 "기억하는" 방법, 즉 메인 애플리케이션에서 포인터가 객체를 향하도록 유지함을 암시한다. 한 가지 접근법으로는 모든 Strategy 객체를 포함하는 Dictionary를 참조하는 인스턴스 변수 하나와, 최근 활성 인스턴스를 향하는 하나의 인스턴스 변수를 가진다.

Graph Visualizer를 이용해 이 접근법의 예제를 들기 위해 우리는 활성 Graph 객체를 가리키는 grapher 변수를 계속 사용할 것이다. 모든 Grapher 인스턴스를 포함하는 Dictionary는 allGraphers라는 이름의 변수에 의해 참조될 것이다:

ViewManager subclass: #GraphVisualizer
	instanceVariableNames:
		'graphPane data grapher graphMessage allGraphers'
	classVariableNames: ''
	poolDictionaries: ''

우리는 allGraphers를 초기화하기 위해 다음 메서드를 추가한다:

GraphVisualizer>>initialize
	"Set up the Dictionary containing all my Strategy objects."
	allGraphers := Dictionary new.
	allGraphers
		at: #BarChart	put: (BarChartGrapher for: self);
		at: #LineGraph	put: (LinePlotter for: self);
		at: #PieChart	put: (PieChartGrapher for: self)

이제 GraphVisualizer 인스턴스 생성을 다음과 같이 변경한다:

GraphVisualizer class>>new
	^super new initialize

또한 Symbol을 Grapher 인스턴스 대신 자신의 아큐먼트로 취하기 위해선 grapher: setter 메서드도 변경해야 한다. Symbol은 allGraphers Dictionary에 키를 명시한다:

GraphVisualizer>>grapher: aSymbol
	"Change my current graphing strategy."
	grapher := allGraphers at: aSymbol.

마지막으로, 사용자가 "Graph Type" 메뉴에서 항목을 선택하면 우리는 다음과 같이 grapher: 를 불러온다:

GraphVisualizer>>useBarChart
	self grapher: #BarChart

GraphVisualizer>>useLineGraph
	self grapher: #LineGraph

GraphVisualizer>>usePieChart
	self grapher: #PieChart

Strategy 객체로서의 도메인 특정적 객체

Strategy 객체는 특정 알고리즘을 구체화하지 않아도 된다; Strategy 객체의 역할을 하는 실제 애플리케이션-특정적 객체일지도 모른다. 여기서는 이 접근법을 보여주는 금융 도메인의 예를 살펴보고자 한다. 잠재적 고객이 은행에 들어와 특정 금액을 대출할 경우 월지불액이 얼마나 되는지 알고 싶어한다고 가정하자. 문제는 대출의 종류가 많아서 동일한 원금 금액이라 하더라도 월지불액은 서로 다르다. 따라서 대출액 계산기 애플리케이션을 상상해보자. 사용자는 대출 유형, 착수금, 대출 기간을 선택하여 시스템과 상호작용하고, 애플리케이션은 이에 따라 월별 지불액을 계산한다. 이러한 유형의 애플리케이션은 은행업무를 보는 층의 키오스크나 은행의 웹사이트에서 이용할 수 있다. 이 애플리케이션의 구조는 다음과 같다:

Dpsc chapter05 Strategy 06.png

사용자가 대출 유형을 선택하면 Mortgage 서브클래스 중 하나가 인스턴스화된다 (ARM은 변동금리 모기지를 의미한다). 월별 지급액을 결정하기 위해 Mortgage 인스턴스 자체가 애플리케이션의 Strategy 객체 역할을 하여 알고리즘적 계산을 수행한다. 알고리즘의 선택은 대출 유형의 선택과 일치한다. 따라서 다른 책임을 가진 도메인 특정적 객체는 Strategy의 역할을 할지도 모른다.

알려진 스몰토크 사용예

ImageRenderer

비주얼웍스에서 ImageRender는 "제한된 색상을 사용해 이미지를 렌더하는 기법을 표현하는 추상 클래스" 이다 (클래스 코멘트 참조). ImageRenderer는 장치에 맞는 적절한 색상을 이용해 그래픽스 장치에 이미지를 그린다 (예: 일부 화면은 256 색을 지원하는 반면 16색을 지원하는 장치도 있다; ImageRenderer는 지원하는 색상에 대해 이미지의 색상을 매핑해야 한다). ImageRenderer의 구체적 서브클래스는 공통된 렌더링 메시지 프로토콜에 대해 유일한 구현을 제공함으로써 서로 다른 렌더링 알고리즘을 캡슐화한다. ImageRenderer 서브클래스에는 NearestPaint, OrderedDither, ErrorDiffusion가 포함되어 있다ㅡ클래스 코멘트를 참조하면 다음과 같다: "서브클래스들 중에는 NearestPaint와 같은 매핑 클래스와 OrderedDither나 ErrorDifusion과 같은 해프토닝 방법이 있다." 그래픽스 장치에 따라 (예: 프린터와 화면) 서로 다른 ImageRenderer 서브클래스를 인스턴스화하여 자체 성능에 따라 렌더링을 실행하지만 (회색 톤-컬러, 컬러/회색 톤 깊이), 객체들은 동일한 메시지 인터페이스를 구현하기 때문에 메시지를 렌더링 객체로 전송하는 코드는 그것이 어떤 타입의 ImageRenderer와 상호작용하는지 신경쓰지 않는다.

뷰-컨트롤러

모델-뷰-컨트롤러 프레임워크에서 뷰-컨트롤러의 관계는 Strategy 패턴의 예제이다. View 인스턴스는 (화면 위젯을 표현) Controller 객체를 사용하여 마우스나 키보드를 통해 사용자 입력을 처리 및 응답한다. View가 다른 사용자 상호작용 전략을 사용하기 위해서는 컨트롤러 객체에 다른 Controller 클래스를 인스턴스화할 수 있다. 이는 사용자 상호작용 스타일을 그때그때 바꿀 수 있도록 런타임 시에 발생하기도 한다; 예를 들어, 인스턴스화 후 입력을 무시하는 컨트롤러로 전환 시 View를 비활성화시킬 수 있다 (어떠한 사용자 입력도 수락하지 않는다).

보험 정책 Policies

ISSC Object Technology Services의 Philip Hartman은 보험회사에서 자동차보험 정책에 여러 가지 비지니스 논리를 구현할 수 있도록 스몰토크 애플리케이션에 Strategy 패턴을 사용하였다. 한 가지 요구사항은 애플리케이션에 보험회사 자회사와 보험 계약자의 거주 주(state)에 따라 보험료 계산의 논리가 달라야 한다는 점이었다. 예를 들어, 폭행이나 사고에 대한 운전자에게 점수를 하고, 자회사와 주에 따라 점수의 부과 방법과 부과된 점수량을 결정하는 데에 서로 다른 원칙을 사용한다. 많은 사례에서 점수는 청구 보험료에 큰 영향을 미친다. 구현에서 Policy 객체는 보험 정책을 나타내고 PointAssignmentRule의 한 구체적 서브클래스의 인스턴스를 향한다. 각 PointAssignmentRule 서브클래스는 운전자에게 점수를 할당하는데 있어 다른 알고리즘을 캡슐화한다. 따라서 이러한 클래스들 중 한 클래스를 인스턴스화함으로써 Policy의 코드를 다양하게 구현할 필요 없이 정책 논리를 다양하게 구현할 수 있다.

관련 패턴

Builder 패턴

이 책을 주의 깊게 살펴본 독자라면 Strategy 패턴의 구조 다이어그램과 Builder (47) 패턴의 구조 다이어그램이 동일한 구조를 가진다는 사실을 눈치챘을 것이다. 우리는 Builder를 Strategy의 특수화 패턴이라고 간주하기도 한다. 단 헬퍼 객체의 기능성과 각 패턴이 언제, 어디서 사용되는지가 다를 뿐이다. Builder 패턴에서 헬퍼 객체는 한 단계씩 Product를 생성하는 업무를 맡고, Director(지시자) 객체가 Builder 에게 하위컴포넌트를 최종 Product에 추가하도록 반복 요청한다. Strategy 패턴에서는 Context를 위한 외부 헬퍼 역할을 하는 Strategy 객체는 어떠한 알고리즘이든 캡슐화하는 것을 목적으로 한다ㅡ꼭 생성 목적이 아니라 런타임 서비스 목적도 가능하다. 다수의 알고리즘이 "메인" Context 객체의 외부에서 캡슐화되고, 각 알고리즘은 객체로서 구체화된다. Strategy 객체는 필요 시 주기적으로 호출되어 완전히 독립된 업무를 1회 수행한다.

추상 팩토리 패턴

Strategy와 Builder 패턴은 구조적으로나 주제상으로 추상 팩토리 (31) 패턴과 연관된다. 후자의 경우, 메인 애플리케이션을 대신해 Product를 생성하기 위해 외부 헬퍼 객체를 불러온다. 선택할 수 있는 팩토리 객체는 다수가 있을 수 있으며 모두 동일한 추상 인터페이스를 제공한다. 따라서 팩토리의 클라이언트는 어떤 팩토리 클래스인지 정확히 알 필요가 없다ㅡ그리고 신경쓰지도 않는다. 특정 타입의 객체를 구성해야 할 경우 최근 팩토리로 일반 메시지를 전송할 뿐이다. 다시 말하지만, 이는 Strategy 패턴의 구조와 비슷하나 객체의 사용 의도, 시기, 장소는 다르다. 추상 팩토리는 Strategy와 같이 주기적 알고리즘 실행보다는 1회 객체 생성에 사용된다.

이러한 패턴들을 구분하는 기준이 때로는 모호함은 틀림없다. Graph Visualizer의 예에서 Grapher Strategy 객체는 제품을 생산한다고 말할 수 있다; 그것이 수신하는 자료를 바탕으로 한 그래프. 전체적으로 Strategy 패턴은 서비스를 구현할 수 있을 때 어떠한 런타임 서비스도 다수의 방법으로 사용할 수 있다.

State 패턴

마지막으로 Strategy 패턴은 State (327) 패턴의 구조와도 비슷하다. 자세한 내용은 State 패턴의 관련 패턴 단락을 참조한다.

Notes

  1. 영상 압축과 관련해 "파일 손실이 있다"는 것은 색상 정보나 픽셀과 같은 일부 그림 자료의 손실을 의미한다.
  2. Car Assembly와 Product Visualization 예제는 Builder (47) 패턴에 정의된 애플리케이션을 바탕으로 한다.