DesignPatternSmalltalkCompanion:State: Difference between revisions

From 흡혈양파의 번역工房
Jump to navigation Jump to search
mNo edit summary
(큰따옴표 수정)
 
Line 11: Line 11:
===논의===
===논의===


많은 경우에서, 객체의 행위는 자신으로 구성된 요청에 응해 시간이 지나면서 변할 것이다. 객체 수명의 “히스토리”와 그 히스토리에서 객체의 현재 시점은 (즉 현재 상태) 객체가 지금 어떻게 행동하는지에 한몫 한다.  
많은 경우에서, 객체의 행위는 자신으로 구성된 요청에 응해 시간이 지나면서 변할 것이다. 객체 수명의 "히스토리"와 그 히스토리에서 객체의 현재 시점은 (즉 현재 상태) 객체가 지금 어떻게 행동하는지에 한몫 한다.  


아래와 같은 모습의 코드는 이미 살펴보았다:
아래와 같은 모습의 코드는 이미 살펴보았다:
Line 122: Line 122:


# 상태 클래스가 사용하길 원하는 모든 인스턴스 변수는 슈퍼클래스에서 정의되어야 한다. 슈퍼클래스가 추가 인스턴스 변수를 모두 정의하지 않았다면 changeClassToThatOf: 메소드는 작동하지 않을 것이다. 이의 변형체는 changeClassToThatOf: 가 아니라 become: 를 사용할 것이다. 그러나 become: 을 전송하기 전에 클래스 내 모든 인스턴스 변수의 값을 새로운 인스턴스로 복사하는 작업이 필요해진다. changeClassToThatOf: 는 비주얼 스몰토크나 IBM 스몰토크에 존재하지 않으므로 become: 접근법이 유일한 선택권이 된다.  
# 상태 클래스가 사용하길 원하는 모든 인스턴스 변수는 슈퍼클래스에서 정의되어야 한다. 슈퍼클래스가 추가 인스턴스 변수를 모두 정의하지 않았다면 changeClassToThatOf: 메소드는 작동하지 않을 것이다. 이의 변형체는 changeClassToThatOf: 가 아니라 become: 를 사용할 것이다. 그러나 become: 을 전송하기 전에 클래스 내 모든 인스턴스 변수의 값을 새로운 인스턴스로 복사하는 작업이 필요해진다. changeClassToThatOf: 는 비주얼 스몰토크나 IBM 스몰토크에 존재하지 않으므로 become: 접근법이 유일한 선택권이 된다.  
# 더 크고 중요한 문제는 State 패턴을 사용 시 가능한 “Context” 객체의 서브클래스 생성을 못하게 만들었다는 점이다. 서브클래스를 만들고 싶었다면ㅡ예로, 여태까지 가장 길게 실행된 인스턴스 변수를 추가하기 위해ㅡ강제로 State-Context 클래스의 모든 서브클래스에 추가로 서브클래스를 만들어야 했을 것이다. 이러한 클래스 폭발(explosion)은 State 패턴에서 반영적 기능 대신 composition을 사용하는 가장 큰 원인이다.
# 더 크고 중요한 문제는 State 패턴을 사용 시 가능한 "Context" 객체의 서브클래스 생성을 못하게 만들었다는 점이다. 서브클래스를 만들고 싶었다면ㅡ예로, 여태까지 가장 길게 실행된 인스턴스 변수를 추가하기 위해ㅡ강제로 State-Context 클래스의 모든 서브클래스에 추가로 서브클래스를 만들어야 했을 것이다. 이러한 클래스 폭발(explosion)은 State 패턴에서 반영적 기능 대신 composition을 사용하는 가장 큰 원인이다.


이번 사례에서는 언어 특정적 기능의 사용이 유연성을 추가하지는 않는다는 사실을 살펴보았다; 대신 디자인을 덜 유연하고 재사용이 힘들게 만든다. 이러한 점은 객체지향 프로그래밍을 설계 시 유념해야 하는 중요한 점이다; “가장 깔끔한” 또는 “가장 매끄러운” 해법이라고 해서 최선의 해법은 아니다. 때로는 가장 간단한 해법이 특별한 특징을 사용하는 해법보다 낫다.  
이번 사례에서는 언어 특정적 기능의 사용이 유연성을 추가하지는 않는다는 사실을 살펴보았다; 대신 디자인을 덜 유연하고 재사용이 힘들게 만든다. 이러한 점은 객체지향 프로그래밍을 설계 시 유념해야 하는 중요한 점이다; "가장 깔끔한" 또는 "가장 매끄러운" 해법이라고 해서 최선의 해법은 아니다. 때로는 가장 간단한 해법이 특별한 특징을 사용하는 해법보다 낫다.  


====활용성====
====활용성====
Line 342: Line 342:
====SMTP/Sockets====
====SMTP/Sockets====


“매우 단순한 VSE SMTP Client” (ParcPlace, 1996)에서는 통신 프로토콜에 명시적 유한 상태 기계(FSM)를 구현하는 State 패턴의 교과서적 사용예제를 보여준다. 이번 본문에서는 SMTP (간이 전자 우편 전송 프로토콜) 메일 시스템을 비주얼 스몰토크 엔터프라이즈에서 구현하는 방법을 보이고자 한다. 이는 SMTP 상태를 State 클래스로 나타내는데 이 클래스는 SocketClient Context 클래스와 협력하여 TCP/IP socket을 통해 SMTP 서버로 메시지를 전송한다.
"매우 단순한 VSE SMTP Client" (ParcPlace, 1996)에서는 통신 프로토콜에 명시적 유한 상태 기계(FSM)를 구현하는 State 패턴의 교과서적 사용예제를 보여준다. 이번 본문에서는 SMTP (간이 전자 우편 전송 프로토콜) 메일 시스템을 비주얼 스몰토크 엔터프라이즈에서 구현하는 방법을 보이고자 한다. 이는 SMTP 상태를 State 클래스로 나타내는데 이 클래스는 SocketClient Context 클래스와 협력하여 TCP/IP socket을 통해 SMTP 서버로 메시지를 전송한다.


====ControlMode====
====ControlMode====


비주얼웍스는 DrawingController (DP 313)와 비슷한 State 패턴의 예제를 포함한다. 비주얼웍스에서는 UI Builder 프레임워크가 클래스 ModalController를 사용하여 서로 다른 UI 컴포넌트의 포지셔닝 및 리사이징을 처리한다. ModalController는 “후에 어떤 실행을 하게 될 ControlModel 객체를 내부에 보유하기 위해 마우스 관련 이벤트의 방향을 전환한다.<ref name="주석5장3">비주얼웍스 ModalController 클래스 코멘트.</ref>
비주얼웍스는 DrawingController (DP 313)와 비슷한 State 패턴의 예제를 포함한다. 비주얼웍스에서는 UI Builder 프레임워크가 클래스 ModalController를 사용하여 서로 다른 UI 컴포넌트의 포지셔닝 및 리사이징을 처리한다. ModalController는 "후에 어떤 실행을 하게 될 ControlModel 객체를 내부에 보유하기 위해 마우스 관련 이벤트의 방향을 전환한다."<ref name="주석5장3">비주얼웍스 ModalController 클래스 코멘트.</ref>
ModalController는 “일부 또는 모든 ModalController의 기본적 제어 순서를 구현하는” ControlMode 서브클래스의 인스턴스를 포함한다 (ValueHolder의 중재자를 통해).<ref name="주석5장4">비주얼웍스 ControlMode 클래스 코멘트.</ref>
ModalController는 "일부 또는 모든 ModalController의 기본적 제어 순서를 구현하는" ControlMode 서브클래스의 인스턴스를 포함한다 (ValueHolder의 중재자를 통해)."<ref name="주석5장4">비주얼웍스 ControlMode 클래스 코멘트.</ref>


ModalController/ControlMode 조합과 [디자인 패턴]편에 소개된 HotDraw 예제는 모두 전이를 제공하는데 있어 명시된 FSM가 없다는 점에서 드문 예제라고 할 수 있다. 대신 사용자는 가능한 상태 리스트로부터 선택함으로써 다음 상태를 결정할 수 있다. 이는 State들이 상태 전이를 통제할 수 없는 예제이므로 Context가 스스로 통제해야만 한다.  
ModalController/ControlMode 조합과 [디자인 패턴]편에 소개된 HotDraw 예제는 모두 전이를 제공하는데 있어 명시된 FSM가 없다는 점에서 드문 예제라고 할 수 있다. 대신 사용자는 가능한 상태 리스트로부터 선택함으로써 다음 상태를 결정할 수 있다. 이는 State들이 상태 전이를 통제할 수 없는 예제이므로 Context가 스스로 통제해야만 한다.  

Latest revision as of 07:39, 20 January 2013

STATE (DP 305)

의도

객체의 내부 상태에 따라 행위를 변경할 수 있게 한다. 이에 따라 객체는 마치 클래스를 바꾸는 것처럼 보인다.

구조

Dpsc chapter05 State 01.png

논의

많은 경우에서, 객체의 행위는 자신으로 구성된 요청에 응해 시간이 지나면서 변할 것이다. 객체 수명의 "히스토리"와 그 히스토리에서 객체의 현재 시점은 (즉 현재 상태) 객체가 지금 어떻게 행동하는지에 한몫 한다.

아래와 같은 모습의 코드는 이미 살펴보았다:

Order>>submit
	self state == #created
		ifTrue:[^self error: 'Must be validated first'].
	self state == #validated
		ifTrue:[OrderProcessor submitOrder: self.
			^self state: #submitted].
	self state == #submitted
		ifTrue:[^self error: 'Can' 't submit this order twice'].
	self state == #deleted
		ifTrue:[^self error: 'Order has been deleted'].

Order 객체의 행위는 주로 최근 상태에 따라 좌우되지만 case 문에 대한 부족한 대체자(poor substitute)에 의해 결정된다. 여러 번의 검사로 인해 #created 코드 대신 각 상태에 불필요한 코드가 실행되고, 새로운 상태를 처리하기 위해 이 코드를 확장하려치면 상태는 악화된다. 더 중요한 것은 다른 Order 메소드에 동일한 종류의 코드를 가진다는 점이다. 따라서 어떤 특정 상태건 전체적 Order의 행위를 이해하거나 수정을 시도하는 것은 큰 업무이다. 이는 상태 종속적 행위의 전체그림을 조립하기 위해 다수의 case-like 메소드를 꼼꼼하게 추리고 변경하여 코드를 적절한 위치에서 잇는 작업을 요구한다. 따라서 이 접근법을 프로그램의 이해, 관리, 확장성 관점에서 보면 문제가 많다.

상태 전이 는 우리 submit 예제에서와 같이 메소드에 암호화된다는 점도 주목한다. 많은 디자인 메소드가 객체의 상태와 조건, 그 상태들 간 전이를 유발하는 이벤트, 전이 시 발생하는 실행을 나타내는 상태 전이 다이어그램을 통해 객체 행위의 변화를 표현한다. 디자인 다이어그램은 이러한 정보를 정확히 요약하고 있으면서도 이전 예제에서와 같은 코드로 해석 시 명료성(clarity)이 상실되곤 한다.

State 패턴은 코드에 이러한 명료성을 다시 부여한다. State 패턴을 사용 시 각 상태는 구분된 객체로서 표현된다. Client는 스스로를 시작상태 객체로 초기화한다. Client는 새로운 상태로 전이하기 위해 자신의 상태 객체를 적절한 새로운 객체로 대체한다. Client가 결정을 내리거나 그 상태를 바탕으로 행위를 제공해야 하는 경우, State 객체에 위임을 통해서 수행한다. 이러한 방식을 통해 Client는 협력하는 상태 객체들에게 상태를 위임하기 때문에 상대적으로 상태 의존성으로부터 자유롭게 유지된다.

State 패턴에서 핵심은 클라이언트의 상태 의존적 행위를 구분된 State 객체 집합으로 분리시키는 것이다. State 객체들은 클라이언트 자체보다는 클라이언트의 현재 상태에 의존하는 시스템의 행위 일부를 구현한다. State 객체들은 각 슈퍼클래스가 개별적 상태를 표현하는 계층구조에서 정의된다. 클라이언트는 Context 클래스에서 구현되는데 이는 State 계층구조와 협력한다. Context가 상태 의존적인 요청을 처리해야 하는 경우, 그것은 요청을 최근 State로 위임한다. 각 State 서브클래스는 요청을 서로 다르게 처리한다.

클래스의 변경이 아니다

이 패턴의 의도 단락에서는 객체의 내부 상태가 변화하면 마치 클래스를 바꾸는 것처럼 보인다고 설명한다. 일부 개발자들은 문제를 이러한 방식으로 바라보길 선호할지 모르지만 실은 객체가 내부 상태에 의존하는 행위가 많을 뿐이다ㅡ너무 많아서 상태가 변하면 서로 다르게 행동한다. 이러한 차이는 극히 달라서 객체는 그 타입에 따라 완전히 다른 특수화처럼 같이 행동하지만 State 패턴의 모든 예제가 이렇게 극적이진 않다.

기본 문제는 객체가 클래스를 바꾸는 것처럼 보이는 것이 아니라 객체의 외부 행위가 내부 상태에 대단히 의존한다는 사실이다. 대부분 객체들의 내부 상태는 변화가 가능하고 그들의 행위에 영향을 미칠 수 있다. State 패턴과 차이점은 Context에 잘 정의된 상태의 수가 제한되어 있고, 이러한 상태들 간 전이를 통제하는 일관된 규칙 집합을 가진다는 점이다.

코드 변환

State 패턴을 적용 시 꽤 일관성 있는 코드 변환을 야기한다. 패턴을 사용하지 않았다면 상태에 의존하는 행위가 case문을 사용하여 그에 따라 행위를 다양하게 표현하는 것이 보통이다:

Object subclass: #Context
	instanceVariableNames: 'state'
	classVariableNames: ''
	poolDictionaries: ''

Context>>request
	"Handle the request according to the Context's state."
	self state == #stateA ifTrue: [^self stateARequest].
	self state == #stateB ifTrue: [^self stateBRequest].
	...
	^self error: 'unknown state'

[Smalltalk Companion] 편에서는 case문의 유해한 사용예와 이를 사용해선 안 되는 이유를 설명한다. 이번 본문에서는 case문의 기본적 문제 두 가지를 논하고자 한다:

  1. 클라이언트가 Context를 만들 가능성이 있는 public 요청마다 동일한 기본 case 구조를 복제해야 한다. 그러한 request 요청은 #stateA, #stateB 등을 검사해야 한다.
  2. 모든 request 요청을 변경하지 않고는 새로운 상태를 추가할 수 없다.

State 패턴은 코드를 변환하여 Context로부터 상태 의존정 코드를 제거한다. 각 요청을 처리하는 다양한 방법들이 다형적으로 구현된다. Context는 여전히 그 state를 알고 있지만 case문을 통해서 실행되는 대신 Context가 자신의 state에게 실제 객체로서 위임한다:

Object subclass: #Context
	instanceVariableNames: 'state'
	classVariableNames: ''
	poolDictionaries: ''

Context>>request
	"Handle the request according to the Context's
	currentState."
	self state handleRequest

state 객체는 가능한 상태로 구성된 계층구조에서 나온다. 추상 클래스는 모든 상태가 갖고 있는 인터페이스를 정의한다:

Object subclass: #State
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

State>>handleRequest
	self error: 'Transition not allowed for this state'.

State의 서브클래스들은 Context가 위치할 수 있는 서로 다른 상태를 구현한다. 각 서브클래스는 handleRequest를 구현하여 그 상태에서 요청을 처리하기에 적절한 행위를 제공한다. 특정 상태가 요청을 처리할 수 없을 경우 슈퍼클래스의 기본 구현이 그 결과에 적절한 오류를 발생시킨다:

State subclass: #ConcreteStateA
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

ConcreteStateA>>handleRequest
	"Handle the request one way."
	...

State subclass: #ConcreteStateB
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

ConcreteStateB>>handleRequest
	"Handle the request another way."
	...

다른 서브클래스들은 다른 상태들을 구현한다. 각 서브클래스는 그 상태에 대한 행위를 캡슐화시켜 Context 나 다른 상태들을 채우지 않는다. 추후 새로운 상태를 추가하기란 쉽다. 주요 단계는 handle 메시지를 적절히 구현하는 새로운 State 서브클래스를 추가하는 것이다.

Context 클래스를 실제로 변경하기

State 패턴의 핵심이 객체의 클래스가 마치 변경된 것처럼 보이도록 만드는 것이라면, 다른 클래스에 실제로 인스턴스를 강제로 넣는 것은 어떨까? 스몰토크에서는 정체성이나 객체의 클래스를 변경하는 메시지로 가능한 일들이, reflection 기능이 없는 C++와 같은 언어에서는 오히려 복잡한 수단을 사용하는 경향이 (거의 필수적) 있다. 모든 방언에서 우리는 become: 메소드를 사용하여 객체의 정체성을 변경할 수 있다. 비주얼웍스에서는 그의 클래스를 변경하기 위해 changeClassToThatOf: 메소드를 사용할 수도 있다.

언뜻 보기에는 State 패턴이 이 불필요한 복잡성의 인스턴스로 보일지도 모른다. 그냥 클래스를 바꾸면 안될까? changeClassToThatOf: 또는 다른 강압적 메소드를 사용 시 기본적으로 두 가지의 문제가 있다:

  1. 상태 클래스가 사용하길 원하는 모든 인스턴스 변수는 슈퍼클래스에서 정의되어야 한다. 슈퍼클래스가 추가 인스턴스 변수를 모두 정의하지 않았다면 changeClassToThatOf: 메소드는 작동하지 않을 것이다. 이의 변형체는 changeClassToThatOf: 가 아니라 become: 를 사용할 것이다. 그러나 become: 을 전송하기 전에 클래스 내 모든 인스턴스 변수의 값을 새로운 인스턴스로 복사하는 작업이 필요해진다. changeClassToThatOf: 는 비주얼 스몰토크나 IBM 스몰토크에 존재하지 않으므로 become: 접근법이 유일한 선택권이 된다.
  2. 더 크고 중요한 문제는 State 패턴을 사용 시 가능한 "Context" 객체의 서브클래스 생성을 못하게 만들었다는 점이다. 서브클래스를 만들고 싶었다면ㅡ예로, 여태까지 가장 길게 실행된 인스턴스 변수를 추가하기 위해ㅡ강제로 State-Context 클래스의 모든 서브클래스에 추가로 서브클래스를 만들어야 했을 것이다. 이러한 클래스 폭발(explosion)은 State 패턴에서 반영적 기능 대신 composition을 사용하는 가장 큰 원인이다.

이번 사례에서는 언어 특정적 기능의 사용이 유연성을 추가하지는 않는다는 사실을 살펴보았다; 대신 디자인을 덜 유연하고 재사용이 힘들게 만든다. 이러한 점은 객체지향 프로그래밍을 설계 시 유념해야 하는 중요한 점이다; "가장 깔끔한" 또는 "가장 매끄러운" 해법이라고 해서 최선의 해법은 아니다. 때로는 가장 간단한 해법이 특별한 특징을 사용하는 해법보다 낫다.

활용성

[디자인 패턴]편에서는 State 패턴의 적절성에 대한 고려사항을 제시하는데 다음 사항들도 포함된다:

  • Context는 발생할 수 있는 상태에 대한 잘 정의되고 한정된 리스트를 가져야 한다. 여느 객체와 마찬가지로 다른 내부 상태 변수들도 가질 수 있다. 단 최근 상태를 정의하는 상태에는 소수의 유효 값만 가질 수 있으므로 그러한 값들 각각은 State 서브클래스로서 구현이 가능하다. 상태 값이 사실상 무제한 수의 유효한 조합을 가질 경우 State 클래스는 무제한의 서브클래스 수를 필요로 하므로 State 패턴은 적절하지 않다.
  • Context는 상태들 간 잘 정의된 전이를 가져야 한다. 가능한 상태에서는 Context가 다른 상태를 변경할 때와, 각 사례에서 다른 상태는 어떠해야 하는지가 명확해야 한다. 상태들 간 전이가 일관성이 없고 주관적인 경우 State 패턴은 적절하지 않다.

구현

구현 부분의 문제 중 하나를 더 명확하게 다룰 필요가 있겠다: 누가 상태 전이를 정의하는가? [디자인 패턴] 편에서 설명하듯이 State 패턴은 어떤 참가자가ㅡContext인지 State인지ㅡ한 상태에서 다른 상태로의 전이 기준을 정의하는지는 명시하지 않는다. State 패턴을 사용 시 한 가지 장점은 Context로부터 상태 의존적 행위를 제거하여 State 객체에 캡슐화한다는 점이다. 이와 비슷하게 Context로부터 상태 전이 행위를 추출하여 State 객체들에게 분배하기도 한다. 이런 방식으로 각 State 객체는 언제 다른 State로 전이해야 하는지와 각 사례에서 어떤 다른 State로 전이되는지를 알게 된다. 이를 위해선 각 State 객체는 그것의 Context가 무엇인지 알고 각 Context에게 새로운 State로 변경할 것을 알려줄 수 있는 방법이 있어야 한다.

다시 한 번, 이전에 정의한 두 개의 상태 클래스를 고려해보자. 두 개의 클래스, ConcreteStateA와 ConcreteStateB가 다음과 같은 상태 전이 다이어그램을 구현한다고 가정하자:

Dpsc chapter05 State 02.png

이 예제에서 각 요청은 Context가 상태를 다른 상태로 변화시키는 결과를 낳는다. 우리가 만일 각 상태가 Context의 다음 상태를 결정하도록 선택할 경우 우리는 아마도 다음과 같이 클래스에 handleRequest 메소드를 구현할 것이다:

ConcreteStateA>>handleRequest
	"Handle the request some way."
	... do state specific things ...
	context state: (ConcreteStateB new context: context).

ConcreteStateB>>handleRequest
	"Handle the request some other way."
	... do state specific things ...
	context state: (ConcreteStateA new context: context).

명확한 점은 여기서 다른 디자인 결정이 내려진다는 점이다:

  • Context가 인스턴스 변수에 유지될 것인가 (위의 예제에서와 같이) 아니면 상태 내의 각 메소드에 전달될 것인가?
  • 각 상태의 새 인스턴스들이 매번 생성될 것인가 아니면 컨텍스트가 그러한 요청을 받을 것인가?

이를 대신하여 Context가 상태를 스스로 변환하도록 만드는 구현이 있다:

Context>>request
	"Handle the request according to the Context's
	currentState."
	state := self state handleRequest

ConcreteStateA>>handleRequest
	... do state specific things ...
	^ConcreteStateB new

허나 이러한 구현의 한 가지 결점은 상태의 handleRequest 메소드가 어떠한 값도 리턴하지 못하도록 막는다는 점이다.

몇몇 경우는 상태 전이에 대한 모든 정보를 상태 클래스로부터 멀리 보관하는 것이 편하다. 예를 들어, 여러 다른 (하지만 서로 관련된) 유한 상태 기계의 State 클래스를 서로 다른 Context 클래스에서 재사용하고 싶다면 이러한 방법이 적절하다. 이런 경우, 전이를 State 클래스로부터 분리시켜 표현할 필요가 있다.

예제 코드

[디자인 패턴]을 살펴보고 State 패턴이 소개하는 코드 변환을 알아보자.

State 패턴 없이 사용

우리 예제에는 TCPConnection이라는 이름의 클래스가 있는데 이 클래스의 인스턴스는 네트워크 연결을 나타낸다 (TCP는 전송 제어 프로토콜, 즉 네트워크 통신의 표준을 의미). 연결은 여러 개 상태 중 하나로 되어 있고 (예: Established, Listen, Closed 중 하나), 그의 행위는 현재 상태에 따라 좌우된다. 예를 들어, 데이터 전송을 요청 시 연결 객체는 그 상태가 Established인지 Closed인지에 따라 다르게 반응한다. 이러한 상태들은 연결의 암시적 부분인 유한 상태 기계를 형성한다. 다음 상태 전이 다이어그램은 이러한 상태들과 그들 간 전이를 나타낸다:

Dpsc chapter05 State 03.png

TCPConnection은 Closed 상태에서 수명이 시작된다. activeOpen 또는 passiveOpen 명령어가 이것을 각각 Established 또는 Listen 상태에게 전송할 것이다. Established 상태에서 closed 명령어는 그것을 Listen 상태로 전송하는 동안 transmit 명령어는 실행을 하지만 Established 상태에 보관한다. Listen 상태에서는 send 명령어가 그것을 Established 상태로 전송한다.

그러한 상태 의존적 행위를 구현하는 한 가지 접근법으로, 최근 상태를 기반으로 서로 다른 코드 또는 메소드들을 구현하는 조건문을 TCPConnection 객체 내에서 부호화하는 방법이 있다:

Object subclass: #TCPConnection
	instanceVariableNames: 'state'
	classVariableNames: ''
	poolDictionaries: ''

TCPConnection>>activeOpen
	"Open the connection."
	self state == #established ifTrue: [^self].
	self state == #listen ifTrue: [^self].
	self state == #closed ifTrue:
		[^self changeStateTo: #established].
	^self error: 'unknown state'

TCPConnection>>send
	"Prepare the connection to send data."
	self state == #established ifTrue: [^self].
	self state == #listen ifTrue:
		[^self changeStateTo: #established].
	self state == #closed ifTrue:
		[^self error: 'closed connection'].
	^self error: 'unknown state'

TCPConnection은 다른 요청들 또한ㅡpassiveOpen, close, transmit: ㅡ비슷한 방식으로 구현한다. 물론 이후에 시스템의 수명에 새로운 상태를 추가해야 하는 경우 조건문을 검토하여 이러한 상태에 대한 계정에 코드를 추가해야 함을 의미한다.

State 패턴과 함께 사용

State 패턴은 더 깔끔하고 쉽게 확장이 가능한 해법을 제시한다. 구조는 다음과 같다:

Dpsc chapter05 State 04.png

이 구조는 앞의 디자인에 포함되어 있던 유한 상태 기계를 가진다.

위에 나타낸 case문 코드가 이미 있다고 가정하고, 위의 State 패턴 구조로 변환하길 원한다고 가정하자.

단계 1: 연결의 상태를 구분된 객체로 나타낸다. 따라서 TCPConnection의 상태 변수는 Symbol과 같은 간단한 열거자(enumeration) 객체가 아니라 이제 고유의 행위를 가진 실제 TCPState 객체이다:

Object subclass: #TCPConnection
	instanceVariableNames: 'state'
	classVariableNames: ''
	poolDictionaries: ''

Object subclass: #TCPState
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

단계 2: TCPState를 위임하기 위해 TCPConnection을 구현한다. 이번 예제에서 우리는 activeOpen과 send의 구현만 나타낼 것이다. 실제 프레임워크에서는 passiveOpen, close, transmit도 구현할 것이다:

TCPConnection>>activeOpen
	"Open the connection."
	self state activeOpen: self

TCPConnection>>send
	"Prepare the connection to send data."
	self state sendThrough: self

단계 3: TCPState를 구현하여 위임을 수락한다:

TCPState>>activeOpen: aTCPConnection
	"Open the connection."
	self subclassResponsibility

TCPState>>sendThrough: aTCPConnection
	"Prepare to send data through the connection."
	self subclassResponsibility

단계 4: TCPState는 사실상 추상 클래스이므로, 가능한 상태마다 구체적 서브클래스를 구현한다:

TCPState subclass: #TCPEstablishedState
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

TCPState subclass: #TCPListenState
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

TCPState subclass: #TCPClosedState
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''

단계 5: 각 서브클래스마다 각 메소드를 적절하게 구현한다:

TCPEstablishedState>>activeOpen: aTCPConnection
	"Do nothing."
	^self

TCPEstablishedState>>sendThrough: aTCPConnection
	"Do nothing."
	^self

TCPListenState>>activeOpen: aTCPConnection
	"Do nothing."
	^self

TCPListenState>>sendThrough: aTCPConnection
	"Prepare to send data through aTCPConnection."
	^aTCPConnection establishConnection

TCPClosedState>>activeOpen: aTCPConnection
	"Open aTCPConnection."
	^aTCPConnection establishConnection

TCPClosedState>>sendThrough: aTCPConnection
	"This is an error!"
	self error: 'closed connection'

단계 6: TCPState가 TCPConnection로 전송하는 메시지를 구현한다. 또한 TCPConnection의 상태를 closed로 설정한다:

TCPConnection>>establishConnection
	"Establish a connection. Do all of the work necessary
	to properly establish a valid connection and verify
	its success."
	self state: TCPEstablishedState new

TCPConnection>>initialize
	"Set the connection's default state."
	state := TCPClosedState new.
	^self

그 후에 TCPConnection를 어떻게 구현하는지(class구현) 예제를 아래쪽에 소개하겠다:

| connection |
connection := TCPConnection new.
connection
	activeOpen;
	transmit: text;
	close

알려진 스몰토크 사용예

SMTP/Sockets

"매우 단순한 VSE SMTP Client" (ParcPlace, 1996)에서는 통신 프로토콜에 명시적 유한 상태 기계(FSM)를 구현하는 State 패턴의 교과서적 사용예제를 보여준다. 이번 본문에서는 SMTP (간이 전자 우편 전송 프로토콜) 메일 시스템을 비주얼 스몰토크 엔터프라이즈에서 구현하는 방법을 보이고자 한다. 이는 SMTP 상태를 State 클래스로 나타내는데 이 클래스는 SocketClient Context 클래스와 협력하여 TCP/IP socket을 통해 SMTP 서버로 메시지를 전송한다.

ControlMode

비주얼웍스는 DrawingController (DP 313)와 비슷한 State 패턴의 예제를 포함한다. 비주얼웍스에서는 UI Builder 프레임워크가 클래스 ModalController를 사용하여 서로 다른 UI 컴포넌트의 포지셔닝 및 리사이징을 처리한다. ModalController는 "후에 어떤 실행을 하게 될 ControlModel 객체를 내부에 보유하기 위해 마우스 관련 이벤트의 방향을 전환한다."[1] ModalController는 "일부 또는 모든 ModalController의 기본적 제어 순서를 구현하는" ControlMode 서브클래스의 인스턴스를 포함한다 (ValueHolder의 중재자를 통해)."[2]

ModalController/ControlMode 조합과 [디자인 패턴]편에 소개된 HotDraw 예제는 모두 전이를 제공하는데 있어 명시된 FSM가 없다는 점에서 드문 예제라고 할 수 있다. 대신 사용자는 가능한 상태 리스트로부터 선택함으로써 다음 상태를 결정할 수 있다. 이는 State들이 상태 전이를 통제할 수 없는 예제이므로 Context가 스스로 통제해야만 한다.

AlgeBrain

AlgeBrain은 대수학을 위한 지능형 교육 시스템이다 (Sherman과 그 동료들, Kevin Singley, Peter Fairweather, Steve Swerling이 구축). 이 시스템은 State 객체들을 이용해 대수 방정식 문제해결의 현재 상태를 표현한다. AlgebraTutor는 어느 시점에서든 여러 구체적 AlgebraState 서브클래스 중 하나의 인스턴스와 함께 구성된다. 서로 다른 AlgebraState 객체는 주어진 대수학 문제의 현재 상태에 적절한 또는 수용 가능한 사용자 실행을 알고 있다. 따라서 사용자가 실행을 수행하면 교사는 현재 State 객체에 답을 위임한다; 이는 사용자의 실행이 옳은지, 그리고 실행이 집중적이고 컨텍스트 의존적인 오류 메시지를 제공할 수 있는지에 따라 결정된다. 각 State 객체는 사용자 실행에서 무엇을 예상하는지 알고 있기 때문에 State들을 사용해 현재 문제를 해결하기 위한 다음 단계가 무엇인지 도와줄 수 있다.

관계형 데이터베이스 인터페이싱

Brown (1996)은 주문 처리 시스템에 사용된 State 패턴의 예를 설명한다. State 객체들은 관계형 데이터베이스에서 객체를 저장하고 삭제하는 서로 다른 행위들을 제공한다.

관련 패턴

State 패턴 대 Strategy 패턴

State 패턴은 그와 가까운 사촌과 같은 Strategy (339) 패턴과 혼동되곤 하며, 때로는 둘 중 어떤 패턴을 사용해야 하는지, 또는 어떤 패턴을 구현한 경우인지 알아채기가 힘들다. 둘을 구별하는 최고의 규칙은, Context가 수명 내내 하나의 상태 (여러 개 중에) 또는 하나의 전략 객체를 포함할 경우 Strategy 패턴을 사용하는 편이 낫다. 반대로 Context가 애플리케이션의 과정 도중에 변경될 경우 많은 상태 객체를 포함하므로 State 패턴을 사용한 것이며, 특히 상태들 간 전이의 순서가 잘 정의되어 있을 경우 더 분명해진다. 객체의 속성에 대한 설정에서는 그 차이를 감지하기 힘든 때가 종종 있다. State 패턴의 경우 외부 클라이언트에 의해 객체가 어떤 상태에 놓여지지만, Strategy 패턴에서는 객체가 스스로 전략을 선택할 것이다.

또 다른 차이로, Context는 그것이 사용 중인 Strategy 객체를 숨기고 있는 듯이 보이지만 그것이 사용하는 State는 그 Client에게 꽤 분명하게 나타난다는 점이다. 예를 들어, Client가 StorageDevice에게 일부 텍스트를 저장할 것을 알리면 장치는 텍스트를 보관하기 전에 여러 압축 기법들 중 하나를 이용할 수 있고, 또 압축을 전혀 하지 않을 수도 있다. 하지만 장치가 텍스트를 어떻게 압축하는지, 압축을 실행하긴 하는지, 또는 텍스트를 매번 같은 방식으로 압축하는지에 관해서는 Client가 알 필요 없다. 압축은 기계에 대해 private로 이루어지고 Client로부터 숨기기 때문에 이러한 서로 다른 압축 객체들은 Strategy 객체에 속한다. 반대로, Client는 TCPConnection을 open하고 나면 연결이 마치 open 상태처럼 행동할 것을 기대한다. Client가 연결을 close 하면 연결이 마치 closed 상태인 것처럼 행동하길 기대한다. 연결의 상태는 Client에게 즉시 드러나므로 상태 객체들은 State 객체들이다.

Notes

  1. 비주얼웍스 ModalController 클래스 코멘트.
  2. 비주얼웍스 ControlMode 클래스 코멘트.