콜렉션의 표현(리스트, 스택, 트리 등)을 드러내지 않고 요소들을 순회할 수 있게 해주는 행동 패턴
콜렉션은 프로그래밍에서 가장 많이 사용되는 데이터 타입 중 하나지만, 단지 객체 그룹의 컨테이너일 뿐
콜렉션들은 단순한 리스트에 요소를 저장하지만, 어떤 콜렉션들은 스택, 트리, 그래프 또는 다른 복잡한 자료 구조를 기반으로 함
콜렉션이 어떻게 구성되어 있든, 다른 코드들이 콜렉션의 요소들을 사용할 수 있도록 요소들에 접근하는 방법을 제공해줘야 함
리스트 기반 콜렉션은 순회가 쉽지만 트리와 같은 복잡한 자료 구조는 그렇지 않음
다양한 콜렉션을 다루는 클라이언트 코드는 요소가 어떻게 저장되는지 신경조차 쓰지 않을 수 있음
하지만 콜렉션들은 요소에 접근하는 각기 다른 방법을 제공하기 때문에 코드를 특정 콜렉션 클래스들에 결합할 수밖에 없음
이터레이터 패턴 - 콜렉션을 순회하는 행동을 이터레이터라는 별도의 객체로 추출
이터레이터 객체는 알고리즘을 직접 구현할 뿐만 아니라 현재 위치, 남은 요소의 갯수 등 순회의 세부 사항들을 캡슐화함 → 여러 이터레이터들이 서로 독립적으로 동시에 같은 콜렉션을 순회할 수 있음
이터레이터는 주로 콜렉션의 요소를 가져오는 단일 주요 메서드 제공
모든 이터레이터는 동일한 인터페이스를 구현 → 적절한 이터레이터가 존재하는 한 클라이언트 코드는 어떤 유형의 콜렉션, 어떤 순회 알고리즘과도 호환 가능
1. 이터레이터 인터페이스 - 콜렉션을 순회하기 위해 필요한 작업들 선언
- 다음 요소 가져오기, 현재 위치 가져오기, 순회 재시작 등
2. concrete 이터레이터 - 콜렉션을 순회하는 특정 알고리즘 구현
- 이터레이터 객체는 순회 진행도를 스스로 추적해야 함
→ 여러 이터레이터들이 같은 콜렉션을 독립적으로 순회할 수 있게 해줌
3. 콜렉션 인터페이스 - 이터레이터들을 콜렉션과 호환시키는 하나 이상의 메서드 선언
- concrete 콜렉션들이 다양한 유형의 이터레이터를 반환할 수 있도록
메서드의 반환 타입을 이터레이터 인터페이스로 선언해야 함
4. concrete 콜렉션 - 클라이언트가 요청할 때마다 특정 concrete 이터레이터 클래스의 새 인스턴스 반환
- 콜렉션 자체 코드도 같은 클래스에 포함
5. 클라이언트 - 콜렉션과 이터레이터 모두를 각각의 인터페이스를 통해 다룸
→ concrete 클래스들에 결합되지 않으면서 다양한 콜렉션과 이터레이터 사용 가능
- 클라이언트는 보통 이터레이터를 직접 생성하지 않고 콜렉션에서 가져오지만,
클라이언트가 특별한 이터레이터를 정의하거나 한 경우 클라이언트가 직접 생성할 수 있음
- 이터레이터는 복잡한 자료 구조를 다루는 세부 사항을 캡슐화해
클라이언트에게 콜렉션 요소에 접근하는 단순한 메서드 제공
- 클라이언트에게도 편리하고, 콜렉션을 부주의하거나 악의를 가진 행동으로부터 보호 가능
- 단순하지 않은 순회 알고리즘은 크기가 매우 큰 경향이 있음
→ 비즈니스 로직에 위치하게 되면 기존 코드의 역할을 모호하게 하고 유지보수를 어렵게 만듦
- 순회 코드를 지정된 이터레이터들로 옮기면 애플리케이션의 코드를 더 간결하고 깔끔하게 만들 수 있음
- 이터레이터 패턴은 콜렉션과 이터레이터 모두에게 일반 인터페이스를 제공
- 코드가 해당 인터페이스를 사용한다면, 해당 인터페이스를 구현한
다양한 콜렉션들과 이터레이터들을 전달받아도 잘 작동함
1. 이터레이터 인터페이스 선언
- 최소한 콜렉션의 다음 요소를 가져오는 메서드는 가지고 있어야 함
- 편의를 위해 이전 요소 가져오기, 현재 위치 추적하기, 순회의 끝 검사하기 등 다른 메서드 추가 가능
2. 콜렉션 인터페이스를 선언하고 이터레이터를 가져오는 메서드 설명
- 반환 타입은 이터레이터 인터페이스 타입이여야 함
3. 이터레이터로 순회할 수 있게 하려는 콜렉션들에게 concrete 반복자 클래스들을 구현
- 이터레이터 객체는 하나의 콜렉션 인스턴스와 연결되어 있어야 함 → 주로 이터레이터의 생성자에 의해 연결됨
4. 콜렉션 클래스 안에 콜렉션 인터페이스 구현
- 클라이언트에게 특정 콜렉션 클래스들을 위한 이터레이터를 생성하는 지름길 제공
- 콜렉션은 연결을 맺기 위해 이터레이터의 생성자에게 자기 자신을 전달해야 함
5. 클라이언트 코드에서 콜렉션 순회 코드를 이터레이터로 변경
- 클라이언트는 콜렉션 요소들을 순회해야 할 때마다 새 이터레이터 객체를 가져옴
- SRP - 거대한 순회 알고리즘들을 개별 클래스들로 분리해 클라이언트 코드와 콜렉션들을 깔끔하게 만들 수 있음
- OCP - 다른 코드를 훼손하지 않으면서 새로운 유형의 콜렉션과 이터레이터들을 구현해 기존 코드에 전달할 수 있음
- 같은 콜렉션을 병렬적으로 순회할 수 있음
- 순회를 지연시키고 필요할 때 재개할 수 있음
- 앱이 단순한 콜렉션들만 다룬다면 패턴을 적용하는 게 과함
- 일부 특별한 콜렉션들의 요소들은 직접 탐색하는 것이 더 효율적일 수 있음
- 이터레이터 패턴을 사용해 컴포지트 트리 순회 가능
- 팩토리 메서드를 반복자와 함께 사용해 콜렉션 자식 클래스들이 해당 콜렉션들과 호환되는
다양한 유형의 이터레이터들을 반환하도록 할 수 있음
- 메멘토를 이터레이터와 함께 사용해 현재 순회 상태를 저장하고 필요한 경우 롤백 가능
- 비지터를 이터레이터와 함께 사용해 복잡한 자료 구조를 순회하며
요소들이 모두 다른 클래스를 가지더라도 요소들에게 특정 작업을 실행할 수 있음
/**
* Iterator Design Pattern
*
* Intent: Lets you traverse elements of a collection without exposing its
* underlying representation (list, stack, tree, etc.).
*/
interface Iterator<T> {
// Return the current element.
current(): T;
// Return the current element and move forward to next element.
next(): T;
// Return the key of the current element.
key(): number;
// Checks if current position is valid.
valid(): boolean;
// Rewind the Iterator to the first element.
rewind(): void;
}
interface Aggregator {
// Retrieve an external iterator.
getIterator(): Iterator<string>;
}
/**
* Concrete Iterators implement various traversal algorithms. These classes
* store the current traversal position at all times.
*/
class AlphabeticalOrderIterator implements Iterator<string> {
private collection: WordsCollection;
/**
* Stores the current traversal position. An iterator may have a lot of
* other fields for storing iteration state, especially when it is supposed
* to work with a particular kind of collection.
*/
private position: number = 0;
/**
* This variable indicates the traversal direction.
*/
private reverse: boolean = false;
constructor(collection: WordsCollection, reverse: boolean = false) {
this.collection = collection;
this.reverse = reverse;
if (reverse) {
this.position = collection.getCount() - 1;
}
}
public rewind() {
this.position = this.reverse ?
this.collection.getCount() - 1 :
0;
}
public current(): string {
return this.collection.getItems()[this.position];
}
public key(): number {
return this.position;
}
public next(): string {
const item = this.collection.getItems()[this.position];
this.position += this.reverse ? -1 : 1;
return item;
}
public valid(): boolean {
if (this.reverse) {
return this.position >= 0;
}
return this.position < this.collection.getCount();
}
}
/**
* Concrete Collections provide one or several methods for retrieving fresh
* iterator instances, compatible with the collection class.
*/
class WordsCollection implements Aggregator {
private items: string[] = [];
public getItems(): string[] {
return this.items;
}
public getCount(): number {
return this.items.length;
}
public addItem(item: string): void {
this.items.push(item);
}
public getIterator(): Iterator<string> {
return new AlphabeticalOrderIterator(this);
}
public getReverseIterator(): Iterator<string> {
return new AlphabeticalOrderIterator(this, true);
}
}
/**
* The client code may or may not know about the Concrete Iterator or Collection
* classes, depending on the level of indirection you want to keep in your
* program.
*/
const collection = new WordsCollection();
collection.addItem('First');
collection.addItem('Second');
collection.addItem('Third');
const iterator = collection.getIterator();
console.log('Straight traversal:');
while (iterator.valid()) {
console.log(iterator.next());
}
console.log('');
console.log('Reverse traversal:');
const reverseIterator = collection.getReverseIterator();
while (reverseIterator.valid()) {
console.log(reverseIterator.next());
}
// Output.txt
Straight traversal:
First
Second
Third
Reverse traversal:
Third
Second
First
참고 자료: Refactoring.guru