Архитектурный шаблон «Итератор» («Iterator») во вселенной «Swift»

в 14:40, , рубрики: iOS, ios development, ios programming, iOS разработка, oop patterns, pop, protocol, protocol-oriented programming, protocols, swift, swift development, swift разработка, ооп, разработка под iOS

«Итератор» – один из шаблонов проектирования, которые программисты чаще всего не замечают, потому что его реализация, как правило, встроена непосредственно в стандартные средства языка программирования. Тем не менее, это тоже один из поведенческих шаблонов, описанных в книге «Банды четырех» (“Gang of Four”, “GoF”) “Шаблоны проектирования” (“Design Patterns: Elements of Reusable Object-Oriented Software”), и понимать его устройство никогда не помешает, а иногда даже может в чем-то помочь.

«Итератор» представляет собой способ последовательного доступа ко всем элементам составного объекта (в частности, контейнерных типов, таких как массив или набор).

Стандартные средства языка

Создать какой-нибудь массив:

let numbersArray = [0, 1, 2]

…а потом «пройтись» по нему циклом:

for number in numbersArray {
    print(number)
}

…кажется очень естественным действием, особенно для современных языков программирования, таких как «Swift». Тем не менее, за «кулисами» этого простого действия находится код, реализующий принципы шаблона «Итератор».

В «Swift» для того, чтобы иметь возможность «итерировать» переменную с помощью for-циклов, тип переменной должен реализовывать протокол Sequence. Помимо прочего, этот протокол требует от типа иметь associatedtype Iterator, который в свою очередь должен реализовывать требования протокола IteratorProtocol, а также фабричный метод makeIterator(), который возвращает конкретный «итератор» для данного типа:

protocol Sequence {
    associatedtype Iterator : IteratorProtocol
    func makeIterator() -> Self.Iterator
    // Another requirements go here…
}

Протокол IteratorProtocol в свою очередь содержит в себе всего один метод – next(), который возвращает следующий элемент в последовательности:

protocol IteratorProtocol {
    associatedtype Element
    mutating func next() -> Self.Element?
}

Звучит как «много сложного кода», но на самом деле это не так. Чуть ниже мы в этом убедимся.

Тип Array реализовывает протокол Sequence (правда, не напрямую, а через цепочку наследования протоколов: MutableCollection наследует требования Collection, а тот – требования Sequence), поэтому его экземпляры, в частности, могут быть «итерированы» с помощью for-циклов.

Пользовательские типы

Что необходимо сделать, чтобы смочь итерировать свой собственный тип? Как это часто бывает, проще всего показать на примере.

Допустим, имеется тип, представляющий книжную полку, который хранит в себе некий набор экземпляров класса, представляющего в свою очередь книгу:

struct Book {
    let author: String
    let title: String
}

struct Shelf {
    var books: [Book]
}

Чтобы иметь возможность «итерировать» экземпляр класса Shelf, этот класс должен соответствовать требованиям протокола Sequence. Для данного примера будет достаточно лишь реализовать метод makeIterator(), тем более что остальные требования протокола имеют реализации по умолчанию. Этот метод должен вернуть экземпляр типа, реализующего протокол IteratorProtocol. К счастью, в случае со «Swift» это очень мало очень простого кода:

struct ShelfIterator: IteratorProtocol {
    private var books: [Book]

    init(books: [Book]) {
        self.books = books
    }

    mutating func next() -> Book? {
        // TODO: Return next underlying Book element.
    }
}

extension Shelf: Sequence {
    func makeIterator() -> ShelfIterator {
        return ShelfIterator(books: books)
    }
}

Метод next() типа ShelfIterator объявлен mutating, потому что экземпляр типа должен тем или иным образом хранить в себе состояние, соответствующее текущей итерации:

mutating func next() -> Book? {
    defer {
        if !books.isEmpty { books.removeFirst() }
    }

    return books.first
}

Данный вариант реализации всегда возвращает первый элемент в последовательности либо nil, если последовательность пуста. В блок defer «обернут» код изменения итерируемой коллекции, который удаляет элемент последнего шага итерации сразу после возврата метода.

Пример использования:

let book1 = Book(author: "Ф. Достоевский",
                 title: "Идиот")
let book2 = Book(author: "Ф. Достоевский",
                 title: "Братья Карамазовы")
let book3 = Book(author: "Ф. Достоевский",
                 title: "Бедные люди")
let shelf = Shelf(books: [book1, book2, book3])

for book in shelf {
    print("(book.author) – (book.title)")
}

/*
Ф. Достоевский – Идиот
Ф. Достоевский – Братья Карамазовы
Ф. Достоевский – Бедные люди
*/

Т.к. все используемые типы (включая Array, лежащий в основе Shelf) базируются на семантике значений (в противовес ссылкам), можно не беспокоиться о том, что значение исходной переменной будет изменено в процессе итерации. При обращении с типами, основанными на семантике ссылок, этот момент стоит иметь в виду и учитывать при создании собственных итераторов.

Классический функционал

Классический «итератор», описанный в книге «Банды четырех», помимо того, чтобы возвращать следующий элемент итерируемой последовательности, может также в любой момент возвращать текущий элемент в процессе итерации, первый элемент итерируемой последовательности и значение «флага», показывающего, остались ли еще элементы в итерируемой последовательности относительно текущего шага итерации.

Конечно, было бы несложно объявить протокол, расширяющий таким образом возможности стандартного IteratorProtocol:

protocol ClassicIteratorProtocol: IteratorProtocol {
    var currentItem: Element? { get }
    var first: Element? { get }
    var isDone: Bool { get }
}

Первый и текущий элементы возвращается опциональными, т.к. исходная последовательность может быть пуста.

Вариант простой реализации:

struct ShelfIterator: ClassicIteratorProtocol {
    var currentItem: Book? = nil
    var first: Book?
    var isDone: Bool = false
    private var books: [Book]
    
    init(books: [Book]) {
        self.books = books
        first = books.first
        currentItem = books.first
    }
    
    mutating func next() -> Book? {
        currentItem = books.first
        books.removeFirst()
        isDone = books.isEmpty
        return books.first
    }
}

В оригинальном описании паттерна метод next() изменяет внутреннее состояние итератора для перехода к следующему элементу и имеет тип Void, а текущий элемент возвращается методом currentElement(). В протоколе IteratorProtocol эти две функции как бы объединены в одну.

Нужда в методе first() также сомнительна, т.к. итератор не изменяет исходную последовательность, и у нас всегда есть возможность обратиться к ее первому элементу (при его наличии, конечно).

И, так как метод next() возвращает nil, когда итерация окончена, метод isDone() также становится бесполезным.

Тем не менее, в академических целях, вполне возможно придумать функцию, которая смогла бы использовать полный функционал:

func printShelf(with iterator: inout ShelfIterator) {
    var bookIndex = 0
    while !iterator.isDone {
        bookIndex += 1
        print("(bookIndex). (iterator.currentItem!.author) – (iterator.currentItem!.title)")
        _ = iterator.next()
    }
}

var iterator = ShelfIterator(books: shelf.books)
printShelf(with: &iterator)

/*
1. Ф. Достоевский – Идиот
2. Ф. Достоевский – Братья Карамазовы
3. Ф. Достоевский – Бедные люди
*/

Параметр iterator объявлен inout, т.к. его внутреннее состояние меняется в процессе выполнения функции. И при вызове функции экземпляр итератора передается не напрямую собственным значением, а ссылкой.

Результат вызова метода next() не используется, имитируя отсутствие возвращаемого значения хрестоматийной реализации.

Вместо заключения

Кажется, это все, что мне хотелось сказать в этот раз. Всем красивого кода и осознанного его написания!

Другие мои статьи о шаблонах проектирования:
Архитектурный шаблон «Посетитель» (“Visitor”) во вселенной «iOS» и «Swift»

Автор: Никита Лазарев-Зубов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js