Patrones de Comportamiento (I): Patrón Iterator


Objetivo:

«Proporcionar una forma de acceder a los elementos de un objeto agregado de forma secuencial sin exponer sus detalles».

Design Patterns: Elements of Reusable Object-Oriented Software

Pese a que no seamos conscientes de ello, cuando programamos utilizamos el patrón Iterator a diario. Casi todas las estructuras de datos que representan colecciones utilizan de algún modo este patrón para proporcionar acceso secuencial a los elementos que las conforman, y tanto Java como .NET ofrecen interfaces que nos invitan a implementar este patrón codificando su comportamiento.

Ojo al detalle: hemos dicho recorrer secuencialmente, esto es, hacer uso de un proceso que sea capaz de situarse en el primer elemento de una colección y obtener la información de ese contenido. Tras esto, dado que hemos dicho que se trata de una operación secuencial, deberemos ser capaces de pasar del elemento actual al elemento al siguiente, obteniendo también su contenido. Por último, será necesario implementar algún mecanismo que nos informe si hemos alcanzado el final de la colección para detener el proceso de iteración.

Por lo tanto, el patrón Iterator debe proporcionar la siguiente funcionalidad:

  • Obtener una referencia al elemento actual de la colección.
  • Obtener una referencia al siguiente elemento de la colección (el situado a continuación del elemento actual).
  • Obtener información sobre si existen más elementos después del actual.
  • Reiniciar la colección para que el iterador apunte nuevamente al primer elemento de la colección.

Seguramente podremos pensar que no tiene mucho sentido implementar nuestro propio patrón Iterator, ya que prácticamente todas las colecciones implementan todas estas operaciones (e incluso alguna más). Sin embargo, existirán muchos supuestos en los que nos será útil.

Comencemos por el principio. Imaginemos que disponemos de una fábrica de vehículos. Esta fábrica guarda un registro pormenorizado de los vehículos que fabrica, que están modelados a través de la siguiente clase:

    public class Vehiculo
    {
        public string Marca { get; set; }
        public string Modelo { get; set; }
        public DateTime FechaFabricacion { get; set; }
        public double Precio { get; set; }

        public Vehiculo(string marca, string modelo, 
            DateTime fechaFabricacion, double precio)
        {
            this.Marca = marca;
            this.Modelo = modelo;
            this.FechaFabricacion = fechaFabricacion;
            this.Precio = precio;
        }

        public string CaracteristicasVehiculo()
        {
            return Marca + " " + Modelo + " fabricado en " +
                FechaFabricacion.ToShortDateString() + " con un precio de " +
                Precio + " euros.\n";
        }
    }

Como nuestros ingenieros han sido previsores, han decidido que la estructura de datos en la cual se almacenarán los vehículos implemente una interfaz, a la que llamaremos IRegistroVehiculos. Esta interfaz definirá las operaciones básicas que se harán sobre el registro, que serán insertar un nuevo vehículo y mostrar información sobre éste (un ejemplo más completo también incluiría métodos para eliminar y modificar, pero nos centraremos de momento en esta funcionalidad básica para no desviarnos del ejemplo):

    public interface IRegistroVehiculos
    {
        void InsertarVehiculo(string marca, string modelo, double precio);
        Vehiculo MostrarInformacionVehiculo(int indice);
    }

Nuestra fábrica de vehículos posee un sistema de gestión que almacena los vehículos en una estructura de datos agregada, como por ejemplo un ArrayList. Nuestra clase, como podremos imaginar, implementará la interfaz IRegistroVehiculos:

    public class RegistroVehiculos : IRegistroVehiculos
    {
        private ArrayList listaVehiculos;

        public RegistroVehiculos()
        {
            this.listaVehiculos = new ArrayList();
        }

        public void InsertarVehiculo(string marca, string modelo, double precio)
        {
            Vehiculo v = new Vehiculo(marca, modelo, DateTime.Now, precio);
            listaVehiculos.Add(v);
        }

        public Vehiculo MostrarInformacionVehiculo(int indice)
        {
            return (Vehiculo)listaVehiculos[indice];
        }
    }

Iterator entra en escena

Hasta ahora hemos creado una clase que contendrá la información sobre los vehículos y otra que contendrá un listado de éstos y será capaz de añadir nuevos vehículos y recuperar un vehículo del cual le pasaremos un índice. Sin embargo, aún no hemos visto ni rastro del patrón Iterator por ningún sitio. Este patrón es, precisamente, la pieza que nos falta para que nuestro modelo esté completo. Si al método MostrarInformacionVehiculo le pasamos un índice mayor que el número de elementos que contenga el ArrayList, lo que obtendremos será una bonita excepción de índice fuera de rango. Una posible solución sería comprobar dentro del método que el número de elementos menos uno es siempre mayor o igual que el índice pasado como parámetro, pero esto no arreglaría el problema: necesitamos una estructura que sea capaz de iterar sobre la colección e informar al cliente si existen más elementos disponibles. Es hora de implementar un iterador.

Para esta implementación utilizaremos, nuevamente, una interfaz que definirá las operaciones que tendrá que realizar. Estas operaciones ya las hemos definido al principio del artículo: un método que «reinicie» el interador colocándolo en el primer elemento, otro que devuelva el elemento actual, otro que nos devuelva el siguiente elemento (incrementando el índice en una unidad) y otro que nos informe si quedan elementos disponibles en la colección:

    public interface IIteratorVehiculo
    {
        void Primero();
        Vehiculo Actual();
        Vehiculo Siguiente();
        bool QuedanElementos();
    }

Comencemos a implementar nuestro iterador. Además de heredar de IIteratorVehiculo, deberá contener una referencia al listado completo de elementos del ArrayList. El método para realizar esta operación, como habremos podido imaginar, se realizará inyectándolo a través del constructor. Además, añadiremos una variable entera que se ocupe de almacenar el índice del elemento en el que se encuentra el iterador en este momento.

    public class IteratorVehiculo : IIteratorVehiculo
    {
        // Referencia al listado completo
        private ArrayList vehiculos;

        // Almacenaremos el índice en el que se encuentra el iterador
        private int posicionActual = -1;

        // El constructor inyectará el ArrayList en el objeto
        public override IteratorVehiculo(ArrayList listado)
        {
            this.vehiculos = listado;
        }
    }

Hemos inicializado la variable posicionActual al valor -1, lo cual implica que nuestro iterador se encuentra en la posición inmediatamente anterior a la primera posición. El método Primero(), de hecho, debería realizar esta misma operación:

        // Operación 1: Reinicio del índice, colocándolo en el elemento anterior al primero
        public void Primero()
        {
            this.posicionActual = -1;
        }

Espera, espera, ¿Por qué colocarlo antes del primer elemento en lugar de referenciar directamente el primer elemento? ¿No sería más intuitivo? Sí, pero sería erróneo. Si el método Primero() colocara el cursor en el primer elemento, ¿qué ocurriría si la lista estuviera vacía? Exacto: estaríamos referenciando un elemento que no existe, y lo que es peor: no tendríamos modo alguno de que nuestro iterador nos informara si existen elementos, ya que el método QuedanElementos(), encargado de esta operación, no podría actuar sobre el primer elemento de la colección. Colocando el índice en el elemento inmediatamente anterior solucionaríamos directamente estos dos problemas.

Lo siguiente que haremos será codificar el método Actual(), que nos devolverá una referencia al elemento cuyo índice se corresponda con el almacenado en posicionActual(), realizando, eso sí, las comprobaciones pertinentes.

        // Operación 2: Acceso al elemento actual
        public Vehiculo Actual()
        {
            // Si no existen elementos, devolveremos null.
            // Si el indice actual es mayor que el mayor indice aceptable, devolveremos null.
            // Si el indice actual es -1, devolveremos null.
            if ((this.vehiculos == null) || 
                (this.vehiculos.Count == 0) || 
                (posicionActual > this.vehiculos.Count - 1) ||
                (this.posicionActual < 0))
                return null;

            // Devolvemos el elemento correspondiente al elemento actual
            else
                return (Vehiculo)this.vehiculos[posicionActual];
        }

Como el índice puede ser -1, será también necesario controlar esta posibilidad.

La siguiente operación nos ofrecerá una referencia al elemento siguiente, incrementando el índice en una unidad.

        // Operación 3: Acceso al siguiente elemento
        public Vehiculo Siguiente()
        {
            // Si no existen elementos, devolveremos null.
            // Si el indice siguiente es mayor que el mayor indice aceptable, devolveremos null.
            if ((this.vehiculos == null) || 
                (this.vehiculos.Count == 0) || 
                (posicionActual + 1 > this.vehiculos.Count - 1))
                return null;

            // Aumentamos el índice en una unidad y devolvemos ese elemento
            else
                return (Vehiculo)this.vehiculos[++posicionActual];
        }

Finalmente, el método QuedanElementos() comprobará si nos encontramos en el último elemento del listado o si, por el contrario, aún quedan elementos disponibles.

        // Operación 4: Comprobación de si existen elementos en la colección
        public bool QuedanElementos()
        {
            // Devolvemos un booleano que será true si la posición siguiente es menor o igual que el
            // máximo índice aceptable (número de elementos del array - 1).
            return (posicionActual + 1 <= this.vehiculos.Count - 1);
        }

Et voilà! Ya tenemos listo nuestro Iterator. Únicamente nos queda algo por hacer: proporcionar a nuestro listado RegistroVehiculos un método que nos genere un Iterator listo para ser utilizado con la colección contenida dentro de él. Es más, ya que tenemos disponibles sus respectivas abstracciones (las interfaces IRegistroVehiculos e IIteratorVehiculo), añadiremos un método a IRegistroVehiculo una función que nos devuelva una referencia a la interfaz IIteratorVehiculo, que será implementada por RegistroVehiculos. Por lo tanto, nuestra interfaz IRegistroVehiculos tendrá el siguiente aspecto definitivo:

    public interface IRegistroVehiculos
    {
        void InsertarVehiculo(string marca, string modelo, double precio);
        Vehiculo MostrarInformacionVehiculo(int indice);
        IIteratorVehiculo ObtenerIterator();
    }

El método correspondiente dentro de RegistroVehiculos se limitará a instanciar un objeto de la clase IteratorVehiculo (que recordemos que implementaba IIteratorVehiculo) inyectándole el ArrayList, devolviendo el objeto como resultado de la función.

        public IIteratorVehiculo ObtenerIterator()
        {
            return new IteratorVehiculo(listaVehiculos);
        }

Para probar el resultado de nuestro patrón, nos bastará el siguiente ejemplo:

            // Declaramos el registro
            IRegistroVehiculos registro = new RegistroVehiculos();

            // Insertamos unos cuantos elementos
            registro.InsertarVehiculo("Volkswagen", "Polo", 12300);
            registro.InsertarVehiculo("Volkswagen", "Golf GTI", 18900);
            registro.InsertarVehiculo("Volkswagen", "Passat", 27000);
            registro.InsertarVehiculo("Volkswagen", "Scirocco", 32100);
            registro.InsertarVehiculo("Volkswagen", "Touareg", 21800);

            // Obtenemos el iterator
            IIteratorVehiculo iterador = registro.ObtenerIterator();

            // Mientras queden elementos
            while (iterador.QuedanElementos())
            {
                // Obtenemos el siguiente elemento
                Vehiculo v = iterador.Siguiente();

                // Mostramos su contenido
                Console.WriteLine(v.Marca + " " + v.Modelo + " fabricado el " + v.FechaFabricacion.ToShortDateString() + " (" + v.Precio + " euros)");
            }

Esto nos proporcionará la siguiente salida:

Aplicación del patron Iterator

Este patrón, como podemos ver, no tiene demasiada complejidad: es más simple que la maquinaria de un chupete. Sin embargo, la pregunta que seguramente ronde la cabeza del lector sea similar a «pero si tenemos un ArrayList, ¿para qué recorrerlo secuencialmente? ¿Para qué usar esas comprobaciones cuando pueden codificarse en nuestro código de forma sencilla? ¿No es rizar el rizo? La verdad es que sí. Este patrón no está pensado para aplicarlo a una estructura de datos que ya posea mecanismos de acceso, como por ejemplo el ArrayList que hemos utilizado aquí: está pensado para ser utilizado en estructuras en los que el cliente no deba o no necesite conocer su estructura interna. Este ejemplo es estúpido porque hemos observado «las tripas» de la clase RegistroVehiculos y sabemos que dentro de ella vive un ArrayList. Pero si no lo supiéramos, el Iterator se encargaría de proporcionarnos acceso a sus miembros.

Vale, de acuerdo, en la ignorancia está la felicidad. Pero aún así, el programador original de la aplicación sabe que dentro de RegistroVehiculos hay un ArrayList, independientemente de que el cliente necesite saberlo o no. ¿Por qué complicarse la vida de esta manera? Porque ¿qué ocurriría si dentro de nuestra estructura de datos no hubiese un ArrayList. O mejor aún: ¿qué ocurriría si nuestro cliente abriese una fábrica en otra ciudad y nuestra aplicación necesitara conectarse de forma remota a la información allí contenida? ¿Cambiaríamos todas nuestras clases e interfaces para proporcionar un conjunto de operaciones que nos permitiera conectarnos de forma remota al nuevo centro? Con este patrón no sería necesario. Bastaría con implementar otra vez la interfaz IIteratorVehiculo de forma que sus operaciones realizaran las peticiones necesarias al servicio remoto, haciéndolo transparente para el usuario. Por ejemplo, podríamos idear una implementación como la siguiente:

    public class IteratorVehiculoRemoto : IIteratorVehiculo
    {

        private String urlServicio;

        // Almacenaremos el índice en el que se encuentra el iterador
        private int posicionActual = -1;

        // El constructor inyectará la dirección del servicio en el objeto
        public IteratorVehiculoRemoto(String urlServicio)
        {
            this.urlServicio = urlServicio;
        }

        #region IIteratorVehiculo Members

        public void Primero()
        {
            this.posicionActual = -1;
        }

        public Vehiculo Actual()
        {
            // Aquí realizaríamos las comprobaciones necesarias para determinar si la petición es válida
            // ...

            // Realizamos la petición HTTP
            WebRequest request = WebRequest.Create(urlServicio + "?Index=" + posicionActual);
            ((HttpWebRequest)request).UserAgent = "Cliente IteratorVehiculoRemoto";
            request.Method = "GET";

            // Obtenemos la respuesta
            WebResponse response = request.GetResponse();
            Stream data = response.GetResponseStream();
            response.Close();

            // Deserializamos el objeto
            byte[] buffer = new byte[data.Length];
            data.Read(buffer, 0, buffer.Length);
            MemoryStream ms = new MemoryStream(buffer);
            IFormatter formatter = new BinaryFormatter();
            ms.Seek(0, SeekOrigin.Begin);
            Vehiculo v = (Vehiculo)formatter.Deserialize(ms);

            return v;
        }

        public Vehiculo Siguiente()
        {
            // Aquí realizaríamos las comprobaciones necesarias para determinar si la petición es válida
            // ...

            // Realizamos la petición HTTP
            WebRequest request = WebRequest.Create(urlServicio + "?Index=" + (++posicionActual));
            ((HttpWebRequest)request).UserAgent = "Cliente IteratorVehiculoRemoto";
            request.Method = "GET";

            // Obtenemos la respuesta
            WebResponse response = request.GetResponse();
            Stream data = response.GetResponseStream();
            response.Close();

            // Deserializamos el objeto
            byte[] buffer = new byte[data.Length];
            data.Read(buffer, 0, buffer.Length);
            MemoryStream ms = new MemoryStream(buffer);
            IFormatter formatter = new BinaryFormatter();
            ms.Seek(0, SeekOrigin.Begin);
            Vehiculo v = (Vehiculo)formatter.Deserialize(ms);

            return v;
        }

        public bool QuedanElementos()
        {
            // Realizamos la petición HTTP
            WebRequest request = WebRequest.Create(urlServicio + "?GetMaxElements");
            ((HttpWebRequest)request).UserAgent = "Cliente IteratorVehiculoRemoto";
            request.Method = "GET";

            // Obtenemos la respuesta
            WebResponse response = request.GetResponse();
            Stream data = response.GetResponseStream();
            response.Close();

            // Obtenemos el resultado de la petición
            StreamReader reader = new StreamReader(data);
            string strResultado = reader.ReadLine();
            if(!string.IsNullOrEmpty(strResultado))
                return Boolean.Parse(strResultado);

            return false;
        }

        #endregion
    }

Como podemos ver, ambos iteradores serán IIteratorVehiculo, y como tales, tendrán los mismos métodos. Por lo tanto, la clase cliente no necesitará saber si está obteniendo los datos desde un ArrayList de nuestra máquina o desde un servicio web alojado en otra Comunidad Autónoma. La transparencia es la clave de este patrón, y por lo tanto, será el objetivo que debemos buscar. Incluso podríamos crear un tercer Iterator que de algún modo fusionara ambos iteradores, haciendo que cuando se llegara al final del iterador del ArrayList se comenzaran a proporcionar elementos del servicio web, tratando ambas colecciones de cara al cliente como si fuera una sola.
Nota: el código que he mostrado para simbolizar la implementación de un iterador ejecutado sobre un servicio no es real. Se trata de una simulación en la que se pretende mostrar, de forma conceptual, cómo podría implementarse el patrón para acceder a elementos remotos. Ningún servicio web ha sido maltratado durante la redacción de este artículo.

¿Cuándo utilizar este patrón? Ejemplos reales

Ya hemos visto cuándo es aconsejable utilizar este patrón: cuando queramos proporcionar una forma de iterar sobre una colección sin que el cliente tenga que conocer los detalles de cómo está implementada. Por fortuna para nosotros, no tendremos que crear una interfaz IIterator, ya que tanto Java como .NET ofrecen una interfaz estándar de la cual es aconsejable que hereden los iteradores que decidamos implementar. Estas clases son IEnumerator en el caso de .NET e Iterator en el caso de Java.
En el caso de .NET, la clase IEnumerator será la encargada de cumplir esta función. A diferencia del ejemplo anterior, no posee un método QuedanElementos(). Sin embargo, esta funcionalidad está cubierta por el método MoveNext(), que en lugar de devolver una referencia al elemento siguiente, comprueba si existen elementos y devuelve el resultado de la consulta. Además, en caso afirmativo, incrementará el índice en una unidad, permitiendo al método Current() acceder al siguiente elemento.

    private class IteratorVehiculoEnumerable<T> : IEnumerator<T>
    {
        // Referencia al listado completo
        private ArrayList listaElementos;

        // Almacenaremos el índice en el que se encuentra el iterador
        private int posicionActual = -1;

        // El constructor inyectará el ArrayList en el objeto
        public IteratorVehiculoEnumerable(ArrayList listaElementos)
        {
            this.listaElementos = listaElementos;
        }

        public void Reset()
        {
            this.posicionActual = -1;
        }

        // Devuelve el elemento actual
        public T Current
        {
            get 
            {
                // Si no existen elementos, devolveremos null.
                // Si el indice actual es mayor que el mayor indice aceptable, devolveremos null.
                // Si el indice actual es -1, devolveremos null.
                if ((this.listaElementos == null) ||
                    (this.listaElementos.Count == 0) ||
                    (posicionActual > this.listaElementos.Count - 1) ||
                    (this.posicionActual < 0))
                    return default(T);

                // Devolvemos el elemento correspondiente al elemento actual
                else
                    return (T)this.listaElementos[posicionActual];
            }
        }

        object IEnumerator.Current
        {
            get { return Current; }
        }

        // La función ElementoSiguiente() de IEnumerable, a diferencia del ejemplo anterior, no devuelve una
        // referencia al siguiente elemento, sino que detecta si existe otro elemento (lo que en la implementación
        // anterior realizaba QuedanElementos()) y en caso afirmativo, incrementa el índice, apuntando al siguiente
        // elemento, que será recuperado mediante Current.
        public bool MoveNext()
        {
            bool quedanElementos = (posicionActual + 1 <= this.listaElementos.Count - 1);
            if (quedanElementos)
                posicionActual++;

            return quedanElementos;
        }

        public void Dispose()
        {
            // Este método se implementará en caso de utilizar recursos no gestionados.
            // En ese caso, aquí se liberarán esos recursos antes de destruir el objeto.
            return;
        }
    }

Además de disponer de la interfaz IEnumerator, también disponemos de la interfaz IEnumerable, que serviría para implementar el registro de vehículos. Esta interfaz proporciona la función GetEnumerable(), que será la encargada de construir el IEnumerator correspondiente.
Por su parte, tal y como hemos dicho, Java proporciona su interfaz Iterator, que tiene el siguiente aspecto:

	public class IteratorVehiculo implements Iterator<Vehiculo> {

	@Override
	public boolean hasNext() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public Object next() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void remove() {
		// TODO Auto-generated method stub
		
	}

Fuentes:

4 comentarios

  1. Hola Daniel,

    Estoy de acuerdo contigo en que es un patrón fácil de entender porque no hay día que no estemos escribiendo un bluce foreach, … Sin embargo, a medida que iba leyendo no acaba de entender porqué este patrón hace necesaria la existencia de un método «QuedanElementos». Justo entonces he llegado a las explicaciones que das sobre .Net y Java y todo ha encajado. MoveNext además de devolver si quedan elementos incrementa la posición y «mata 2 pájaros de un tiro».

    Muchas gracias por el post.

    1. El patrón es únicamente un esquema. Cada cual debe adaptarlo acorde a sus necesidades. Java y .NET, como podemos observar, los implementan de forma distinta, pero mantienen su filosofía.

      Gracias a ti por leerme. Y por el feedback, por supuesto 🙂

  2. Hola Daniel……muy bueno el post. Sin duda me haré asiduo a este blog.

    No entiendo bien esto :

    object IEnumerator.Current
    {
    get { return Current; }
    }

    Muchas gracias !!!

  3. Excelente Post!! Gracias

    Solo una duda ,no me quedo claro esto:

    object IEnumerator.Current
    {
    get { return Current; }
    }

Deja un comentario