Iterator Pattern은 순회 로직을 순회자(Iterator) 객체로 분리합니다. 이러한 추상화 작업을 통해 순회 불가능한 객체더라도 인터페이스만 구현한다면 순회 가능한 객체로 만들 수 있습니다.
Iterator Pattern
은 GoF의 분류 체계에서 행위(Behavioral) 패턴에 속합니다.
예를 들어, 자료구조 중 배열은 메모리 구조의 특징 덕분에 0..<배열크기 범위로 인덱스를 증가시키며 요소에 접근할 수 있습니다. 허나 리스트의 경우엔 순회 로직을 직접 구현하지 않는 한 인덱스를 이용한 순회가 불가능합니다. 리스트 클래스를 순회 가능한 객체로 만들기 위해선 별도의 구현을 추가해야 합니다. 여기서 Iterator Pattern을 적용한다면 리스트 클래스의 세부 구현을 변경하지 않고도 순회 가능한 객체로 확장할 수 있습니다.
Iterator Pattern을 구현하기 위해 필요한 역할은 다음과 같습니다.
- Iterator(반복자): 순회 로직을 추상화한 인터페이스입니다. 다음 요소를 반환하는
next()
메서드와 현재 요소가 마지막 요소인지 확인하는hasNext()
메서드를 포함합니다. - ConcreteIterator(구체적인 반복자): Iterator 인터페이스를 구현한 객체입니다.
- Aggregate(집합체): 순회 가능한 객체임을 나타내는 인터페이스입니다. ConcreteIterator 객체를 생성하여 반환하는
makeIterator()
메서드를 포함하기도 합니다. (aka. Iterable) - ConcreteAggregate(구체적인 집합체): Aggregate 인터페이스를 구현한 객체입니다.
Java 언어로 배우는 디자인 패턴 입문의 Iterator Pattern 예제를 Swift로 작성해보았습니다.
protocol Iterable<Element> {
associatedtype Element where Element == Iter.Element
associatedtype Iter: Iterator
func makeIterator() -> Iter;
}
protocol Iterator<Element> {
associatedtype Element
mutating func next() -> Element?
}
struct Book {
private(set) var name: String
}
struct BookShelf {
private var books: [Book] = []
var count: Int { books.count }
func book(at index: Int) -> Book {
return books[index]
}
mutating func add(book: Book) {
books.append(book)
}
}
extension BookShelf: Iterable {
typealias Element = Book
typealias Iter = BookShelfIterator
func makeIterator() -> Iter {
return BookShelfIterator(self)
}
}
struct BookShelfIterator: Iterator {
typealias Element = Book
private let bookShelf: BookShelf
private var index: Int
init(_ bookShelf: BookShelf) {
self.bookShelf = bookShelf
index = 0
}
mutating func next() -> Element? {
if bookShelf.count > index {
defer { index += 1 }
return bookShelf.book(at: index)
}
return nil
}
}
var bookShelf = BookShelf()
bookShelf.add(book: Book(name: "Book1"))
bookShelf.add(book: Book(name: "Book2"))
bookShelf.add(book: Book(name: "Book3"))
bookShelf.add(book: Book(name: "Book4"))
var iterator = bookShelf.makeIterator()
while let book = iterator.next() {
print(book.name)
}
// output:
// Book1
// Book2
// Book3
// Book4
Swift에서는
nil
값을 제공하므로hasNext()
메서드 대신next()
메서드가nil
을 반환하면 순회를 종료하도록 구현합니다.
코드에 따르면 각 구조체는 다음 역할을 따릅니다.
Iterator
: IteratorBookShelfIterator
: ConcreteIteratorIterable
: AggregateBookShelf
: ConcreteAggregate
이렇게 순회를 담당하는 로직을 별도의 클래스로 분리하여 확장성을 높이는 것이 Iterator Pattern의 핵심입니다. 인터페이스를 활용한 이러한 확장성 및 다형성 덕분에 Iterator
인터페이스만 구현하고 있으면 모든 순회가 필요한 로직에 적용할 수 있습니다.
이미 Swift에서는 IteratorProtocol 프로토콜을 제공하여 어떠한 클래스든 IteratorProtocol
을 채택하여 for in
구문에 활용할 수 있습니다. (Aggregate 역할을 Sequence
프로토콜이 수행합니다.)
Apple 공식 문서의 예제를 가져왔습니다.
struct Countdown: Sequence {
let start: Int
func makeIterator() -> CountdownIterator {
return CountdownIterator(self)
}
}
struct CountdownIterator: IteratorProtocol {
let countdown: Countdown
var times = 0
init(_ countdown: Countdown) {
self.countdown = countdown
}
mutating func next() -> Int? {
let nextNumber = countdown.start - times
guard nextNumber > 0 else { return nil }
times += 1
return nextNumber
}
}
let countdown3 = Countdown(start: 3)
for count in countdown3 {
print("\(count)...")
}
// output:
// 3..
// 2..
// 1..
Countdown
은 ConcreteAggregate 역할을 수행하며, CountdownIterator
는 ConcreteIterator 역할을 수행합니다. 이렇게 순회 로직을 분리하는 방식으로 Collection 형식이 아니라 하더라도 순회 로직을 추가함으로서 순회 가능한 객체를 만들 수 있습니다.