의도
- 반복자 -> 리스트, 스택, 트리 등의 컬렉션 요소들으 기본 뵤현을 노출하지 않고 순회할 수 있도록 해주는 행동 디자인 패턴
문제
- 객체 그룹의 집합인 컬렉션이 있다 가정해보자.
- 보통 컬렉션들을 저장하는 곳은 배열이지만, 일부는 스택, 트리, 그래프 등의 조금 더 복잡한 자료 구조에 저장되기도 한다.
- 자료 구조에 상관없이 컬렉션은 객체에 접근할 수 있는 방법을 제공해야 한다.
- 배열이나 스택과 같은 경우 간단하게 요소 순회가 가능하지만, 트리의 경우를 가정하면 DFS, BFS등의 여러 방식의 순회방식을 고려할 수 있다
- 이 과정에서 순회 알고리즘이 많아질수록 컬렉션의 주요 책임은 명확해지지 않게 된다.
- 또한 클라이언트는 순회 방식에 관심을 가질 필요가 없음에도 불구하고 클라이언트 코드가 어떤 방식으로 순회를 할지 알려줘야 하기 때문에 컬렉션 클래스와 클라이언트 클래스가 결합되버리는 문제가 생기게 된다.
해결책
- 컬렉션의 순회 동작을 반복자라는 별도의 객체로 추출
- 반복자에는 순회 알고리즘과 함께 현재 위치, 남은 객체들의 수와 같은 순회에 필요한 세부 정보들을 함께 저장함 => 이를 통해 여러 반복자들이 독립적으로 같은 컬렉션을 통과할 수 있게 됨
- 반복자에서는 컬렉션의 요소를 가져오기 위한 주 메서드를 제공하고, 반복자가 순회가능한 동안 클라이언트가 이 메서드를 실행함
- 모든 반복자들은 같은 인터페이스를 구현해 클라이언트 코드에서 모든 반복자에 접근할 수 있도록 해야 함
구조
interface Iterator<T> {
current(): T;
next(): T;
key(): number;
valid(): boolean;
rewind(): void;
}
interface Aggregator {
getIterator(): Iterator<string>;
}
class AlphabeticalOrderIterator implements Iterator<string> {
private collection: WordsCollection;
private position: number = 0;
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();
}
}
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);
}
}
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());
}
적용
- 컬렉션이 복잡한 데이터 구조로 이루어져 있지만, 구조의 복잡성을 숨기고 싶을 때 사용
반복자 패턴을 통해 데이터 구조와 작업을 캡슐화 할 수 있음
- 앱 전체에서 순회 코드의 중복을 피하고 싶을 때 사용
알고리즘의 코드는 부피가 매우 크기 때문에 비즈니스 로직에 통합해 사용하는 것이 효율적임
- 여러 순회 방식을 사용해야 하거나, 어떤 순회 방식을 사용해야할지 모를 때 사용
반복자 패턴과 컬렉션들은 각각 인터페이스가 호환되어 있음
구현 방법
- 반복자 인터페이스 선언, 이 때 컬렉션에서 다음 요소를 가져오는 메서드를 반드시 가져야 하고, 순회에 필요한 메서드들을 추가할 수 있음
- 컬렉션 인터페이스를 선언하고 반복자를 가져오는 메서드를 구현
컬렉션 인터페이스의 반환 타입은 반복자 인터페이스와 같아야 함
- 반복자들이 순회하게 할 수 있도록 하고 싶은 컬렉션에 대해 구상 반복자 클래스 구현
- 컬렉션 클래스에서 컬렉션 인터페이스를 구현해 클라이언트가 반복자들을 생성할 수 있도록 해줌
- 클라이언트 코드에서 컬렉션 순회 코드를 반복자 객체를 사용하는 방식으로 조정
장단점
- 부피가 큰 순회 알고리즘들을 별도의 클래스로 추출해 단일 책임 원칙 준수
- 새로운 컬렉션과 반복자들을 코드 변경없이 코드에 추가할 수 있게 되어 개방, 폐쇄 원칙 준수
- 같은 컬렉션을 병렬로 순회할 수 있으며, 순회의 중단, 계속을 구현할 수 있음
- 앱의 컬렉션이 단순한 경우 패턴을 굳이 적용할 필요가 없음
- 반복자를 사용하는 것이 반드시 효율적인 결과는 아닐 수 있음