DesignPatternSmalltalkCompanion:Iterator
ITERATOR (DP 257)
의도
내부 표현 방법을 노출하지 않고 복합 객체의 원소를 순차적으로 접근할 수 있는 방법을 제공한다.
구조
논의
객체 집합이나 리스트를 반복 순회하는 기능은 스몰토크 환경에서 재사용의 훌륭한 예를 제공한다. 객체지향 프로그래밍 환경의 수많은 상황에서 재사용 또는 재사용의 가능성이 발생하지만, 가장 단순한 형태는 새로운 문제에 대해 기존 클래스를 사용하는 것이다. 물론 이것은 기본 클래스 라이브러리가 제공한 기본 추상적 데이터형을 재사용할 때 발생한다. Iterator 패턴은 이러한 가장 기본적인 재사용 형태의 예제를 몇 가지 소개하고자 한다.
서론에서 언급하였듯, 스몰토크 언어는 많은 것을 포함하지 않는다. 주요 스몰토크 환경의 장점들 중 하나는 광범위한 기본 클래스 집합이다. Iterator 패턴의 “구현”은 기본 스몰토크 라이브러리에 있는 재사용 기능의 사례에 불과한 것으로 드러났다. 따라서 이미 잘 작동하는 것을 단순히 사용만 하면 되는데 기존에 있는 것을 다시 만들기 위해 시간을 낭비할 필요가 없다 (그것도 어쩌면 시대에 뒤쳐진 버전을). 구체적으로 iteration과 관련해, Collection 클래스들이 다양한 반복 및 필터링 기능을 포함한 public 인터페이스를 제공하기 때문에 우리는 Iterator 클래스 집합을 구현할 필요가 없다.
내부 Iterators
단순히 객체 집합체를 연속적으로 순회하기 위해 Collection 클래스들은 do: 메서드를 구현한다. do: 메서드는 한 블록을 argument로서 취하여 집합체 내의 각 엔트리와 블록의 로컬 변수를 바인딩하여 블록이 일부 오퍼레이션을 실행하거나 집합체 내에 각 객체를 관련시킬 수 있게 해준다. 따라서 집합체 내 각 요소에 대한 일부 오퍼레이션은 다음을 통해 쉽게 실행할 수 있다:
partsCollection do: [:part | part | partdraw]
이와 비슷하게, 객체의 집합체를 반대 방향으로 순회하는 방법은 다음과 같다:
undoMessages reverseDo: [:msg | msg perform]
스몰토크의 내부 Interatorㅡ순회 대상이 되는 객체 고유의 Iteratorㅡ는 블록에 의해 생성될 수 있다. 블록을 통해 우리는 순회적으로 평가할 코드를 집합체 객체에게 전달할 수 있다.
Collection은 추상 클래스이다. 이것의 구체적 서브클래스는 (몇 가지 특별 사례나 특별 사용의 클래스를 제외하고) 단일 클래스의 객체들뿐만 아니라 이질적 집단(heterogeneous aggregation)도 포함한다 (동일한 클래스의 요소일 필요가 없다). 게다가 모든 Collection들은 do: 메시지에 응답한다: Collection은 일반 메서드를 구현하고, 각 구체적 서브클래스는 필요 시 고유의 버전을 구현한다. 따라서 집합체 유형에 따라 다른 Iterator나 iterator 오퍼레이션을 사용할 필요가 없다. 위의 예제에서 우리는 순회 중인 집합체 종류가 무엇인지 혹은 그것이 포함하고 있는 객체의 종류가 무엇인지 신경 쓸 필요가 없다. 필요한 것은 각 요소가 draw 메시지에 응답할 수 있어야 한다는 점이지만 (partsCollection case에 있는) draw는 물론 다형적으로 구현이 가능하다. [디자인 패턴]에 소개된 C++의 예제와 비교 시 눈에 띄는 차이점들이 있다. (1) iteration을 실행하는 클라이언트는 그것이 순회하는 리스트 타입이 무엇인지를 알고 적절한 Iterator 클래스를 인스턴스화해야 한다. (2) 클라이언트는 리스트에 적절한 Iterator를 리스트에게 요청해야 한다. (3) 우리는 또 다른 클래스를 다형적 Iterator로 구현해야만 한다 (DP 258).
필터링 Iterators
[디자인 패턴] 편에서는 FilteringListIterator (DP 258, 269-270)에 관해서도 언급을 하고 있다. 여기서 Iterator는 구체적 기준이나 제약에 만족하는 집합체의 요소들만 리턴한다. 결과 목록을 순회 시 추가 메시지 전송을 필요로 하긴 하지만 Collection 클래스들은 필터링과 같은 용도에 자체 메서드를 제공하기도 한다. select: 는 이러한 필터링 메시지들 중 하나이다. do: 와 마찬가지로 select: 는 블록 argument를 취한다; 집합체의 각 요소는 블록의 로컬 변수에 바인딩되어 있고 블록 내 코드에 의해 검사된다. 검사 자체는 불 표현식으로서, 주로 최근 집합체 엔트리로 전송되는 메시지이다. select: 의 결과, 검사 조건에 만족하는 요소들의 집합체가 생긴다. 이 결과로 생긴 집합체를 순회하기 위해선 do: 를 한 번 더 실행하면 되겠다. 예를 들어보자:
(timers select: [:aTimer | aTimer isActive])
do: [:timer | timer tick]
코드를 더 명확하게 하기 위해 다음과 같이 쓸 수 있다:
activeTimers := timers select: [:aTimer | aTimer isActive].
activeTimers do: [:aTimer | aTimer tick
먼저, timers를 대상으로 활성화된 엔트리만 필터링을 하였다 (즉, isActive 메시지에 true 값을 리턴하는 엔트리만 필터링). 그리고 필터된 리스트를 순회하였다.
Collection 클래스들은 reject: 메시지도 구현하였는데, 이 메시지는 select: 의 부분적 순회에 그친다. 따라서 다음 코드를 실행한 후 dormantTimers는 활성화되지 않은 모든 타이머의 집합을 참조할 것이다:
dormantTimers := timers reject: [:aTimer | aTimer isActive]
Iteration 도중에 요소의 추가 및 제거
스몰토크의 초보자들이 자주 범하는 한 가지 오류는 일부 검사를 바탕으로 집합체에 iteration를 실행하면서 요소들을 추가하거나 제거한다는 것이다 (DP 261 참조). 그러나 do: 를 이용해 집합체에 iteration을 실행하는 동안 요소들을 추가하거나 제거할 경우 결과는 예상과 다를 것이다. do: 메서드는 루프에서 증가하는 인덱스를 사용하여 집합체의 각 요소로 접근한다ㅡ따라서 요소를 지우게 되면 엔트리를 완전히 잃게 될지도 모른다; 엔트리를 추가할 경우 특정 엔트리를 두 번 접근하게 될 수도 있다. 다음과 같은 코드는 이러한 문제를 야기할 수 있다:
"Don't do this!"
timers do: [:aTimer |
aTimer isActive ifFalse: [timers remove: aTimer]
이에 따라 우리는 집합체를 반복하는 동안 엔트리를 추가하거나 제거하고, 집합체 원본에 있는 모든 요소에 대해 한 번만 iteration 접근시킬 방법이 필요하다. 이러한 상황을 자연스럽게 해결하는 방법은 집합체의 복사본에 iteration을 실행하면서 원본에서 요소를 추가 및 삭제하는 것이다:
"Do this"
timers copy do: [:aTimer |
aTimer isActive ifFalse: [timers remove: aTimer]]
외부 Iterators
이제 집합체들은 iteration을 위한 고유의 인터페이스를 (내부 코드도 함께) 제공한다. 그러나 구분된 포인터를 (위치 표시기) 집합체에 유지하고 싶다고 가정하자. 예를 들어, 다수의 포인터를 하나의 리트스에 모아 그 리스트에서 어느 때건 한 회 이상의 순회를 보류 상태로 두고자 하는 경우가 있다. 또는 다수의 객체나 메서드로 서로 다른 부분의 iteration을 수행해야 하는 경우도 있다; 따라서 우리는 Iterator를 전달할 수 있어야 한다. 이러한 위치 표시기를 유지하는 방법은 리스트의 외부에 있는 객체를 사용하는 것이다. 그러한 외부 Iterator는 Collection 자체의 iteration 메서드보다 iteration 프로세스에 대해 더 미세한 조정을 클라이언트에게 제공한다. 단 외부 Iterators는 Iterator 별로 추가 객체를 요구한다.
위치 포인터를 집합체에서 유지하는 객체는 집합체를 참조하는 인스턴스 변수 하나와, 최근 요소의 인덱스에 대한 변수 하나를 필요로 한다. 이러한 객체를 전방 Iterator, 즉 집합체를 전방으로 순회하는 Iterator로 만들 경우 first, currentItem, next, isDone과 같은 메서드를 필요로 할 것이다 (DP 257).
[디자인 패턴] 편에서와 마찬가지로 next와 first에게는 위치 표시기를 조작만 시키고currentItem이 집합체로부터 최근 엔트리를 리턴하도록 만들 수도 있다. 아니면 순회에 해당하는 인터페이스를 단순화시키기 위해 currentItem을 제거하여 next가 위치 표시기를 업데이트한 후 최근 가리킨 엔트리를 리턴하도록 만들 수도 있다. 따라서 집합체의 전방 순회(forward traversal)에 first, next, isDone을 사용할 수 있다.
스몰토크 기반 클래스 라이브러리에서 이러한 기능을 정확히 제공하는 클래스의 하위계층구조를 포함하고 있다는 사실이 다시 한 번 드러났다 (메시지명은 약간씩 다르지만). 추상 클래스 Stream의 몇 가지 구체적 서브클래스를 이용해 요소들의 집합체를 “stream over”ㅡ순회ㅡ할 수 있다.
Stream의 구체적 서브클래스에는 집합체로부터 객체를 읽어오는 ReadStream, 객체를 집합체에 저장하는 WriteStream, 이 두 가지를 모두 지원하는 ReadWriteStream이 포함된다. 우리는 기존의 집합체에서 항목을 검색해야 하므로 ReadStream이 가장 적절하겠다. ReadStream은 다음과 같이 인스턴스화되고 집합체와 연관된다:
stream := ReadSteram on: aCollection
그리고 나면 다음 메시지를 이용해 집합체를 순회한다: next는 위치 표시기를 증가시켜 최근 엔트리를 리턴한다; reset은 첫 엔트리 이전에 위치 표시기를 설정하여 그 다음 next가 첫 엔트리를 리턴할 것이다; 그리고 atEnd는 isDone과 동일한 기능을 제공하여 우리가 집합체에서 마지막 요소를 이미 보았는지를 검사한다. 이에 따라 stream은 다음과 같이 마지막까지 순회할 수 있다:
stream reset.
[stream atEnd] whileFalse:
[entry := stream next.
"do some processing with 'entry'"]
Stream은 코드 내 한 위치에서 시작해 선형 순회를 한꺼번에 실행하기 위해 do: 또한 구현한다는 사실도 밝혀졌다. do: 의 블록 argument에 제공된 각 엔트리는 Stream의 집합체에서 다음 엔트리이다. do: 는 attend 조건을 처리하지만 reset은 처리하지 않는다ㅡ즉 최근 위치부터 집합체 마지막 요소까지 반복할 것이다. 따라서 stream을 다음과 같이 순회할 수 있다:
stream
reset;
do: [:entry | "do something with 'entry'"]
두 예제에서 모두 스트림이 금방 인스턴스화되었다면 reset 메시지가 불필요하다. 즉, 생성 시 스트림이 그 position을 0으로 설정한다는 의미이다.
다시 말하지만 Stream은 그것이 스트리밍하는 집합체의 유형을 신경쓰지 않는다는 것이 중요하다. 예를 들어, next 메시지는 집합체의 타입이나 객체의 타입과 상관없이 집합체 내의 다음 객체를 리턴할 것이다. 그러므로 Iterator 클래스와 Collection 클래스의 1대 1 매핑을 만족하기 위한 필요조건이 없다. 각 Collection 서브클래스마다 다른 타입의 Iterator를 제공하지 않아도 되며, 집합체에게 인스턴스화할 Iterator의 적절한 타입을 요청하기 위한 팩토리 메서드도 필요가 없다. 그 결과, 클라이언트를 프로그래밍할 때 한 가지 고민이 줄어든 셈이다.
우리는 다음과 같이 외부 (Stream) Iterator를 사용할 수도 있다. DP 265 페이지와 같이 Employee 객체의 Collection이 있고, Employee 클래스는 print 오퍼레이션을 지원한다고 가정하자. 리스트를 인쇄하기 위해 애플리케이션은 Stream을 argument로 취하는 printEmployee: 메서드를 정의해야 할 것이다:
EmployeeApplication>>printEmployees: aReadStream
aReadStream reset.
[aReadStream atEnd] whileFalse:
[aReadStream next printDetails]
모든 직원을 인쇄하기 위해 다음과 같이 코딩할 것이다:
EmployeeApplication>>printAllEmployees
| stream |
stream := ReadStream on: employees.
self printEmployees: stream
스트림을 argument로 취하여 printEmployee: 로 전달할 때 한 가지 이점은, 스트림이 ReadStream 프로토콜을 만족하는 다른 종류의 스트림을 통과시켜도 printEmployee: 가 신경쓰지 않는다는 점이다. 따라서 ReverseReadStream: 이라는 이름의 역순회 스트림 클래스를 갖고 있다면 똑같은 printEmployees: 메서드를 이용해 모든 직원들을 역순으로 인쇄할 수 있다:
EmployeeApplication>>printAllEmployeesReversed
| stream |
stream := ReverseReadStream on: employees.
self printEmployees: stream
문자열 반복하기
스몰토크에서 String은 Character의 집합체이다. 따라서 Collection 반복 오퍼레이션을 이용해 String의 각각의 Character를 취하여 작업할 수 있다.
가장 먼저, 내부 Iterator를 이용하여 String 내의 각 구현부에 Character 메시지를 적용할 수 있다:
aString do: [:aCharacter |
fileStream nextPut: aCharacter asUppercase]
더 중요한 것은, Stream 클래스에는 String의 반복 도중에 사용할 수 있는 간단한 파싱 오퍼레이션과 같이 (nextWord, nextLine) String 특정적인 여러 메서드뿐만 아니라, 집합체를 순회하며 특정 객체를 검색하는 오퍼레이션과 같이 문자열 처리를 촉진하는 일반 집합체 처리 메서드들도 포함되어 있다는 점이다. 예를 들어, 문자열에서 주석을 제거하기 위해서는 (한 쌍의 쌍따옴표) 다음과 같은 메서드를 구현할 수 있겠다:
String>>withoutComments
| inStream outStream |
inStream := ReadStream on: self.
outStream := WriteStream on: String new.
[inStream atEnd] whileFalse: [
"Copy all characters up to the first double-quote:"
outStream nextPutAll: (inStream upTo: $").
"Skip all until (and including) the next double-quote:"
inStream skipTo: $"].
^outStream contents
Composite 반복하기
여태까지는 flat 집합체의 순회 방법과 오퍼레이션을 보였다. 그러나 좀 더 복잡하고 계층적인 Composite (137) 트리와 같은 구조들에 대한 iteration도 필요한 것이 사실이다. 우선 단일 루트 노드를 가진 순수 트리 구조의 순회를 살펴보자. 이 구조의 루트 노드에는 자식 노드가 있을 수도, 그리고 없을 수도 있다; 각 자식 노드는 재귀적으로 동일한 방식으로 정의된다. 따라서 대충 다음과 같은 구조가 나온다:
Tree라는 이름의 클래스를 정의해보자. 위의 구조에서 각 요소는 이 클래스의 인스턴스이다. 구조에서 잎(leaf)들은 0의 자식을 가진 Tree들이다.
Object subclass: #Tree
instanceVariableNames: 'children name'
classVariableNames: ''
poolDictionaries: ''
Tree class>>named: aSymbol
"Instance creation method."
^self new
name: aSymbol;
children: OrderedCollection new
Tree>>addChild: aTree
children add: aTree
child과 name에 대한 접근자 메서드를 가정해보자. 이제 do: 에서와 마찬가지로 블록을 전달하는 일반적인 방식으로 Tree 구조에 대한 반복을 허용하기 위해 다음과 같은 메서드를 정의할 수 있다. 이 코드는 트리의 깊이 우선 순회방식을 구현한다:
Tree>>recursiveDo: aBlock
self children do: [:aTree | aTree recursiveDo: aBlock].
aBlock value: self
Tree를 순회하는 것은 recursiveDo: 를 Tree의 루트 노드로 전송함으로써 시작된다:
| top middle bottom |
top := Tree named: #Top.
middle := Tree named: #Middle.
bottom := Tree named: #Bottom.
bottom addChild: (Tree named: #D); "a leaf"
addChild: (Tree named: #c).
middle addChild:bottom;
addChild: (Tree named: #B).
top addChild: middle;
addChild: (Tree named: #A).
top recursiveDo: [:aTree |
Transcript cr; show: aTree name asString].
이 코드를 실행 시 다음과 같은 결과가 Transcript에 나타난다:
D
C
Bottom
B
Middle
A
Top
히스토리 목록
Command (245) 패턴의 경우, 필요 시 전방이나 후방으로 이동하여 리스트 엔트리로 접근하게 해주고 어느 때건 새 엔트리를 추가할 수 있게 해주는 특수 사례의 순회 객체가 필요하다. 우리 리스트는 임시로 정열되며, 사용자가 실행한 오퍼레이션과 실행을 나타내는 Command 객체를 포함한다. 이러한 히스토리 목록을 보유 시 Command 패턴에서 논한 다단계 undo-redo 기능을 구현할 수 있도록 해준다. 사용자가 오퍼레이션의 undo를 선택하면 우리는 가장 최근 Command를 검색하여 unexecute하도록 명령한다. 사용자가 또 다시 undo를 요청하면, 이전 Command를 검색하여 다시 명령한다. 사용자가 이미 취소된 오퍼레이션을 redo하기로 결정하면 전방으로 이동할 것이다. Redo을 할 때마다 목록에서 최근 위치 표시기를 향해 다음 Command 객체를 검색하여 execute를 요청할 것이다.
우리의 첫 번째 선택은 매우 쉬운 방법으로서 OrderedCollection으로 리스트를 표현하는 것이다. 다음으로, 위에 설명된 행위를 구현하기 위해 특별한 유형의 Iterator가 필요하다. HistoryStream을 ReadWriteStream의 서브클래스로서 비주얼웍스 이미지로 정의함으로써 시작하고자 한다. (이를 쓴 이후로 비주얼 스몰토크와 IBM 스몰토크에서 ReadWriteStream은 비주얼웍스 구현과 다른 행위를 표시한다. 비주얼웍스에서는 스트림으로 메시지를 전송하여 스트림의 집합체에 새로운 엔트리를 추가할 수 있는 반면, 다른 환경에서는 오류가 발생할 것이다; 따라서 우리는 강제로 캡슐화를 깨고 집합체로 직접 전송된 메시지를 이용해 스트림의 집합체로 엔트리를 추가해야 한다. 따라서 비주얼웍스에서는 다음을 이용하면 된다:
| myColl myStream |
myColl := OrderedCollection new: 10.
myStream := ReadWriteStream on: myColl.
myStream nextPut: 1
다른 스몰토크 방언에서 이 코드를 사용 시 myColl 크기 이상의 인덱스를 시도하기 때문에 walkback을 야기한다 (Collection의 크기는 new: 로 요청되는 엔트리의 수가 아니라 그에 포함된 객체의 수에 따라 달라진다). 비주얼웍스 구현은 필요 시 집합체 크기의 확장을 다룬다. 결과적으로 우리는 HistoryStream에 대해 비주얼웍스 구현에서와 동일한 코드를 선택하였다.)
HistoryStream은ㅡ다른 모든 Stream과 마찬가지로ㅡ최근 위치 표시기를 유지할 것이다; 하지만 HistoryStream의 경우 항상 다음에 이용이 가능한 슬롯, 즉 새로운 리스트에 삽입될 Command의 위치를 가리킬 것이다. 사용자가 애플리케이션과 상호작용하면서 우리는 히스토리 목록의 최근 위치에 새로운 Command를 추가한다. 여러 개의 오퍼레이션을 취소하여 시간을 뒤로 이동할 경우 그 다음 사용자의 실행은 리스트의 끝이 아니라 그 시점에 추가되어야 한다.
사용자 상호작용 시나리오를 고려해보자. 동시에 우리는 히스토리 목록의 관련 행위를 상세히 묘사할 것이다. 이것은 구현 요구사항에 대한 우리의 이해를 구체화시켜줄 것이다. 다음 다이어그램에서 동그라미 모양은 리스트의 Command 객체, 화살표는 히스토리 목록의 위치 표시기가 가리키는 곳, 숫자는 위치 표시기의 현재 값을 표시한다.
첫 시나리오로, 사용자가 하나의 오퍼레이션을 실행한다. 애플리케이션은 사용자 요청을 처리하고 난 후 적절한 Command를 히스토리 목록에 추가한다. Stream에서 상속된 인덱싱 스킴(indexing scheme)은 0-relative 이기 때문에 하나의 Command를 추가 후 위치 표시기는 1로 증가할 것이다.
다음으로, 사용자는 두 번째 실행작용을 수행한다; 히스토리 목록은 다음과 같은 모습이 된다:
이제 사용자는 Undo 메뉴항목을 선택한다. 우리는 히스토리 목록에서 이전 Command를 검색하여 위치 표시기를 낮추고 그에 상응하는 집합체 엔트리 값을 (두 번째, 인덱스 1에 상응하는) 리턴한다. 이 시점에 위치 표시기는 그대로 둔다. 따라서 첫 번째 undo 후에는 다음과 같은 모습일 것이다:
두 번째 undo 요청이 발생한다: 히스토리 목록은 첫 번째 엔트리를 반환한다. 두 번째 undo를 처리한 후 모습은 다음과 같을 것이다:
사용자가 이제 Redo, 즉 "가장 최근 실행한 undo 작업을 다시 하세요"를 요청한다. 따라서 리스트의 첫 번째 Command를 리턴하고ㅡ그래야 애플리케이션이 execute 메시지를 이 곳으로 전송할 수 있다ㅡ위치 표시기를 증가시킨다. Redo 후 다음과 같은 모습이다:
모든 경우에서 위치 표시기는 new 엔트리가 삽입되어야 하는 곳을 가리킨다. 예를 들어, 사용자가 새로운 오퍼레이션을 실행한다고 치자. 우리는 새로운 Command 객체가 히스토리 목록의 첫 엔트리 이후에 삽입되길 원한다. 두 번째 Command를 삭제하길 바라는 것이다; 이것은 이미 undo (redo이 아니라) 되었으므로 더 이상 사용자 상호작용 히스토리의 일부가 아니다. 히스토리는 “rewritten(재작성)”되어 다음 undo 시 이용할 수 있는 Command는 사용자의 가장 최근 실행을 나타낸다. 따라서 새로운 Command의 삽입 후 히스토리는 다음과 같은 모습을 띤다:
코드는 아래와 같다. 위의 모든 행위는 단순히 작은 규모의 메서드 집합을 정의하고 Stream 라이브러리에서 기존 기능을 재사용함으로써 구현할 수 있다는 점을 주목하자. HistoryStream은 position, readLimit, collection 인스턴스 변수들을 ReadWriteStream으로부터 상속한다:
ReadWriteStream subclass: #HistoryStream
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Undo-Redo'
우리는 HistoryStream을 구현하여 그것의 클라이언트가 히스토리 목록 내에 캡슐화된 실제 Collection 만 알고 있도록 한다; 모든 클라이언트 상호작용은 HistoryStream 객체와 함께 있을 것이다:
HistoryStream class>>new
^self on: OrderedCollection new
표준 WriteStream 메시지인 next:Put: 를 전송하여 새로운 Command를 리스트에 추가할 수 있다. 이는 추가된 Command를 리턴하여 다른 모든 nextPut: 구현부와 일치된다:
HistoryStream>>nextPut: aCommand
"Add aCommand at current 'point in time' and eliminate
any entries after it."
super nextPut: aCommand.
self truncate.
^aCommand
HistoryStream>>truncate
| prevReadLimit |
prevReadLimit := readLimit.
"Logically remove entries after the current position:"
readLimit := position.
"Physically remove them too so they can be garbage-
collected:"
prevReadLimit > readLimit
ifTrue:
[readLimit to: prevReadLimit do:
[:index | collection at: index + 1 put: nil] ].
"add 1 to indx because Streams are 0-relative
and Collections are 1-relative."
오퍼레이션의 undo를 원하면 히스토리 목록에서 현재 시점 이전의 Command를 검색한다:
HistoryStream>>previous
"Return the Command prior to the current position.
Return nil if we're already at the start."
self position = 0 ifTrue: [^nil].
self position: self position - 1.
^self peek
우리는 HistoryStream이 ReadWriteStream으로부터 next를 상속하도록 둔다. Command가 redo에 대한 최근 위치 이상을 필요로 할 때 우리는 next를 HistoryStream으로 전송한다. 이는 최근 시점 이후 논리적으로 하나의 Command가 있음을 가정한다; next는 readLimit에 유지된 논리적 경계선 이상의 엔트리는 어떤 것도 리턴하지 않을 것이다 (position이 readLimit 이상을 가리키면 position을 증가시키고 nil을 리턴한다). 이것이 바로 위에 정의된 truncate가 작동하는 이유이다.
undo-redo 기능에 HistoryStream을 사용하는 방법에 대한 상세한 내용은 Command (245) 패턴을 참조한다.
구현
다음 문제들은 스몰토크에서 Iterator를 사용 시 적용된다.
- 혼합 집합에 대한 반복. C++와 같이 강한 타입핑된 언어에서의 Iterator와 달리 스몰토크의 Iterator들은 그들이 순회하는 집합체 내 객체의 정확한 클래스는 신경 쓰지 않는다. C++에서 리스트는 특정 타입의 객체, 즉 특정 클래스의 객체나 객체의 서브클래스만 포함하도록 선언된다. 스몰토크에서는 이것이 사소한 문제에 속한다. Collection에는 어떤 종류의 객체든 포함할 수 있다. Collection을 반복하고 그 요소에 메시지를 전송할 때 고려해야 할 한 가지는 각 요소가 그 메시지를 이해하고 어떤 클래스든 동일한 메시지 인터페이스를 다형적으로 구현할 수 있어야 한다는 점이다.
- 집합체 타입마다 다른 Iterator를 사용해야 하는가? [디자인 패턴]에서 언급한 바와 같이 Iterator의 C++ 버전은 우리가 순회하고자 하는 집합체 타입마다 추상적 Iterator 클래스와 구체적 Iterator 서브클래스를 정의한다. 따라서 List에 대한 ListIterator 클래스, SkipList에 대한 SkinListIterator 등을 갖는다 (DP 258). 스몰토크에서는 다음과 같은 이유로 이러한 요구사항을 갖고 있지 않다. (1) 내부 Iterator의 경우 모든 Collection 서브클래스는 동일한 반복 프로토콜을 구현한다. (2) 외부 Iterator의 경우에도ㅡStreamㅡ동일한 반복 프로토콜을 구현한다.
- 내부 Iterator 대 외부 Iterator. 내부 Iterator 대신 외부 Iterator의 사용을 선택해야 할 때는 언제일까? 외부 Iterator를 사용하는 중요한 이유는 메서드들간 공유가 쉽고 객체들 간 전달이 쉽기 때문이다. 외부 Iterator는 클라이언트에게 iteration에 대한 정밀한 통제력과, 하나의 클라이언트 메서드에 단일 위치로부터 전송된 1회성 내부 반복 메시지 대신 필요 시 다수의 코드 위치로부터 iteration 단계를 호출하는 기능을 제공한다. 스몰토크에서 찾아볼 수 있는 또 다른 근거로는, 외부 Iterator Stream 클래스들이 수많은 문자열 조작 메서드를 제공한다는 것을 들 수 있다.
구체적 예제를 살펴보자. 문자열, 즉 예를 들어 파일로부터 레코드를 파싱하길 원한다고 가정하자. 문자열은 직원에 대한 여러 필드를 포함하고 있으며, 우리는 각 필드에 정보를 인증하고자 한다. 첫 번째 필드는 직원의 이름이며, 두 번째 필드는 부사 번호를 포함하며, 각 필드는 세미콜론으로 구분된다. 이를 다음과 같이 구현할 것이다EmployeeRecordValidator>>validateRecord: aString "The record has already been read and is in aString. Validate each field." | stream | stream := ReadStream on: aString. self validateEmployeeName: stream; validateDepartmentNumber: stream; ... EmployeeRecordValidator>>validateEmployeeName: aStream | name | name := aStream upTo: $;. "Perform the validation now:" ... EmployeeRecordValidator>>validateDepartmentNumber: aStream "The stream is already positioned to point at the department number; get it from the stream and validate it." | dept | dept := aStream upTo: $;. "Perform the validation now:" ...
고유의 명시적 인덱스를 문자열로 유지하였고, 세미콜론으로 끝나는 필드를 도출하기 위해 고유의 문자열 순회를 실행하였으며 각 메서드에 두 개의 argument, 즉 문자열과 그 인덱스를 전달하였다. 하지만 Stream은 단일 객체에 두 개의 argument를 구성하여ㅡ집합체와 최근 위치 표시기ㅡ공유하기 수월하도록 만들었다. - 절차적 추상화. 간단한 반복 메서드에 더해 스몰토크 기본 라이브러리의 내부 및 외부 Iterator는 다른 공통된 반복 시나리오에 대해 절차적 (메서드 차원의) 추상화를 몇 가지 제공한다. 예를 들어, 집합체에서 각 하위요소를 바탕으로 합계를 계산하고자 할 경우 do: iteration 내에서 스스로 모든 작업을 하는 대신 inject:into: 메시지를 전송할 수 있다 (예제 코드 단락 참조); 각 요소에 오퍼레이션을 실행한 결과를 포함하는 새로운 집합체를 생성하고자 할 때 collect: 메시지를 사용할 수 있다; 구체적 문자와 마주칠 때까지 문자열을 순회하고자 할 때는 Stream>>upTo: 메시지를 사용할 수 있다. 프로그래머로서 이러한 추상 메서드를 사용 시 실행하는 코드의 전체량이나 어디까지 신경을 써야 하는지에 대한 정보가 별로 없다 (추상화 개념에 대한 전반적 이유).
- 필터링 Iterators. 스몰토크 라이브러리의 Iterator는 집합체의 요소들을 필터링하기 위한 몇 가지 추상화를 제공한다. 예를 들어, 집합체의 논리적 하위집합에 오퍼레이션을 실행하고자 할 때 우리는 select: 또는 reject: 메시지를 전송함으로써 그러한 요소들을 선택할 수 있다.
예제 코드
위에서 Iterator코드의 많은 사용예를 보였다. 그러나 기초적 사용을 위해 급히 정보가 필요한 독자들을 위해 간단하면서 단순한 예제를 소개하고자 한다.
기업의 재정분석 애플리케이션 업무 중 하나는 기업이 직원의 봉급에 얼마나 많은 돈을 소비하고 있는지 계산하는 것이다. 애플리케이션이 현재 employee 객체의 집합체를 갖고 있다고 가정하면 우리는 이 집합체에 반복을 실행하여 여러 방법으로 직원들의 임금 총액을 계산할 수 있다.
첫째, 고유의 내부 iteration 기능을 사용해 집합체를 순회한다:
FinancialAnalyst>>calculateTotalSalaries: employees
"Sum the salaries of all employees."
| total |
total := 0.
employees do: [:anEmployee |
total := total + anEmployee salary].
^total
이러한 유형에 대한 summing-while-iterating가 흔히 요구되므로 스몰토크 Collection 객체들은 이러한 용도만을 위한 절차적 추상화를 제공한다:
FinancialAnalyst>>calculateTotalSalaries: employees
^employees
inject: 0
into: [:total :anEmployee |
total + anEmployee salary].
외부 Iterator인 ReadStream로도 동일한 업무를 수행할 수 있다:
FinancialAnalyst>>calculateTotalSalaries: employees
| stream total |
stream := ReadStream on: employees.
total := 0.
[stream atEnd] whileFalse:
[| anEmployee |
anEmployee := stream next.
total := total + anEmployee salary].
^total
Stream은 1회 순회를 위한 do: 메서드를 지원한다:
FinancialAnalyst>>calculateTotalSalaries: employees
| stream total |
stream := ReadStream on: employees.
total := 0.
stream do: [:anEmployee |
total := total + anEmployee salary].
^total
알려진 스몰토크 사용예
Iterator 패턴은 스몰토크로 작성된 애플리케이션이라면 어디서든 사용되는 패턴들 중 하나일 것이다. 사용예는 여러분에게 맡기도록 하겠다!
관련 패턴
Iterator 패턴에서 히스토리 목록 변형체는 Command (245) 패턴과 함께 사용된다. 히스토리 목록은 대화형 애플리케이션에서 사용자가 초기화한 오퍼레이션을 나타내는 Command 객체들을 보관할 수 있으며, 히스토리 목록은 다단계의 undo 및 redo을 허용한다.
트리 구조에서 iteration은 트리와 그 컴포넌트 객체를 나타내기 위해 Composite (137) 패턴을 함축적으로 포함한다.