¿Qué hace que el iterador sea un patrón de diseño?

9

Me he estado preguntando qué es lo que hace que el Iterator sea especial en comparación con otras construcciones similares, y que hizo que la Gang of Four lo enumerara como un patrón de diseño.

El iterador se basa en el polimorfismo (una jerarquía de colecciones con una interfaz común) y la separación de preocupaciones (la iteración sobre las colecciones debe ser independiente de la forma en que se estructuran los datos).

Pero, ¿qué sucede si reemplazamos la jerarquía de colecciones con, por ejemplo, una jerarquía de objetos matemáticos (entero, flotante, complejo, matriz, etc.) y el iterador por una clase que representa algunas operaciones relacionadas en estos objetos, por ejemplo, funciones de potencia. El diagrama de clases sería el mismo.

Probablemente podríamos encontrar muchos más ejemplos similares como Writer, Painter, Encoder y probablemente mejores, que funcionen de la misma manera. Sin embargo, nunca he oído que ninguno de estos se llame Patrón de diseño.

Entonces, ¿qué hace que el iterador sea especial?

¿Es el hecho de que es más complicado porque requiere un estado mutable para almacenar la posición actual dentro de la colección? Pero entonces, el estado mutable generalmente no se considera deseable.


Para aclarar mi punto, permítanme dar un ejemplo más detallado.

Aquí está nuestro problema de diseño:

Digamos que tenemos una jerarquía de clases y una operación definida en los objetos de estas clases. La interfaz de esta operación es la misma para cada clase, pero las implementaciones pueden ser completamente diferentes. También se supone que tiene sentido aplicar la operación varias veces en el mismo objeto, por ejemplo, con diferentes parámetros.

Aquí hay una solución sensata para nuestro problema de diseño (prácticamente una generalización del patrón iterador):

Para la separación de preocupaciones, las implementaciones de la operación no deben agregarse como funciones a la jerarquía de clases original (objetos de operando). Dado que queremos aplicar la operación varias veces en el mismo operando, debe estar representado por un objeto que tenga una referencia al operando, no solo por una función. Por lo tanto, el objeto operando debe proporcionar una función que devuelva el objeto que representa la operación. Este objeto proporciona una función que realiza la operación real.

Un ejemplo:

Hay una clase base o interfaz MathObject(nombre estúpido, lo sé, tal vez alguien tiene una mejor idea) con clases derivadas MyIntegery MyMatrix. Para cada MathObjectuna se Powerdebe definir una operación que permita el cálculo de cuadrado, cubo, etc. Entonces podríamos escribir (en Java):

MathObject i = new MyInteger(5);
Power powerOfFive = i.getPower();
MyInteger square = powerOfFive.calculate(2); // should return 25
MyInteger cube = powerOfFive.calculate(3); // should return 125
Frank Puffer
fuente
55
"El diagrama de clases sería el mismo", ¿y qué? Un patrón de diseño no es un diagrama de clase. Es una abstracción de alto nivel para una clase de soluciones a un problema recurrente.
Doc Brown
@DocBrown: Correcto, pero ¿no son operaciones matemáticas, escribir objetos en un archivo, salida gráfica o codificar problemas recurrentes de datos, como la iteración?
Frank Puffer
La elección del patrón de diseño es subjetiva (es decir, a los ojos de los "diseñadores", o las personas que juzgan los diseños). La denominación de los patrones de diseño tiene la intención de ser independiente del dominio (para que no nos distraigamos pensando que es específico del dominio). Solo mi opinión, no tengo referencias para citar.
rwong
@FrankPuffer Si describe una solución común para escribir objetos en un archivo, puede escribir su solución y llamarla Patrón de escritura de objetos si hacerlo es útil.
Brandin
3
Estás pensando demasiado en esto. Un patrón de diseño es una solución bien conocida para un problema informático común, y eso es todo. Utiliza el patrón cuando puede reconocer y aplicar los beneficios que proporciona.
Robert Harvey

Respuestas:

9

La mayoría de los patrones del libro GoF tienen las siguientes cosas en común:

  • resuelven problemas de diseño básicos , utilizando medios orientados a objetos
  • las personas a menudo enfrentan estos problemas amables en programas arbitrarios, independientemente del dominio o negocio
  • son recetas para hacer que el código sea más reutilizable, a menudo haciéndolo más SÓLIDO
  • presentan soluciones canónicas a estos problemas

Los problemas resueltos por estos patrones son tan básicos que muchos desarrolladores los entienden principalmente como soluciones para las características faltantes del lenguaje de programación , lo cual es un punto de vista válido (tenga en cuenta que el libro GoF es de 1995, donde Java y C ++ no ofrecían tantos características como hoy).

El patrón iterador encaja bien en esta descripción: resuelve un problema básico que ocurre con mucha frecuencia, independientemente de cualquier dominio específico, y como usted mismo escribió, es un buen ejemplo de "separación de preocupaciones". Como seguramente sabrá, el soporte de iterador directo es algo que se encuentra en muchos lenguajes de programación actuales.

Ahora compare esto con los problemas que eligió:

  • escribir en un archivo, eso es en mi humilde opinión simplemente no lo suficientemente "básico". Es un problema muy específico. Tampoco hay una buena solución canónica: hay muchos enfoques diferentes sobre cómo escribir en un archivo, y no hay una "mejor práctica" clara.
  • Pintor, codificador: lo que sea que tenga en mente con eso, esos problemas me parecen aún menos básicos y ni siquiera independientes del dominio.
  • Tener la función de "potencia" disponible para diferentes tipos de objetos: a primera vista, podría valer la pena ser un patrón, pero su solución propuesta no me convence, parece más un intento de calzar la función de potencia en algo similar a El patrón iterador. Implementé una gran cantidad de código con cálculos de ingeniería, pero no puedo recordar una situación en la que un enfoque similar a su objeto de función de potencia me hubiera ayudado (sin embargo, los iteradores son algo con lo que tengo que lidiar a diario).

Además, no veo nada en su ejemplo de función de potencia que no pueda interpretarse como una aplicación del patrón de estrategia o el patrón de comando, lo que significa que esas partes básicas ya están en el libro GoF. Una mejor solución podría contener métodos de sobrecarga del operador o de extensión, pero esas son cosas que están sujetas a las características del lenguaje, y eso es exactamente lo que el "OO significa" utilizado por la "Pandilla" no podría proporcionar.

Doc Brown
fuente
The problems solved by these patterns are so basic that many developers think their main purpose is to be workarounds for missing programming language features- La ironía es que los desarrolladores de software usan rutinariamente patrones de diseño de software que tienen 20 años de edad y siguen creyendo que están escribiendo código de última generación.
Robert Harvey
@RobertHarvey: No creo que muchos desarrolladores implementen el patrón iterador hoy en la "forma OO" sugerida por el GoF. Lo implementan típicamente por los medios provistos por el lenguaje o es lib estándar (por ejemplo, en C # usando IEnumerabley yield). Pero para otros patrones de GoF, lo que escribiste probablemente sea cierto.
Doc Brown
1
Relevante para workarounds for missing programming language features: blog.plover.com/prog/johnson.html
jrw32982 apoya a Monica el
8

The Gang of Four cita la definición del patrón de Christopher Alexander:

Cada patrón describe un problema que ocurre una y otra vez en nuestro entorno, y luego describe el núcleo de la solución a ese problema [...]

¿Cuál es el problema resuelto por los iteradores?

Intención: proporcionar una forma de acceder a los elementos de un objeto agregado de forma secuencial sin exponer su representación subyacente.

...

Aplicabilidad: use el patrón Iterator

  • para acceder al contenido de un objeto agregado sin exponer su representación interna
  • para soportar múltiples recorridos de objetos agregados
  • para proporcionar una interfaz uniforme para atravesar diferentes estructuras agregadas (es decir, para soportar la iteración polimórfica).

Entonces, uno podría argumentar que el patrón iterador es, por definición, específico del dominio para las colecciones. Y eso está perfectamente bien. Otros patrones, como el patrón de intérprete, son específicos de dominio para lenguajes específicos de dominio, los patrones de fábrica son específicos de dominio para la creación de objetos, ... Por supuesto, esta es una comprensión bastante tonta de "específico de dominio". Mientras sea un par recurrente de problema-solución, podemos llamarlo un patrón.

Y es bueno que exista el patrón Iterator. Suceden cosas malas si no lo usas. Mi anti-ejemplo favorito es Perl. Aquí, cada colección (matriz o hash) incluye el estado del iterador como parte de la colección. ¿Por qué es esto malo? Podemos iterar fácilmente sobre un hash con un while, cada bucle:

while (my ($key, $value) = each %$hash) {
  say "$key => $value";
}

¿Pero qué pasa si llamamos a una función en el cuerpo del bucle?

while (my ($key, $value) = each %$hash) {
  do_something_with($key, $value, $hash);
}

Esta función ahora puede hacer casi lo que quiera, excepto:

  • agregue o elimine entradas hash, ya que estas alterarían el orden de iteración de manera impredecible (en C ++, por ejemplo, esto invalidaría el iterador).
  • iterar la misma tabla hash sin hacer una copia, ya que eso consumiría el mismo estado de iteración. Ups

Si la función llamada debe usar el iterador, el comportamiento de nuestro bucle se vuelve indefinido. Eso es un problema. Y el patrón iterador tiene una solución: poner todo el estado de iteración en un objeto separado que se crea por iteración.

Sí, por supuesto, el patrón iterador está relacionado con otros patrones. Por ejemplo, ¿cómo se instancia el iterador? En Java, tenemos un genérico Iterable<T>y una Iterator<T>interfaz. Un iterable concreto como ArrayList<T>crea un tipo particular de iterador, mientras que un HashSet<T>puede proporcionar un tipo de iterador completamente diferente. Eso me recuerda mucho el patrón abstracto de fábrica, donde Iterable<T>es la fábrica abstracta y Iteratorel producto.

Un iterador polimórfico también puede interpretarse como un ejemplo del patrón de estrategia. Por ejemplo, un árbol puede ofrecer diferentes tipos de iteradores (pre-orden, en orden, post-orden, ...). Externamente, todos compartirían una interfaz de iterador y producirían elementos en alguna secuencia. El código del cliente solo debe depender de la interfaz del iterador, no de ningún algoritmo de recorrido de árbol en particular.

Los patrones no existen de forma aislada, independientes entre sí. Algunos patrones son soluciones diferentes para el mismo problema, y ​​algunos patrones describen la misma solución en diferentes contextos. Algunos patrones implican otro. Además, el espacio del patrón no se cierra cuando pasa la última página del libro de Patrones de diseño (consulte también su pregunta anterior ¿La Banda de los Cuatro exploró a fondo el "Espacio del patrón"? ). Los patrones descritos en el libro de Patrones de diseño son muy flexibles y amplios, abiertos a variaciones infinitas, y definitivamente no son los únicos patrones existentes.

Los conceptos que enumera (escritura, pintura, codificación) no son patrones porque no describen una combinación de problema y solución. Una tarea como "Necesito escribir datos" o "Necesito codificar datos" no es realmente un problema de diseño y no incluye una solución; una "solución" que solo consiste en "lo sé, crearé una clase de escritor" no tiene sentido. Pero si tenemos un problema como "No quiero que los gráficos a medio renderizar se pinten en la pantalla", entonces puede existir un patrón: "¡Lo sé, usaré gráficos con doble búfer!"

amon
fuente
Buena respuesta, gracias. Todavía no estoy completamente convencido de que lo que escribes en el último párrafo se aplica aquí. He editado mi pregunta para explicar a qué me refiero.
Frank Puffer