Паттерн Итератор

В данной заметке я опишу паттерн Итератор.
Данный паттерн относится к поведенческим шаблонам проектирования.

Распечатай это

Допустим нам нужно распечатать список треков с альбома “Procrastinate them all” группы “Procrastinallica”.
Наивная имплементация (Swift) выглядит так:

for i=0; i < tracks.count; i++ {
    print(tracks[i].title)
}

Вдруг при сборке обнаруживается что класс объекта tracks не отдает количество треков в вызове count, мало того, еще и к его элементам нельзя обратиться по индексу. Ой...

Отфильтруй

Допустим мы пишем статью для журнала "Wacky Hammer", нам нужен список треков группы "Djentuggah" в которых bpm превышает 140 ударов в минуту. Интересная особенность этой группы, что ее записи хранятся в огромной коллекции underground групп, не отсортированная по альбомам, или по каким-либо другим признакам.
Представим себе что работаем с языком без функциональных возможностей:

var djentuggahFastTracks = [Track]()

for track in undergroundCollectionTracks {
    if track.band.title == "Djentuggah" && track.info.bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

Вдруг в коллекции оцифрованных кассет обнаруживается пару треков группы, и редактор журнала предлагает найти в этой коллекции треки и написать о них.
Знакомый Data Scientist сообщает, что вооружившись ML алгоритмом классификации треков Djentuggah можно будет не прослушивать коллекцию из 200 тысяч кассет вручную, а определить их автоматически.
Попробуем:

var djentuggahFastTracks = [Track]()

for track in undergroundCollectionTracks {
    if track.band.title == "Djentuggah" && track.info.bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

let tracksClassifier = TracksClassifier()
let bpmClassifier = BPMClassifier()

for track in cassetsTracks {
    if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

Ошибаемся

Теперь перед самой отправкой в печать, редактор сообщает что 140 ударов в минуту вышли из моды, людей больше интересуют 160, поэтому статью надо переписать, добавив необходимые треки.
Переписываем:

var djentuggahFastTracks = [Track]()

for track in undergroundCollectionTracks {
    if track.band.title == "Djentuggah" && track.info.bpm == 160 {
        djentuggahFastTracks.append(track)
    }
}

let tracksClassifier = TracksClassifier()
let bpmClassifier = BPMClassifier()

for track in cassetsTracks {
    if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

Самые внимательные заметили ошибку, параметр bpm изменен только для первого прохода по списку. Если бы проходов по коллекциям было больше, то и шанс ошибится был бы выше, именно потому нужно использовать принцип DRY.
Приведенный выше пример можно развивать и дальше, например добавив условие что нужно найти несколько групп с разным bpm, по именам вокалистов, гитаристов, это будет увеличивать шанс ошибки из-за дублирования кода.

Добавляем итератор

В литературе итератор описывается как совокупность двух протоколов/интерфейсов, первый это интерфейс итератора состоящий из двух методов - next(), hasNext()
next() отдает обьект из коллекции, а hasNext() сообщает что дальше есть объект и список не закончился.
Однако на практике я наблюдал итераторы с одним методом - next(), когда список заканчивался, из этого обьекта возвращался null.
Второй это коллекция которая должна иметь интерфейс отдающий итератор - метод iterator(), есть вариации с интерфейсом коллекции которая возвращает итератор в начальной позиции и в конечной - методы begin() и end() - используется в C++ std.
Использование итератора в приведенном выше примере позволит убрать дублирование кода, устранит шанс ошибиться из-за дублирования условий фильтрации. Также будет проще работать с коллекцией треков по единому интерфейсу - при изменении внутренней структуры коллекции, интерфейс останется старым и внешний код затронут не будет.

Перепишем:

let bandFilter = Filter(key: "band", value: "Djentuggah")
let bpmFilter = Filter(key: "bpm", value: 140)
let iterator = tracksCollection.filterableIterator(filters: [bandFilter, bpmFilter])

while let track = iterator.next() {
    print("\(track.band) - \(track.title)")
}

Изменение коллекции и я

Во время работы итератора коллекция может измениться, таким образом приводя внутренний счетчик итератора в некорректное состояние и вообще ломая такое понятие как "следующий объект". Многие фреймворки содержат проверку на изменение состояние коллекции, и в случае изменений возвращают ошибку/exception. Некоторые реализации позволяют удалять объекты из коллекции во время работы итератора, предоставляя метод remove() в итераторе.

Источники

https://refactoring.guru/ru/design-patterns/iterator

Исходный код

https://gitlab.com/demensdeum/patterns/