Iterator Pattern in Swift

design-pattern
Written on Aug 9, 2023


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로 작성해보았습니다.

Language: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: Iterator
  • BookShelfIterator: ConcreteIterator
  • Iterable: Aggregate
  • BookShelf: ConcreteAggregate

이렇게 순회를 담당하는 로직을 별도의 클래스로 분리하여 확장성을 높이는 것이 Iterator Pattern의 핵심입니다. 인터페이스를 활용한 이러한 확장성 및 다형성 덕분에 Iterator 인터페이스만 구현하고 있으면 모든 순회가 필요한 로직에 적용할 수 있습니다.


이미 Swift에서는 IteratorProtocol 프로토콜을 제공하여 어떠한 클래스든 IteratorProtocol을 채택하여 for in 구문에 활용할 수 있습니다. (Aggregate 역할을 Sequence 프로토콜이 수행합니다.)

Apple 공식 문서의 예제를 가져왔습니다.

Language:swift
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 형식이 아니라 하더라도 순회 로직을 추가함으로서 순회 가능한 객체를 만들 수 있습니다.

References