O Padrão Arquitetônico Iterador no Universo Rápido

Um "iterador" é um dos padrões de design que os programadores geralmente não percebem, porque sua implementação, como regra, é embutida diretamente nas ferramentas padrão da linguagem de programação. No entanto, esse também é um dos padrões comportamentais descritos no livro “Gang of Four”, “GoF”, “Design Patterns: Elementos de Software Orientado a Objetos Reutilizáveis” e compreende seu dispositivo nunca dói, e às vezes pode até ajudar em algo.

Um "iterador" é um método de acesso seqüencial a todos os elementos de um objeto composto (em particular, tipos de contêiner, como uma matriz ou conjunto).

Ferramentas de linguagem padrão


Crie algum tipo de matriz :

let numbersArray = [0, 1, 2] 

... e depois "caminhe" por um ciclo :

 for number in numbersArray { print(number) } 

... parece uma ação muito natural, especialmente para linguagens de programação modernas como Swift . No entanto, nos bastidores dessa ação simples, há um código que implementa os princípios do padrão Iterator.

Em "Swift", para poder "iterar" uma variável usando for , o tipo de variável deve implementar o protocolo Sequence . Entre outras coisas, esse protocolo exige que o tipo tenha um Iterator tipo associatedtype , que por sua vez deve implementar os requisitos do protocolo IteratorProtocol , bem como o método de fábrica makeIterator() , que retorna um "iterador" específico para este tipo:

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

O protocolo IteratorProtocol , por sua vez, contém apenas um método - next() , que retorna o próximo elemento na sequência:

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

Parece "muito código complicado", mas na verdade não é. Abaixo veremos isso.

O tipo Array implementa o protocolo Sequence (embora não diretamente, mas através da cadeia de herança do protocolo : o MutableCollection herda os requisitos de Collection e o último herda os requisitos de Sequence ), para que suas instâncias, em particular, possam ser "iteradas" usando for -cycles.

Tipos personalizados


O que precisa ser feito para poder iterar seu próprio tipo? Como costuma acontecer, é mais fácil mostrar um exemplo.

Suponha que haja um tipo representando uma estante de livros que armazena um determinado conjunto de instâncias de uma classe, que por sua vez representa um livro:

 struct Book { let author: String let title: String } struct Shelf { var books: [Book] } 

Para poder "iterar" uma instância da classe Shelf , essa classe deve atender aos requisitos do protocolo Sequence . Neste exemplo, basta implementar o método makeIterator() , principalmente porque os outros requisitos de protocolo têm implementações padrão . Este método deve retornar uma instância de um tipo que implementa o protocolo IteratorProtocol . Felizmente, no caso do Swift, esse código é muito pouco, muito simples:

 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) } } 

O método next() do tipo ShelfIterator declarado mutating , porque a instância do tipo deve de alguma forma armazenar o estado correspondente à iteração atual:

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

Esta opção de implementação sempre retorna o primeiro elemento da sequência ou nil se a sequência estiver vazia. O bloco de defer "empacotado" com o código para alterar a coleção iterada, que remove o elemento da última etapa da iteração imediatamente após o retorno do método.

Exemplo de uso:

 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)") } /* .  –  .  –   .  –   */ 

Porque todos os tipos usados ​​(incluindo a Array subjacente à Shelf ) são baseados na semântica dos valores (em oposição às referências) , não é necessário se preocupar com o valor da variável original sendo alterada durante a iteração. Ao manipular tipos com base na semântica de links, esse ponto deve ser lembrado e levado em consideração ao criar seus próprios iteradores.

Funcionalidade clássica


O “iterador” clássico descrito no livro “Gangs of Four”, além de retornar o próximo elemento da sequência iterável, também pode a qualquer momento retornar o elemento atual no processo de iteração, o primeiro elemento da sequência iterável e o valor da “flag” indicando se ainda há elementos em uma sequência iterada em relação à etapa de iteração atual.

Obviamente, seria fácil declarar um protocolo, expandindo assim os recursos do IteratorProtocol padrão:

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

Os elementos primeiro e atual são retornados opcionais, pois a sequência de origem pode estar vazia.

Opção de implementação simples:

 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 } } 

Na descrição original do padrão, o método next() altera o estado interno do iterador para ir para o próximo elemento e é do tipo Void , e o elemento atual é retornado pelo método currentElement() . No protocolo IteratorProtocol , essas duas funções são como se combinadas em uma.

A necessidade do first() método first() também é duvidosa, porque o iterador não altera a sequência original e sempre temos a oportunidade de acessar seu primeiro elemento (se houver, é claro).

E, como o método next() retorna nil quando a iteração é concluída, o método isDone() também se torna inútil.

No entanto, para fins acadêmicos, é bem possível criar uma função que possa usar toda a funcionalidade:

 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. .  –   */ 

O parâmetro iterator é declarado inout porque seu estado interno muda durante a execução da função. E quando a função é chamada, a instância do iterador é transmitida não diretamente por seu próprio valor, mas por referência.

O resultado da chamada do método next() não é usado, simulando a ausência do valor de retorno de uma implementação de livro didático.

Em vez de uma conclusão


Isso parece ser tudo que eu queria dizer dessa vez. Todo o código bonito e deliberado escrevê-lo!

Meus outros artigos sobre padrões de design:

Source: https://habr.com/ru/post/pt437614/


All Articles