El patrón arquitectónico iterador en el universo rápido

"Iterator" es uno de los patrones de diseño que los programadores a menudo no notan, porque su implementación, como regla, está integrada directamente en las herramientas estándar del lenguaje de programación. Sin embargo, este es también uno de los patrones de comportamiento descritos en el libro "Gang of Four", "GoF", "Patrones de diseño: elementos de software orientado a objetos reutilizables" , y comprender su dispositivo nunca duele, y a veces incluso puede ayudar en algo.

Un "iterador" es un método de acceso secuencial a todos los elementos de un objeto compuesto (en particular, los tipos de contenedor, como una matriz o conjunto).

Herramientas de lenguaje estándar


Crea algún tipo de matriz :

let numbersArray = [0, 1, 2] 

... y luego "caminar" a través de él en un ciclo :

 for number in numbersArray { print(number) } 

... parece una acción muy natural, especialmente para lenguajes de programación modernos como Swift . Sin embargo, detrás de escena de esta acción simple hay un código que implementa los principios del patrón Iterator.

En "Swift", para poder "iterar" una variable usando for -cycles, el tipo de variable debe implementar el protocolo de Sequence . Entre otras cosas, este protocolo requiere que el tipo tenga un Iterator tipo associatedtype , que a su vez debe implementar los requisitos del protocolo IteratorProtocol , así como el método de fábrica makeIterator() , que devuelve un "iterador" específico para este tipo:

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

El protocolo IteratorProtocol , a su vez, contiene solo un método: next() , que devuelve el siguiente elemento en la secuencia:

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

Parece "mucho código complicado", pero en realidad no lo es. A continuación veremos esto.

El tipo Array implementa el protocolo de Sequence (aunque no directamente, sino a través de la cadena de herencia del protocolo : MutableCollection hereda los requisitos de la Collection , y este último hereda los requisitos de la Sequence ), por lo que sus instancias, en particular, se pueden "iterar" utilizando for -cycles.

Tipos personalizados


¿Qué se debe hacer para poder iterar su propio tipo? Como suele suceder, es más fácil mostrar un ejemplo.

Supongamos que hay un tipo que representa una estantería que almacena un cierto conjunto de instancias de una clase, que a su vez representa un libro:

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

Para poder "iterar" una instancia de la clase Shelf , esta clase debe cumplir los requisitos del protocolo de Sequence . Para este ejemplo, será suficiente implementar el método makeIterator() , especialmente porque los otros requisitos del protocolo tienen implementaciones predeterminadas . Este método debería devolver una instancia de un tipo que implementa el protocolo IteratorProtocol . Afortunadamente, en el caso de Swift, este es un código muy pequeño y muy simple:

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

El método next() del tipo ShelfIterator declara mutating , porque la instancia de tipo de alguna manera debe almacenar el estado correspondiente a la iteración actual:

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

Esta opción de implementación siempre devuelve el primer elemento de la secuencia, o nil si la secuencia está vacía. El bloque de defer "envuelve" con el código para cambiar la colección iterada, que elimina el elemento del último paso de iteración inmediatamente después de que el método regrese.

Ejemplo 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 los tipos utilizados (incluido el Shelf subyacente de la Array ) se basan en la semántica de los valores (a diferencia de las referencias) , no tiene que preocuparse por el cambio del valor de la variable original durante la iteración. Al manejar tipos basados ​​en la semántica de enlaces, este punto debe tenerse en cuenta y tenerse en cuenta al crear sus propios iteradores.

Funcionalidad clásica


El clásico "iterador" descrito en el libro "Gangs of Four", además de devolver el siguiente elemento de la secuencia iterable, también puede devolver en cualquier momento el elemento actual en el proceso de iteración, el primer elemento de la secuencia iterable y el valor de la "bandera" que indica si todavía hay elementos en una secuencia iterada en relación con el paso de iteración actual.

Por supuesto, sería fácil declarar un protocolo ampliando así las capacidades del protocolo de IteratorProtocol estándar:

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

Los elementos primero y actual se devuelven opcionales ya que la secuencia fuente puede estar vacía.

Opción de implementación simple:

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

En la descripción original del patrón, el método next() cambia el estado interno del iterador para ir al siguiente elemento y es de tipo Void , y el elemento actual es devuelto por el método currentElement() . En el protocolo IteratorProtocol , estas dos funciones se combinan en una sola.

La necesidad del first() método first() también es dudosa, porque el iterador no cambia la secuencia original, y siempre tenemos la oportunidad de acceder a su primer elemento (si lo hay, por supuesto).

Y, dado que el método next() devuelve nil cuando finaliza la iteración, el método isDone() también se vuelve inútil.

Sin embargo, para fines académicos, es bastante posible encontrar una función que pueda utilizar la funcionalidad completa:

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

El parámetro iterator se declara inout porque su estado interno cambia durante la ejecución de la función. Y cuando se llama a la función, la instancia del iterador se transmite no directamente por su propio valor, sino por referencia.

El resultado de llamar al método next() no se utiliza, simulando la ausencia del valor de retorno de una implementación de libro de texto.

En lugar de una conclusión


Esto parece ser todo lo que quería decir esta vez. Todo hermoso código y escritura deliberada!

Mis otros artículos sobre patrones de diseño:

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


All Articles