[Delphi] Design Patterns GoF – Iterator

Olá, leitores, estou de volta!
O artigo de hoje finalmente retoma a série de artigos sobre Design Patterns. Em continuidade, discutiremos sobre um padrão de projeto que é pouco conhecido na teoria, mas bastante aplicado na prática: o Iterator. Talvez você mesmo já tenha codificado este padrão sem ter ciência. Acompanhe!

Introdução

Em programação, é muito comum trabalhar com listas, coleções, mapas, ou qualquer outra estrutura que seja “iterável”. Quando percorremos os registros de um DataSet, por exemplo, estamos iterando uma tabela, partindo do primeiro registro e movendo para os próximos até chegar ao último. Os métodos First e Next nos permite a navegação no DataSet para que possamos trabalhar com os dados.

A ideia por trás do Iterator segue este mesmo conceito. Podemos “transformar” uma classe em uma estrutura que permita a iteração dos dados processados por ela. Mas este não é o objetivo principal do padrão. A proposta do Iterator é disponibilizar uma forma de percorrer uma coleção (ou lista) sem a necessidade de conhecer a representação dos dados. A origem dos dados é conhecida, mas o modo como eles são lidos e processados em uma lista é encapsulado.

Para o cliente do Iterator, os métodos de acesso aos itens serão sempre os mesmos, independente do formato dos dados. Tecnicamente, imagine, por exemplo, que fosse possível navegar entre os itens de um TObjectList com os métodos First, Prior, Next e Last, tal como fazemos com um DataSet. Este tipo de “padrão de navegação” é o que alcançamos com o Iterator.

Uma das maiores vantagens deste padrão de projeto – além da padronização no mecanismo de navegação – é a imparcialidade dos tipos de dados carregados. Em um mundo tão versátil, como este da programação, é altamente recomendável modelar sistemas que possam trabalhar com diferentes tipos de dados de modo uniforme.

Exemplo de codificação do Iterator

Para este artigo, utilizaremos basicamente o mesmo cenário de negócio do artigo sobre o Chain of Responsibility. Apenas para recordação, neste cenário mencionado, carregamos e enviamos um arquivo em uma “corrente de classes” até que uma delas consiga processá-lo, inserindo o conteúdo em um DataSet.

Já com o Iterator, o comportamento será ligeiramente diferente. As classes responsáveis pela leitura dos arquivos receberão métodos especiais para navegação dos dados, definidos em uma Interface. Ao final da codificação, observaremos que, embora os dados sejam oriundos de arquivos de diferentes formatos, poderemos navegar no conteúdo de maneira homogênea.

Para demonstrar essa ação, a aplicação fará a leitura de dados de clientes que estão armazenados em arquivos CSV e XML, simulando ambientes em que é necessário importar dados para a aplicação, mas cada usuário trabalha com um formato diferente. Eu prezo por essa abordagem por refletir a realidade de alguns projetos.

No contexto do Iterator, quatro elementos devem ser criados. Os dois primeiros são Interfaces: Iterator e Aggregate. A primeira define o contrato dos métodos de navegação, enquanto a segunda define um método para criação do Iterator. Os dois últimos elementos são implementações dessas Interfaces: a classe Concrete Iterator, que define a codificação dos métodos de navegação; e as classes Concrete Aggregate, responsáveis pela criação do Iterator, informando a lista que será manipulada.

Podemos resumir o parágrafo anterior em uma única frase: o cliente utiliza um Aggregate para obter a instância do um Iterator. Este, por sua vez, possui os métodos para navegação em uma lista.

Classe de modelagem

Em primeiro lugar, considere a seguinte classe de modelagem:

A ideia é trabalhar com uma lista preenchida com objetos do modelo acima.

Interface Iterator

Começaremos, então, pelo Iterator, definindo alguns métodos para navegação:

Interface Aggregate

Em seguida, escreveremos também a Interface Aggregate, que possui apenas dois métodos: um para obter uma instância do Iterator e outro para obter a referência da lista de objetos:

Classe Concrete Iterator

O próximo passo é definir a implementação concreta das Interfaces. O Concrete Iterator receberá a codificação a seguir. Observe que, para manipular a lista, faz-se necessária a utilização de uma variável de controle que, neste caso, será FIndice. Além disso, precisamos da referência de um Aggregate para acessar a lista de objetos.

Bem simples, não é?

Classe Concrete Aggregate

A etapa mais “complicada” deste contexto é a implementação das classes Concrete Aggregate, pois envolve a leitura dos arquivos, logo, haverá um Concrete Aggregate para cada formato.

No nosso cenário, como há apenas dois tipos de arquivo (CSV e XML), definiremos, então, duas classes Concrete Aggregate. Cada uma receberá o caminho do arquivo no construtor para que possamos carregá-lo e popular a lista de objetos. Vale destacar também que precisamos criar e destruir a lista de objetos no construtor e destrutor, respectivamente.

O primeiro Concrete Aggregate refere-se ao formato CSV:

O segundo Concrete Aggregate é responsável pelo processamento de arquivos XML:

Em ação!

Agora, vamos conferir: Iterator? OK. Aggregate? OK. Concrete Iterator? OK. Concrete Aggregate? OK. Bom, tudo pronto para colocar o Iterator em ação! 🙂

Como exemplo, o método abaixo navega na lista de objetos para adicionar o nome do cliente em uma TListBox:

A navegação na lista fica bem mais compreensível, não acham? Mas a legibilidade não é a única vantagem. Lembram-se que também devemos ler arquivos XML? Pois bem, a codificação do método é praticamente a mesma, com exceção apenas do caminho do arquivo e da criação do Concrete Aggregate. Ganhamos essa facilidade pelo fato de que o Iterator é o mesmo!

Caso surja um novo formato, como JSON, basta apenas criar um novo Concrete Aggregate e associá-lo ao Iterator. Fácil, fácil.

Conclusão

Leitores, a codificação deste artigo os fizeram lembrar de algo? Talvez, sim. Em abril, publiquei um artigo sobre 3 formas de percorrer uma lista. A última delas consiste no método GetEnumerator, que provê a possibilidade de iterar uma determinada lista com o método MoveNext.

Podemos afirmar, portanto, que GetEnumerator retorna um Iterator. Quando possível, acesse o artigo novamente e veja que o método MoveNext é utilizado tanto para uma lista de strings quanto para uma lista de objetos. Para o cliente deste método (ou seja, nós, desenvolvedores), a representação dos dados é algo que não devemos nos preocupar. 😉

O projeto de exemplo deste artigo está no endereço do GitHub abaixo. Neste projeto, há algumas modificações, como a busca do objeto (pelo Iterator) e exibição dos dados em componentes TEdit. Para fins de teste, disponibilizei um arquivo CSV e um aquivo XML no subdiretório “Dados” do projeto.

 

Até a próxima, pessoal!


 

André Celestino