C# -  Padrão Comportamental Gof - Iterator


Neste artigo vou apresentar o padrão comportamental Gof Iterator.

Segundo a definição da Gang Of Four(GoF), o padrão Iterator fornece uma maneira de acessar os elementos de um objeto agregado (coleção) sequencialmente sem expor sua representação subjacente, ou seja, este padrão permite o acesso sequencial a elementos de uma coleção sem expor a lógica interna.

A ideia principal deste padrão é extrair o comportamento transversal de uma coleção para um objeto separado chamado de iterador.

Assim este padrão define um objeto separado (iterador) que encapsula o acesso e a travessia de um objeto agregado.  Isso significa que, usando o padrão Iterator , podemos acessar os elementos de uma coleção de forma sequencial, sem a necessidade de conhecer sua representação interna.

Lembrando que n a programação orientada a objetos, as coleções se comportam como um container que contém muitos objetos.



A maioria das coleções armazena seus elementos em listas simples. No entanto, algumas delas são baseados em pilhas, árvores, gráficos e outras estruturas de dados complexas.

O padrão Iterator encapsula um iterador que é usado para percorrer o contêiner de objetos e acessar os elementos deste contêiner.

Iterator : Exemplo

Vejamos um exemplo para entender melhor o padrão Iterator.

Vamos imaginar uma coleção qualquer que armazena objetos como em um contêiner :



Esta coleção pode ser um Array, um ArrayList, uma List, Dictionary, etc.

A estrutura de dados da List será diferente da estrutura de dados da ArrayList que será diferente do Dictionary e o mecanismo de armazenamento de List será diferente do mecanismo de armazenamento de Array e também será diferente de ArrayList.

Portanto, o principal uso do padrão Iterator é acessar os elementos (object1, object2, object3 e object4) de uma coleção (ou seja, List, ArrayList e Array) de maneira sequencial. Para isso ele usa iteradores. (usamos iteradores com bastante frequência)

Qualquer controle remoto que usamos funciona da mesma forma , basta pegar o controle e começar a pressionar as teclas para cima ou para baixo para frente ou para trás.  Esse é o trabalho que um iterador faz.

Na linguagem C# podemos iterar facilmente uma coleção usando um laço for ou foreach (na maioria das coleções o foreach é mais indicado)

foreach(var elemento in colecao)

{

   //...elemento

}

Neste código temos que o laço for-each acessa sequencialmente os elementos da coleção sem expor a lógica interna, e isso é feito graças a implementação do padrão iterator disponível na plataforma .net

Mas aqui, dependendo do tipo de coleção, se você quiser usar uma estrutura de dados diferente, será necessário alterar o código do cliente. Não seria bom fornecer uma interface uniforme para percorrer diferentes tipos de coleções especificas ?

Podemos então fazer uma implementação do padrão Iterator e assim criar um iterador customizado.  Um exemplo de código seria o seguinte:

foreach (var elemento = iterator.First(); !iterator.IsDone(); elemento=iterator.Next())

{

    //...iterator.CurrentItem();

}

Assim podemos usar o padrão iterator para implementações quando tivermos uma situação que a implementação da plataforma .NET não nos atender.

A plataforma .NET oferece uma implementação do padrão Iterator para trabalhar com coleções e listas.

Padrão Iterator

Plataforma .NET

Aggregate

IEnumerable<T>

CreateIterator()

GetEnumerator()

Iterator()

IEnumerator<T>

First()

Primeira chamada de MoveNext()

Next()

MoveNext()

IsDone()

MoveNext() retorna um valor

CurrentItem()

Current

Na tabela acima temos a correspondência entre os recursos definidos no padrão Iterator e os usados na implementação feita na plataforma .NET; com essa implementação podemos facilmente acessar a maioria das coleções e coleções genéricas definidas na plataforma .NET.

Diagrama UML

O diagrama UML do padrão Iterator segundo o Gof apresenta os seguintes participantes

1- Iterator - Define uma interface para acessar e percorrer os elementos de uma coleção (pode ser uma interface/classe abstrata);

2- ConcreteIterator - Implementa o Iterator e mantém o controle da posição atual ao atravessar/percorrer o agregado ou coleção;

3- Aggregate - Define uma interface para criar um objeto Iterator. (classe abstrata);

4- ConcreteAggregate -  Implementa a interface de criação de Iterator para retornar uma instância do ConcreteIterator adequado;

Esta é a classe que implementa a expressão. Isso pode ter outras instâncias do Expression.

5- Client - É uma classe que constrói a árvore de sintaxe abstrata para um conjunto de instruções na gramática fornecida.  Esta árvore é construída com a ajuda de instâncias das classes NonTerminalExpression e TerminalExpression.

Essa é a classe que cria a árvore de sintaxe abstrata para um conjunto de instruções na gramática especificada. Essa árvore é criada com a ajuda de instâncias das classes NonTerminalExpression e TerminalExpression.

Quando podemos usar o padrão

Podemos usar o padrão Iterator nos seguintes cenários :

1- Quando sua coleção tiver uma estrutura de dados complexa e você deseja ocultar sua complexidade dos clientes;
O padrão encapsula os detalhes do trabalho com uma estrutura de dados complexa, fornecendo ao cliente vários métodos simples de acesso aos elementos da coleção.

2- Para reduzir a duplicação do código usada para percorrer coleções no seu aplicativo;
O código de algoritmos de iteração não triviais tende a ser muito volumoso e complexo.  Mover o código de passagem usado nos iteradores pode ajudar a tornar o seu código do mais enxuto e limpo.

3- Quando quiser que seu código seja capaz de percorrer diferentes estruturas de dados ou quando os tipos dessas estruturas forem desconhecidos de antemão.
O padrão fornece algumas interfaces genéricas para coleções e iteradores e isso permite usar vários tipos de coleções e iteradores que implementam estas interfaces.


Vantagens do padrão

Como vantagens deste padrão temos que :

- Fornece suporte a iteração para estruturas de dados que originalmente não a possui;
- Permite definir mais de um tipo de iterador;
- Simplifica o código para acessar diferentes tipos de coleções;
- Trata variáveis de todos os tipos, tamanhos e formas uniformemente, cabendo na memória ou não
- Segue os princípios SRP e Open/Closed;

Desvantagem

E como desvantagem podemos citar que :

O padrão é menos eficiente ao percorrer elementos de algumas coleções especializadas diretamente, usando mais memória do que acesso direto ao elemento, e este padrão não é recomendado para coleções simples.

Aplicação prática do padrão

Como exemplo de aplicação do padrão vamos criar uma coleção de objetos Cliente que é representado pela classe cliente
e contém as propriedades Id e Nome um construtor padrão :



Nosso objetivo será criar uma coleção de objetos Cliente e usar o padrão Iterator para iterar sobre os elementos desta coleção:




Para isso vamos implementar o padrão Iterator  criando uma interface Iterator  onde vamos definir os métodos First para retornar o primeiro elemento o método Next para percorrer a coleção e a propriedade IsDone para indicar se a iteração sobre a coleção foi concluída ou não:

Vamos criar também uma classe concreta para tratar a coleção com os seguintes métodos :



- Count - usado para contar elementos da coleção;
- Add - usado para incluir elementos na coleção;
- Get - usado para obter um elemento da coleção;

Implementação prática

Levando em conta este cenário vamos implementar o padrão Iterator usando uma aplicação Console .NET Core (.NET 5.0) criada no VS 2019 Community.

A seguir temos o diagrama de classes obtido a partir do VS 2019 na implementação do padrão:

Podemos identificar as seguinte classes :

- IAbstractIterator - Define a interface que vai permitir retornar o objeto Iterator;
- IAbstractCollection -
Representa o Aggregate;
- ConcreteCollection -   I
mplementa a interface IAbstractCollection para retornar uma instância da classe Iterator;
- Iterator - Implementa a interface AbstractIterator e também mantém o controle da posição atual na travessia dos elementos;

A seguir temos o código usado na implementação:

1- A interface IAbstractIterator

   //Iterator
    public interface IAbstractIterator
    {
        Cliente First();
        Cliente Next();
        bool IsDone { get; }
    }

2- A Interface IAbstractCollection

//Aggregate
public interface IAbstractCollection
{
    Iterator CreateIterator();
}

3- Classe ConcreteCollection

 using System.Collections.Generic;

namespace Iterator2
{
    //ConcreteAggregate
    public class ConcreteCollection : IAbstractCollection
    {
        private List<Cliente> listaClientes = new List<Cliente>();

        //Cria o Iterator
        public Iterator CreateIterator()
        {
            return new Iterator(this);
        }

        // Conta os itens
        public int Count
        {
            get { return listaClientes.Count; }
        }

        //Adiciona itens na coleção
        public void AddCliente(Cliente cliente)
        {
            listaClientes.Add(cliente);
        }

        //Retorna um item da coleção
        public Cliente GetCliente(int posicao)
        {
            return listaClientes[posicao];
        }
    }
}

4- Classe Iterator

    //ConcreteIterator
    public class Iterator : IAbstractIterator
    {
        private int current = 0;
        private int step = 1;

        private ConcreteCollection collection;
        public Iterator(ConcreteCollection collection)
        {
            this.collection = collection;
        }
        // Retorna o primeiro item
        public Cliente First()
        {
            current = 0;
            return collection.GetCliente(current);
        }
        // Retorna o proximo item
        public Cliente Next()
        {
            current += step;
            if (!IsDone)
            {
                return collection.GetCliente(current);
            }
            else
            {
                return null;
            }
        }
        // Verifica se a iteração terminou
        public bool IsDone
        {
            get { return current >= collection.Count; }
        }
    }

7- Program

using System;
namespace Iterator2
{
    class Program
    {
        static void Main(string[] args)
        {
            // Cria uma coleção
            ConcreteCollection colecao = new ConcreteCollection();
            colecao.AddCliente(new Cliente("Pedro", 10));
            colecao.AddCliente(new Cliente("Maria", 11));
            colecao.AddCliente(new Cliente("Manoel", 12));
            colecao.AddCliente(new Cliente("Miriam", 13));
            colecao.AddCliente(new Cliente("Jefferson", 14));
            // Cria o iterator que vai encapsular a iteração
            Iterator iterator = colecao.CreateIterator();
            Console.WriteLine("### Usando o padrão Iterador ###\n");
            Console.WriteLine("Pressione algo para iniciar");
            Console.ReadKey();
            //  iterator      
            Console.WriteLine("Iterando sobre a coleção de Clientes :\n ");
            for (Cliente cliente = iterator.First(); !iterator.IsDone; cliente = iterator.Next())
            {
                Console.WriteLine($"ID : {cliente.Id} & Nome : {cliente.Nome}");
            }
            Console.Read();
        }
    }
}

A execução do projeto irá apresentar o seguinte resultado:

Temos assim um exemplo básico de implementação do padrão Iterator.

Conhecer o padrão Iterator pode fazer a diferença em cenários onde temos  coleções que dependem de um algoritmo específico para percorrer os seus elementos.

Você pode usar este padrão quando tiver diversas coleções com tipos de iterações diferentes. Para poder reaproveitar essa lógica e reduzir a quantidade de código duplicado, podemos encapsulara coleção em um Iterator e usar onde for necessário e se a lógica de iteração mudar, basta alterar na implementação específica do Iterator.

Pegue o código do projeto aqui :
  Iterator1.zip

"Em Deus está a minha salvação e a minha glória; a rocha da minha fortaleza, e o meu refúgio estão em Deus."
Salmos 62:7

Referências:


José Carlos Macoratti