DesignPatternSmalltalkCompanion:Command
COMMAND (DP 233)
의도
요청request 또는 오퍼레이션(동작)을 객체로 캡슐화함으로써 서로 다른 오퍼레이션, 큐, 또는 로그 요청으로 클라이언트를 파라미터화하고, 오퍼레이션의 취소도 가능하게 한다.
구조
논의
때로는 메시지 선택기에 대한 지식이나 심지어 수신자에 대한 지식 없이 일반적인 방식으로 객체로 메시지를 전송해야 할 필요가 있다. 초보자에게는 거의 불가능한 일처럼 들릴지도 모르겠다. 객체가 어떤 메시지를 보낼지 또는 어느 객체에게 보낼지도 모른 채 어떻게 다음 객체에게 메시지를 전달할 수 있을까? 이를 Command 패턴에서 보여주고자 한다.
인터페이스 위젯과의 사용자 상호작용은 이러한 기능이 왜 필요한지 예를 보여준다. 사용자가 버튼 또는 메뉴 항목을 클릭하면 그에 대한 응답으로 무언가 발생한다. 비주얼 스몰토크와 그보다 오래된 디지토크(Digitalk) 환경에서 사용된 간결한 형태의 모델-패인 (model-pane) scheme 또는 모델-뷰-컨트롤러 예제에서는 이러한 위젯들이 (뷰, 패인) 기본 모델이나 애플리케이션에 부착되어 있다. 버튼을 누르거나 메뉴를 선택했을 때 그에 대한 응답의 실행은 애플리케이션의 책임이다. 예를 들어, 사용자가 텍스트 편집기에서 Cut 메뉴 항목을 선택하면 애플리케이션이 최근 선택된 텍스트를 삭제한다. 사용자 인터페이스 위젯들은 사용자 상호작용 이벤트에 대한 응답으로 무엇을 해야 할지 모른다. 또한 이벤트는 애플리케이션 특정적이므로 이벤트가 발생 시 애플리케이션으로 어떤 메시지를 전달해야 할지도 모른다; 어떠한 애플리케이션이든 재사용이 가능하도록 하기 위해 위젯은 일반적 방식으로 구현된다. 그 결과 우리는 비 애플리케이션 특정적 위젯들이 구체적 메시지를 애플리케이션으로 전달할 수 있도록 해주는 메커니즘이 필요하다.
이러한 메커니즘은 Command 패턴을 사용 시 제공된다. 이는 우리가 메시징 요청을 다른 객체가 참조할 수 있는 객체로서 나타내어 객체들에게 전달할 수 있도록 해준다. 이는 하나의 해법이 되는데 스몰토크에서는 어떻게 구현되는지 보이고자 한다. 하지만 일반 위젯이 그들의 모델에서 호출할 수 있는 구체적 기능성에 따른 메커니즘과 관련해 비주얼 스몰토크와 IBM 스몰토크는 또 다른 해법을 제시하는데 이 해법 또한 살펴보겠다.
구현과 예제 코드
Command 객체는 그것이 전달하는 메시지를 하드코딩하거나 런타임 시 파라미터화 시킬 수도 있다. 후자의 경우를 Pluggable Command라 부르겠다. 본 단락에서는 두 가지를 어떻게 구현하는지 논하고 예제로 나타내고자 한다. 또한 Command 패턴을 사용 시 undo-redo 기능을 어떻게 활성화시키는지도 소개하겠다. 위에서 주목한 바와 같이 이벤트가 발생 시 애플리케이션 특정적 메시지를 전송하기 위해 위젯을 구성하는 데에 발생하는 문제에 대한 대안적 접근법 또한 설명하겠다.
하드코딩된 Commands
스몰토크에서 대안 해법을 제공하긴 하지만 [디자인 패턴]에 구현된 Command 패턴은 undo와 redo 기능과 같은 목적에 유용하다. 이는 MacApp와 같은 주요 애플리케이션 프레임워크에도 사용된다 (Schmucker, 1986; Collins, 1995).
우리의 사용자 상호작용 예제로 돌아가자면, Command 객체들은 위젯과 연관된 오퍼레이션을 실행하는 방법을 아는 객체와ㅡ주로 애플리케이션 자체ㅡ스크린 위젯 사이에 위치할 수 있다. 예를 들어, "Cut"이라는 텍스트를 가진 버튼이 위젯이라면 Command 객체의 반대편에는 자르기 오퍼레이션을 어떻게 실행하는지 알고 있는 객체가 있다.
각 위젯은 애플리케이션으로부터 어떠한 Command 인스턴스로 연결할지를 듣는다. Command는 두 가지의 지식, 즉 (1) 메시지를 전달해야 하는 특정 객체와 (Command의 수신자), (2) 요청을 받을 시 객체에게 정확히 어떤 메시지를 보내야 하는지를 안다. 위젯 이벤트가 발생하면 (예: 버튼을 클릭하면) 위젯은 그의 Command 객체로 일반적인 execute 메시지를 단순히 전달한다. 이 시점에서 Command는 다른 메시지를 (또는 메시지들을) 수신자를 향해 간접 지정한다 (MacApp에서는 명령어 객체로 전송되는 메시지가 DoIt이 된다).
따라서 Command는 한 쌍의 수신자-실행을 나타낸다. 수신자는 애플리케이션에 의해 설정되어 Command 객체 내의 인스턴스 변수에서 관리된다. 실행은, execute 메서드에 의해 Command의 수신자로 전송되는 구체적 메시지(들)로부터 도출된다. 그러므로 각 구체적 오퍼레이션마다 구체적 Command 서브클래스가 필요하다: CutCommand 객체는 cut 메시지를 전송할 수 있고, RevertCommand 인스턴스는 selectAll, delete, reopenFile과 같은 메시지들의 시퀀스를 전송함으로써 편집된 파일을 이전에 저장된 상태로 되돌릴 수 있다. CutCommand의 인스턴스를 이용해 Cut 버튼을 애플리케이션과 연결시킬 수 있고, Revert 메뉴 항목은 RevertCommand 객체로 처리할 수 있다.
여기서는 Command가 위젯과 애플리케이션 객체를 어떻게 분리시키고, 어떻게 전자의 일반 메시지가 (execute) 후자에게 수신된 애플리케이션 특정적 메시지로 해석되는지를 보여주는 작은 상호작용 뷰를 소개하겠다:
전체적 그림은 다음 구조 다이어그램의 높은 단계에 나타난다. 애플리케이션은 구체적 Command 인스턴스로 MenuItem 객체들을 구성한다. 사용자가 메뉴 항목을 선택하면 그에 상응하는 MenuItem 인스턴스가 Command 객체로 execute를 전송한다:
이 예제에 해당하는 예제 코드를 살펴보자. 먼저 추상적 클래스 Command를 다음과 같이 정의한다:
Object subclass: #Command
instanceVariableNames: 'receiver'
classVariableNames: ''
poolDictionaries: ''
그리고 인스턴스 생성 클래스 메서드가 필요하다:
Command class>>for: aReceiver
"Create a new instance and set its receiver
to aReceiver."
^self new receiver: aReceiver
인스턴스 변수에 대한 getter-setter 메서드 쌍도 필요하겠다:
Command>>receiver
^receiver
Command>>receiver: anObject
receiver := anObject
Command 객체의 핵심인 메서드는 다음과 같다:
Command>>execute
"I'm an abstract class; subclasses must override this."
self subclassResponsibility
이제 우리는 execute 메서드의 구체적 구현으로 일부 구체적 서브클래스를 정의할 수 있다:
Command subclass: #CutCommand
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
CutCommand>>execute
receiver cut
Command subclass: #PasteCommand
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
PasteCommand>>execute
receiver paste
이 예제에서 execute는 수신측에 메시지 시퀀스를 전송한다:
Command subclass: #RevertCommand
instanceVariableNames: ''
classVariableNaems: ''
poolDictionaries: ''
RevertCommand>>execute
receiver
selectAll;
delete;
reopenFile
따라서 Command의 각 서브클래스는 수신측에 서로 다른 애플리케이션 특정적 메시지를 전송함으로써 자신만의 execute 버전을 구현한다. 하지만 각 오퍼레이션마다 새로운 Command 서브클래스가 필요하다는 사실이 이 접근법의 단점이다. 그렇지만 각 오퍼레이션마다 구체적 클래스가 필요하다는 것은 각 클래스를 execute 오퍼레이션을 undo하는 데에 필요한 정확한 메시지로 프로그램화할 수 있음을 의미한다. 게다가 Command 객체에서 undo 기능을 구현하는 것은 execute가 수신측의 상태를 변화시키는 메시지를 전송하기 전에 그 상태에 대한 정보를 저장할 필요가 있음을 암시한다; 따라서 각 구체적 Command 클래스는 그것의 오퍼레이션을 위해 정확히 어떤 상태 정보를 기억해야 하는지를 알 수 있다.
예를 들어, CutCommand는 cut 메시지를 수신측에 전달하기 전에 최근 선택과 관련된 정보를 저장해야 할 것이다. 그리고 나면 undo를 달성하기 위해 unexecute 메서드를 구현한다 (MacApp에서 undo 메서드의 이름은 UndoIt이다). 다음 예제 코드에서는, unexecute에 필요한 변경을 적용하기 위해 CutCommand 클래스를 리턴한다. 코드에서 우리는 두 가지 단순 가정을 한다: (1) 단일 연속 선택이 있으며, (2) 편집기로 최근 선택내용을 요청 시 문서 내에서 최근 선택사항의 위치에 관한 정보뿐만 아니라 최근 선택된 객체나 실제 텍스트를 포함한 단일 객체를 검색한다 (예: 비주얼 스몰토크에서 TextSelection 객체는 선택된 텍스트와 관련된 몇 가지 정보들, 즉 시작 위치와 종료 위치, 텍스트가 나타나는 텍스트 패인과 관련된 정보를 캡슐화한다):
Command subclass: #CutCommand
instanceVariableNames: 'selectionBeforeCut'
classVariableNames: ''
poolDictionaries: ''
CutCommand>>execute
selectionBeforeCut := receiver currentSelection.
receiver cut
CutCommand>>unexecute
receiver
selectBefore: selectionBeforeCut origin;
paste: selectionBeforeCut text
이제 애플리케이션은 Command를 이용해 버튼과 같은 위젯을 연결할 수 있다:
AnApplication>>open
...
self addSubpane:
(Button new
contents: 'Cut';
command: (CutCommand for: self);
...)
다음 상호작용 다이어그램은 이 패턴에 수반되는 협력관계를 보여준다:
그림설명: 여기서 이벤트가 발생하여 anInvoker에 메서드를 호출함
aReceiver는 aClient와 구분된 객체일 수도 있다는 점과 단일 객체가 두 가지 역할을 할 수도 있다는 점을 주목한다; 이는 클라이언트가 결정해야할 일이며, 명령어 객체의 수신자가 누군지 aComand에게 알리는 일도 클라이언트가 수행한다. anInvoker란 라벨이 붙은 객체는 앞서 설명한 예제의 위젯에 해당하겠다. 우리 다이어그램과 DP 237에 실린 다이어그램에는 언어기반의 차이점이 있다. [디자인 패턴] 편에서는 new Command 코드가 나타난다; 이 코드는 C++ 언어 고유의 인스턴스 생성문을 나타내고 그 결과 aCommand의 인스턴스화가 야기된다. 스몰토크에서 Command 인스턴스 생성은 Command 클래스 객체로 new 메시지를 전송하였을 때 발생한다 (for: 인스턴스 생성 메시지에 대한 Command 클래스의 응답 시 발생).
undo-redo 히스토리 목록
Command 패턴의 한 가지 주요 시사점은, 사용자가 실행한 모든 오퍼레이션의 이력을 유지할 수 있도록 해준다는 점이다. 애플리케이션은 사용자 실행이 발생할 때마다 Command 객체를 히스토리 목록과 연관시킬 수 있다. 이에 우리는 다단계의 undo-redo 기능을 구현하는 수단을 가진다. 오퍼레이션을 undo하기 위해선 히스토리 목록으로부터 이전 Command를 검색하여 unexecute 메시지로 보낸다. 그리고 이미 undo된 오퍼레이션을 redo하고 싶다면 같은 Command 객체를 검색하여 다시 execute로 전송한다. Iterator 패턴에서 우리는 HistoryStream라 불리는 히스토리 목록 클래스의 구현과 상세한 설명을 비롯해, undo에는 후방으로, redo은 전방으로 목록을 순회하는데 필요한 메커니즘을 제공하고 있다. 하지만 Command 패턴에서는 애플리케이션이 undo-redo에 관한 히스토리 목록을 어떻게 이용하는지만 묘사하고자 한다.
우리 애플리케이션 클래스인 AnApplication가 historyList라는 이름의 인스턴스 변수를 정의한다고 가정하자:
AnApplication class>>new
^self basicNew initialize
AnApplication>>initialize
historyList := HistoryStream new.
...
AnApplication>>saveForUndo: aCommand
historyList nextPut: aCommand deepCopy
Command 객체는 수신측 객체로부터 상태에 관한 간략한 정보를 얻을 수 있으며, 정보를 다음에 호출 시 변경될 것이므로 (물론 undo 오퍼레이션을 진행하기 전일 것이다) 우선 객체의 복사본을 저장한다.
saveForUndo: 메서드는 애플리케이션과 Command 객체들 간 어느 정도의 결합도를 암시한다 (Command는 saveForUndo: 메시지를 애플리케이션으로 보내기 위해 애플리케이션에 관해 알아야 한다). Command 클래스 정의에 인스턴스 변수를 추가할 경우, 인스턴스 생성 시 애플리케이션에 관한 지식을 각 Command에 제공할 수 있다. 단 이러한 목적을 달성하기 위해 Command의 receiver 인스턴스를 사용 시 애플리케이션을 가리킬 것인지 여부가 확실치 않으므로 사용해선 안 된다는 사실을 주목한다. 이에 따라 application 변수 및 연관된 setter 메서드를 추가하기 위해 Command 정의로 돌아가 인스턴스 생성 메서드를 변경하고 execute 메서드를 재정의한다:
Object subclass: #Command
instanceVariableNames: 'receiver application'
classVariableNames: ''
poolDictionaries: ''
Command>>application: anObject
application := anObject
Command class>>receiver: aReceiver application: anApplication
"Instance creation."
^self new
receiver: aReceiver;
application: anApplication
Command>>execute
"Here in the abstract superclass we define execute as a
Template Method. First, invoke handleExecute and then
save the Command object in the history list. This way,
subclasses just override handleExecute and get history
list behavior for free."
self handleExecute.
application saveForUndo: self
구체적 서브클래스 Command는 이제 고유의 handleExecute 메서드를 정의한다:
CutCommand>>handleExecute
selectionBeforeCut := receiver currentSelection.
receiver cut
앞서 살펴보았듯이 구체적 서브클래스도 unexecute 메서드에서 실행 특정적 undo 행위를 구현할 수 있다.
Command 클래스를 정의하였으면 애플리케이션의 undo 및 redo 오퍼레이션을 구현할 수 있게 된다:
MyApplication>>undo
| aCommandOrNil |
"If there are no previous Commands to undo, exit:"
(aCommandOrNil := historyList previous) isNil
ifTrue: [^Screen default ringBell].
aCommandOrNil unexecute
AnApplication>>redo
| aCommandOrNil |
"If there are no redo-able Command objects in the
history list, exit:"
(aCommandOrNil := historyList next) isNil
ifTrue: [^Screen default ringBell].
aCommandOrNil handleExecute
redo는 execute 대신 Command에게 handleExecute를 전송한다는 점을 주목한다. Redo의 경우, 히스토리 목록에 새 Command를 추가하려는 것이 아니다; 이러한 작업은 사용자 프로그램이 새로운 오퍼레이션을 실행하면 불러오는 execute에서 이루어진다. 단지 이미 목록에 있는 오퍼레이션을 redo하고 싶을 뿐이다.
주요 애플리케이션 객체에 의해 구현되는 undo-redo을 살펴보았다. 실제로는 이러한 목적을 달성하기 위해 애플리케이션은 구분된 undo 관리자 객체를 사용하기도 한다. 이러한 접근법은 WindowsBuilder와 EFX에서 사용된다 (Alpert et al., 1995). 애플리케이션은 인스턴스 변수가 undo 관리자를 가리키도록 유지하고, 이러한 관리자는 히스토리 목록을 유지하여 사용자가 요청하는 undo-redo 기능을 호출한다.
대체 가능한 Commands
Command의 이전 버전에서는 Command의 수신측으로 전달될 메시지를 각 Command의 execute 메서드에 하드코딩시켰다. 그 결과, 발생할 수 있는 사용자 실행마다 서로 다른 Command 클래스가 필요하다: 자르기(cut)에 하나, 복사하기(copy)에 하나, 붙여넣기(paste)에 하나씩이 필요하다.
하드코딩된 Commands의 C++ 버전에서는 Command의 수신측을 참조하는 인스턴스 변수가 구체적 Command 서브클래스에서 선언된다는 점도 주목한다ㅡOpenCommand 클래스에서는 (DP 239) _application이란 이름을 가지며 Application을 타입으로 가진다; PasteCommand (DP 240)에서는 _document란 이름을 가지며 Document 클래스의 것으로 선언된다. 반면 우리는 추상적 Command 클래스에서 일반 receiver 인스턴스 변수를 선언한다. 강한 타이핑 언어인 C++에서는 Command의 수신측을 구체적 타입으로 결정하길 바라지 않는다; 특정 Command 타입에 따라 변할 수 있다. 스몰토크에서 이러한 문제는 굉장히 간단해진다. 타이핑 없이도 receiver 변수는 런타임 시 어떠한 클래스의 변수든 참조할 수 있다.
[디자인 패턴] 편에서는 C++ 를 중심으로 이러한 까다로운 점을 몇 가지 논하고 있다. C++ 템플릿을 이용해 Command의 수신자를 파라미터화하는 개념 또한 고려한다. 이런 방식으로 우리는 SimpleCommand 단일 클래스를 정의하고 (DP 240-241) 서로 다른 타입의 수신자를 이용해 그 클래스를 인스턴스화할 수 있다. SimpleCommand 클래스는 멤버 함수를 가리키는 인스턴스 변수도 가진다. 따라서 이 변수는 execute의 행위도 파라미터화할 수 있게 해준다. 우리는 클래스를 인스턴스화할 때 포인터를 적절한 메서드로 전달하여 execute가 파라미터화된 메서드를 호출한다.
execute의 행위를 다른 방법으로도 파라미터화시킬 수 있다. Command 객체는 특수화된 역할을 가진 Adapter에 불과하다; 이것은 위젯이 전송한 단일의 일반 메시지 execute 를 애플리케이션 특정적 메시지(들)로 조정한다; Adaptee는 Command의 수신자이다. Adapter 패턴이 사용되었으므로 우리는 단일의 대체 가능한 또는 파라미터화된 Adapter/Command 클래스를 정의하여 클라이언트가 공급한 메시지를 수신자에게 전달하거나 수신자를 argument로 이용함으로써 클라이언트가 공급한 블록을 실행할 수 있다. 이 두 가지 방법은 Adapter (105) 패턴에서 이미 논한 바 있으므로, 아래에서는 스몰토크 중심의 대체 가능한 메시지-전달 Command 버전을 소개하고자 한다.
하지만 실행 특정적인 하드코딩된 Command 구현의 이점은ㅡ각 사용자 실행마다 구체적 클래스를 가진ㅡ이러한 클래스들이 undo 오퍼레이션에 관련된 저장에 필요한 수신자 상태와 정보를 정확히 알고 있다는 점이다. 예를 들어, 자르기 오퍼레이션은 붙여넣기와 다른 상태를 저장해야 한다. 일반적으로 행하는 방식을 알 방법이 없기 때문에 Command를 undo-redo 기능에 참여시키고 싶다면 대체 가능한 Command를 사용해선 안 되겠다.
스몰토크 중심의 대체 가능한 명령어. 여기서는 오퍼레이션별로 하나의 타입 대신 하나의 일반적 타입의 Command 객체를 구현하기 위한 또 다른 접근법을 제시하고자 한다. 이 Command는 Pluggable Adapter와 같은 수신자에게 단일 메시지 선택기를 전달할 수 있도록 해주어, Pluggable Adapter가 하지 않을 법한 일, 즉 전달된 메시지를 비롯해 전달되는 argument를 우리가 기억할 수 있도록 해준다.
Object subclass: #PluggableCommand
instanceVariableNames: 'receiver selector arguments'
classVariableNames: ''
poolDictionaries: ''
각 인스턴스 변수 당 접근자 메시지를 이미 썼다고 가정해보자. 인스턴스 생성은 다음과 같이 작성될 것이다:
Command class>>receiver: anObject
selector: aSymbol
arguments: anArrayOrNil
"Instance creation."
^self new
receiver: anObject;
selector: aSymbol;
arguments: (anArrayOrNil isNil
ifTrue: [#()]
ifFalse: [anArrayOrNil])
이후 애플리케이션은 PluggableCommand를 인스턴스화하고 위젯으로 메울 것인데, 이는 앞서 보인 예제와 거의 비슷하다. 차이점이라면 애플리케이션이 Command의 수신측 뿐 아니라 선택기까지 명시할 것이며, 선택적으로는 argument도 명시할 수 있다는 점이다:
AnApplication>>open
| command |
...
command := PluggableCommand
receiver: self
selector: #cut
arguments: (Array with: myTextPane).
self addSubpane:
(Button new
contents: 'Cut';
command: command;
...)
이제 위젯이 일반 실행 메시지를 Command 객체로 전송하면, 다음과 같은 코드가 호출된다:
PluggableCommand>>execute
"Answer the result of sending the selector to
the receiver along with any arguments."
^self receiver
perform: self selector
withArguments: self arguments
방금 여기서 정의한 클래스는 실제적으로 기존 비주얼 스몰토크의 Message 클래스와 동일하다. (IBM 스몰토크와 비주얼웍스에서 Message 클래스는 selector와 argument에 상응하는 두 개의 인스턴스 변수만 포함한다. 하지만 그들의 서브클래스는 비주얼 스몰토크의 Message클래스와 같이 receiver 변수를 추가한다. IBM 스몰토크에서 이 서브클래스의 이름은 DirectedMessage이고, 비주얼웍스에서는 MessageSend라는 이름이다. 비주얼 스몰토크의 Message 인스턴스의 경우, execute 메시지의 실제적 이름은 perform이고, IBM의 DirectedMessage는 send이며, 비주얼웍스의 MessageSend 클래스는 value이다. 다음 논의에서는 비주얼 스몰토크 구현에서와 같이 Message 클래스만 언급할 것이다.) Message 인스턴스들은 객체가 다른 객체에게 전송하는 메시지의 구체화이다. Message와 perform: 은 매우 일반적인 방식으로 어떤 메시지든 객체로 생성, 보관, 전송할 수 있게 해준다. 따라서 스몰토크에서는 Command 객체의 계층구조를 하나의 파라미터화된 Message 개체로 대체하는 것이 가능하다.
이벤트 기반의 대화형 애플리케이션
위와 같이 Command 패턴을 구현 시 한 가지 단점은 하나의 오퍼레이션으로만 연관이 가능하기 때문에 각 위젯별로 하나의 명령어만 가능하다. 메뉴 항목에서는 아무 문제가 없다. 항목을 클릭하면 애플리케이션에서 하나의 구체적 오퍼레이션만 호출하길 원하기 때문이다. 하지만 많은 위젯들은 왼쪽 버튼 마우스 클릭이나 오른쪽 버튼 클릭, 더블 클릭, 위젯 위로 마우스 이동과 같은 다수의 이벤트와 연관된다. 우리는 각 위젯에 대해 메시지-수신자 쌍을 이러한 이벤트들과 연관할 수 있길 원한다. IBM 스몰토크와 비주얼 스몰토크에서는 이를 허용한다. 두 접근법 모두 Observer 패턴에서 상세히 논한 바 있다 (비주얼 스몰토크에서의 사용은 314페이지의 SASE를 참조하고, IBM 스몰토크에서의 사용은 317페이지의 SASE를 참조한다); 여기서는 비주얼 스몰토크의 구현에서 기본이 되는 개념들을 간단히 살펴보겠다.
모든 비주얼 스몰토크 패인(pane)은 그와 관련된 다수의 이벤트를 갖는다. 간단한 예로, 사용자가 RadioButton를 켜기 위해 이를 클릭하면 이것은 하나의 이벤트를 생성하고, 버튼이 꺼질 때 또 다른 이벤트를 생성한다. 대화형 애플리케이션은 윈도우 창을 빌드할 때 각 패인에게 구체적 이벤트가 발생 시 어떠한 일을 해야 하는지를 말해준다. 기본적으로 이 애플리케이션은 Message를ㅡ메시지 선택기, 수신자, 선택적으로 argument도 포함됨ㅡ이러한 이벤트 각각과 연관시킨다. (사실 Message 자체를 제외하고 다른 대안책도 있지만ㅡ블록을 사용할 수도 있음ㅡ여기서는 평이한 바닐라 메시지 case를 논하고자 한다.)
애플리케이션이 윈도우 창을 빌드할 때는 구체적 이벤트가 발생 시 무슨 일을 해야 하는지 각 위젯에게 알려준다. 스크린 인터페이스에 고객이 사용 중인 신용카드를 보여주는 라디오 버튼 집합이 포함되어 있는 POS(판매시점관리) 애플리케이션이 있다고 가정하자:
PointOfSale>>open
...
self addSubpane:
(RadioButton new
contents: 'American Express';
when: #TurnedOn send: #americanExpressPicked to: self;
...
when:send:to: 메시지는, "당신의 turnedOn 이벤트가 발생하면 나에게 americanExpresspicked 메시지를 전송해주세요"라는 의미를 가진다. 모든 패인은 handlers라는 이름의 인스턴스 변수를 가지는데, 이는 이벤트 명을 키로 가지며 그에 상응하는 값은 이벤트가 발생 시 취할 행동을 나타내는 사전이다. when:send:to: 메시지와 그 변형체는 이 사전에 엔트리를 구성한다. when: argument는 이벤트 명을 (키), send:to: 파라미터들은 연관된 Message 객체에 대한 수신자와 선택기를 명시한다 (값). 이벤트가 발생하면 handlers에서 연관된 Message가 검색되어 실행하라는 명령을 한다.
when:send:to:의 변형체에는 다수가 있는데 가장 주목할 만한 것은 when:send:to:with: 으로서, 여기서는 with: 파라미터가 이벤트가 발생 시 메시지와 함께 전송될 argument를 (또는 다수의 argument들을) 명시한다. (또 다른 변형체는 이벤트가 발생 시 평가해야 할 블록을 명시하도록 해주기도 하지만 명확성을 위해 여기서는 Message 버전에 초점을 두고자 한다.)
이 접근법에서 요점은 다음과 같다:
- 우리는 Message를 Command 객체로 사용하여 애플리케이션 객체와 사용자 인터페이스 위젯을 연결해주는 일반 메커니즘을 제공할 수 있다ㅡ좀 더 구체적으로 말해, 애플리케이션의 선택에 따른 애플리케이션 객체나 다른 수신자로 전송된 메시지와 각 패인 이벤트를 연결하기 위한 목적이다. 이 메시지는 argument를 포함할 수도 있다.
- 이 접근법은 다수의 이벤트를 각 위젯과 연관시키고 이러한 이벤트 각각을 서로 다른 Command와 연관되도록 허용하기 때문에 실제적 이벤트 구동의 대화형 애플리케이션을 구축하도록 해준다.
- 위젯마다 다수의 이벤트가 있으므로 이벤트-실행의 매핑을 모두 추적하기 위한 메커니즘도 필요하겠다. 비주얼 스몰토크에서 이러한 구현은 테이블 위주이며, 이벤트 명을 키로 한 위젯에 사전이 딸려 있다.
알려진 스몰토크 사용예
비주얼 스몰토크와 IBM 스몰토크의 대화형 애플리케이션
위에서 나타낸 바와 같이 비주얼 스몰토크에 구축된 모든 사용자 인터페이스 애플리케이션들은ㅡ즉, 대화형 윈도우 창을 통합하는 애플리케이션들ㅡ위젯별로 이벤트 구동의 다수의 Command를 사용한다. 메시지 세부 내용은 서로 다르지만 IBM 스몰토크는 비슷한 접근법을 취한다. 비주얼 스몰토크에서와 같이 IBM 스몰토크 애플리케이션들도 특정 이벤트가 발생 시 각 위젯에게 어떤 애플리케이션 특정적 메시지를 보내야 하는지를 말해주고, 각 위젯마다 다수의 이벤트가 연관된다 (일부 이벤트는 모든 위젯에게 공통되며, 일부는 위젯마다 다르다). 따라서 당신은 위젯마다 다수의 Command를 구성할 수 있다.
비주얼 스몰토크 MenuItems
비주얼 스몰토크에서는 MenuItem 인스턴스가 Command 객체처럼 행동한다. 다음과 같은 모양의 풀다운 메뉴가 있다고 가정하자:
이 메뉴는 다양한 방법으로 구축할 수 있으며, 아래에 한 예를 들어보겠다:
AnApplication>>buildEditMenu
| menu |
menu := Menu new.
menu
appendItem: 'Cut' selector: #ut;
appendItem: 'Copy' selector: #copy;
appendItem: 'Paste' selector: #paste;
appendSeparator;
appendItem: 'Revert' selector: #revert;
owner: self;
title: 'Edit'.
^menu
이로 인해 4개의 MenuItem 객체를 포함하는 Menu가 생긴다. 각 MenuItem 객체는 선택기를 참조하는 인스턴스 변수를 하나씩 갖고 있다; 사용자가 메뉴 항목을 선택하면 메시지 선택기가 Menu의 소유자에게 전달된다 (perform: 을 이용하여). 따라서 사용자가 Cut 항목을 선택하면 cut 메시지가 AnApplication에게 전송된다.
위를 대신해 Message 객체를 다른 메뉴 항목과 연관시킬 수도 있다. 따라서 우리는 MenuItem을 설정하여 원하는 수신자에게 메시지를 전송하고 argument를 포함할 수도 있다. 예를 들어, buildEditMenu에 다음을 추가할 수도 있다:
menu
appendItem: 'Insert Date'
action:
(Message
receiver: myTextPane
selector: #paste:
arguments: (Array with: Date today printString)).
지연된 데이터베이스 업데이트
우리 중 한 명이 Command 패턴을 이용해 관계형 데이터베이스의 사용자 변경의 목록을 유지함과 동시 지연된 데이터베이스 배치 업데이트를 개별 트랜잭션으로서 실행시키고자 하였다. 애플리케이션은 사용자로 하여금 디스크로부터 데이터베이스 스키마를 검색할 수 있게 해주었고, 결국 스몰토크 내부표현으로 변환되었다. 사용자는 컬럼의 이름을 바꾸거나, 컬럼의 삭제 및 추가 등을 할 수 있는데, 이러한 오퍼레이션은 스몰토크의 스키마 표현만을 변경할 것이다 (디스크 상의 데이터베이스가 아니라). 각 변경은 히스토리 목록에 명령어 객체로서 기록되었다. 사용자 오퍼레이션의 이력으로부터 변경을 취소 및 redo할 수 있었지만 스몰토크의 데이터베이스의 "복사본"만 변경되었다. 사용자 상호작용이 끝날 무렵 명령어 객체는 오퍼레이션에 상응하는 적절한 SQL 코드를 생성하여 실제 물리적 데이터베이스 스키마에 코드를 실행할 것이다. 따라서 Command 패턴을 사용하여 변경 내용을 저장하고 모든 수정내용이 완결될 때까지 적용을 지연하였다.
객체지향 데이터베이스에서의 undo
또 다른 프로젝트에서 우리 중 한 명이 위와 비슷한 목적으로 다시 Command 패턴을 이용했다. 여기서 애플리케이션은 undo 기능을 구현해야 하는 특수화된 편집기를 포함했다; 문제는 객체에 일어나는 모든 변경 내용이 애플리케이션의 객체지향 데이터베이스에 즉시 업데이트되어 취소가 (undo 기능을 위한) 쉽지 않다는 점이었다. 이를 대신히 우리는 사용자가 객체를 편집하기 시작할 때 객체의 깊은 복사를 진행하였다. 모든 편집 변경은 복사본에 적용되어 명령어의 히스토리 목록에 저장되었다. 이번 경우, 명령어 객체들은 기본적으로 편집기에 의해 객체 복사본으로 전송된 메시지를 기록하는 Message 객체들이다. 사용자가 편집을 완료하면 변경 내용을 그대로 고수할 수 있고 파기할 수 있다. 변경 내용을 적용하고 싶을 경우 히스토리 목록에 저장된 메시지가 원본 객체에 재적용된다; 취소할 경우에는 객체 복사본은 버려지고 변경되지 않은 원본 객체가 남아 있다.
font-size:15px}WindowBuilder
ObjectShare Systems의 사용자 인터페이스 빌더인 WindowBuilder는 Command 패턴의 변형체와 함께 undo-redo 히스토리 목록을 사용한다. 사용자가 인터페이스 빌더 윈도우 창에서 오퍼레이션을 실행하면 WindowBuilder 객체는 WBUndoManager에서 WBUndoAction 인스턴스를 히스토리 목록에 추가한다. WBUndoAction은 대체가 가능하다. WBUndoAction 객체들은 실행을 위한 문(statement)들을 하드코딩하는 대신 클라이언트가 undo 및 redo 행동을 모두 수행하는 코드를 공급할 수 있게 해준다. 각 실행마다 단일 선택기를 저장하는 대신 두 가지를 모두 공급하여 블록으로서 보관된다.
관련 패턴
Adapter
Tailored Adapter (106) 패턴은 Command 패턴의 첫 번째 구현과 (하드코딩된 Commands 단락에서 설명한 바와 같이) 꽤 비슷하다. Tailored Adapter는 서로 다른 두 개의 객체 사이에 위치하며, 그 중 한 객체로부터 나머지 객체로 전달되는 메시지를 어떻게 해석하는지를 알고 있다. 다음 상호작용 다이어그램에서 나타나는 Command 객체와 상당히 흡사하다:
이러한 Command의 구현은 execute 메시지를 어떻게 해석해야 하는지를 알고 있는 Tailored Adapter패턴의 구현으로 간주할 수 있다. Command 객체에서 변환된 메시지를 전송 받는 "Adaptee"는 receiver 변수에서 유지된다. Tailored Adapter와 하드코딩된 Command 패턴에서는 전달될 메시지가 하드코딩된다 (Command의 execute 메서드와 Adapter의 value, value: 메서드). 이 두 개의 패턴은 서로 경쟁적인 패턴이라기보다는 Command 패턴에 Adapter 패턴이 사용된다고 보는 편이 옳다.
우리의 대체 가능한 Command는 기본적으로 대체 가능한 Adapter (107) 패턴에 해당한다. Command 객체가 execute 메시지를 수신 시 "Adapter" 로 (receiver) 어떤 메시지를 전달할지를 Command 객체에게 말해준다. 이 객체는 전송될 메시지를 내부적으로 보관하므로 위에서 언급한 하드코딩된 패턴 대신 일반적인 해석 메커니즘을 구현한다.
Observer
앞서 설명한 비주얼 스몰토크의 이벤트 구동식 메커니즘은 Observer 패턴의 (305) 변형체이다. 이것은 위젯이 다른 객체들에게 구체적 이벤트, 또는 언제 변경되는지를 알려주길 원할 때 사용된다.