Patrón de diseño Iterador

Este post es el primero de una serie de posts relativos a los patrones de diseño Software. Elijo este patrón por ser uno de los más conocidos, incluso para la gente que desconoce los patrones de diseño o cree no haberlos usado nunca.

Todo el mundo que esté leyendo este post, alguna vez se ha enfrentado al problema de encapsular una estructura de datos, y por tanto, se ha enfrentado a uno de sus principales problemas, ¿cómo dejar usar una estructura de datos (normalmente recorriéndola) sin perder la encapsulación?. El patrón de diseño Iterador trata de responder a este problema (y alguno más de paso).

Anteriormente (me refiero a en lenguaje C), se desarrollaban listas enlazadas definiendo estructuras de datos como la siguiente:

struct LISTA
{
    /** Dato del nodo de la lista, en este ejemplo un entero, pero podría ser cualquier otro. */
    int dato;
    /** Puntero al siguiente elemento de la lista. */
    struct LISTA *siguiente;
};

Aunque se desarrollaban funciones de utilidad, para tareas como: Crear un nodo, insertarlo en una posición dado o en orden, borrar nodos, … y por tanto estas tareas estaban “encapsuladas” en funciones, aún nos quedaban varios problemas a resolver:

  1. La encapsulación es voluntaria, dado que cualquiera puede acceder a los nodos de la lista a su voluntad.
  2. Para recorrer los datos de la lista, es necesario operar con los nodos, con la consecuente peligrosidad de tal acción, dado que los accesos a la lista son desde cualquier sitio que use dicha lista.
  3. El recorrido de la lista es engorroso.
  4. Tenemos que hacer una estructura de datos lista (y funciones de acceso) para cada tipo de datos que queramos, o hacer que el tipo de dato sea un void *, lo cual es peligroso (dado que hay que hacer castings todo el tiempo) y poco práctico (por la misma razón).
  5. Además adolece de estas otros problemas: poca usabilidad, mantenibilidad, y además es muy poco elegante!

La propuesta para solucionar estos problemas en C++ es usar un template y el patrón de diseño Iterador.

En el siguiente diagrama de clases podemos verlo con más claridad:


[graphviz name=PatronIteradorDiagramaClases]
node [shape=record];
rankdir=BT;
{ rank=same; Iterador [label=”{Iterador||virtual operator ++ () = 0;\nvirtual operator — () = 0;\nvirtual operator * () = 0;\nvirtual operator == (const Iterator &otro) = 0;}”]; }
{ rank=same; ListaIterador [label=”{Lista::Iterador|Lista *lista;\nNODO *nodo;|operator ++ ();\noperator — ();\noperator * ();\noperator == (const Iterator &otro);}”]; }
{ rank=same; Lista [label=”{Lista|NODO *nodos;|Iterador Inicio ();\nIterador Fin ();\nIterador Insertar (const T &dato);}”]; }
{ rank=same; ClienteA [label=”{ClienteA|Lista lista;|void MetodoA ();}”]; ClienteB [label=”{ClienteB||void MetodoB (Iterador *inicio, Iterador *fin);}”]; }
ListaIterador -> Iterador [arrowhead=onormal];
ListaIterador -> Lista [arrowhead=vee, arrowtail=odiamond];
Lista -> ListaIterador [arrowhead=vee, style=dashed];
ClienteA -> Lista [arrowhead=vee, arrowtail=diamond];
ClienteA -> ListaIterador [arrowhead=vee, style=dashed];
ClienteA -> ClienteB [arrowhead=vee, style=dashed];
ClienteB -> Iterador [arrowhead=vee, style=dashed];
[/graphviz]

Hay varios participantes:

  • Iterador Esta clase es una interfaz, que proporcionará las definiciones de los métodos que todo iterador deberá cumplir, de este modo desacoplamos el uso de iteradores, de cada una de los iteradores particulares de las estructuras de datos.
  • Lista Esta clase representa a la clase template de una estructura de datos lista enlazada, donde esta clase será la encargada, además de la estructura de datos y sus métodos, de crear iteradores del tipo particular para lista enlazada (Lista::Iterador).
  • Lista::Iterador Esta clase (clase interna pública de Lista) es la implementación del Iterador genérico, que sabe cómo recorrer los nodos de la clase Lista.

Se han dibujado dos tipos de clientes también:

  • ClienteA Este cliente tiene un objeto de la clase Lista y usa iteradores.
  • ClienteB Este otro cliente, no contiene al objeto de la clase Lista (como el anterior), sino que sólo la usa, y gracias al interfaz Iterador está desacoplado de la implementación particular.

Finalmente podemos ver a continuación cómo se usaría todo en la implementación de los métodos de ClienteA y de ClienteB:

void ClienteA::MetodoA ()
{
    // Declaramos dos iteradores.
    Lista::Iterador inicio, fin;
    // Declaramos un objeto ClienteB.
    ClienteB clienteB;
 
    // Inicializamos los iteradores por medio de la lista.
    inicio = lista.Inicio ();
    fin = lista.Fin ();
 
    // Ahora se los pasamos a Cliente B.
    clienteB.MetodoB (&inicio, &fin);
}
 
void ClienteB::MetodoB (Iterador *inicio, Iterador *fin)
{
    // Iteramos hasta alcanzar el final, consiguiendo no acoplarnos con el tipo final.
    while ((*inicio) != (*fin))
    {
        // Hacemos algo con el dato del iterador...
        **inicio;
        // Avanzamos la posición del iterador.
        (*inicio)++;
    }
}

Deja un comentario