Padrão Iterator

Frequentemente lidamos com coleções de dados na qual precisamos navegar e tomar uma ação para cada dado. Um padrão que pode ajudar a abstrair esta navegação, independente da estrutura de dado, é o padrão Iterator. Neste post vou te mostrar o que é este padrão.

Coleções e suas estruturas

Podemos definir uma coleção da seguinte maneira:

Um grupo de coisas acumuladas em um lugar

Então listas, árvores binárias ou até grafos podem ser considerados coleções, pois não são nada além de dados acumulados de maneira específica, certo?

O grande problema é como acessá-los, pois exige-se um algoritmo para navegar por todos os elementos de uma lista, assim como exige-se outro algoritmo para fazer o mesmo com uma árvore binária.

Padrão Iterator

O padrão Iterator pode ser aplicado para:

Componentes

Os componentes deste padrão são:

Iterator no .NET

Felizmente, o .NET tem um conjunto de interfaces muito interessantes que representam o padrão Iterator, assim como aplo uso destas interfaces.

O iterator

O .NET disponibiliza uma interface chamada IEnumerator assim como sua variação genérica, o IEnumerator<T>.

A interface disponibiliza o método MoveNext() (com retorno boleano) para posicionar-se no próximo elemento da coleção, o método Reset() para voltar ao início da coleção e a propriedade Current para acessar o elemento atual.

A coleção

Para o lado da coleção, existe a interface IEnumerable e sua variação genéria IEnumerable<T>.

Esta interface contém apenas o método GetEnumerator() com o tipo de retorno IEnumerator quando acessado de um IEnumerable ou IEnumerator<T> quando acessado de um IEnumerable<T>.

Classes concretas

Uma implementação interessante do padrão que está bem debaixo do nosso nariz é a classe string. A classe string implementa a interface IEnumerable<Char> e consequentemente tem um método GetEnumerator() que retorna uma instância da classe CharEnumerator.

Então podemos percorrer todos os caracteres de uma string da seguinte maneira:

string texto = "abcde";

using (var enumerator = texto.GetEnumerator())
{
while (enumerator.MoveNext())
{
char caractere = enumerator.Current;
//...
}
}

Obs: O using é necessário pois a interface IEnumerator herda de IDisposable.

IEnumerable e o foreach

No C#, o foreach é um baita de um recurso pois simplifica o comando for, mas ele não é só um meio de simplificar o comando.

O uso do foreach é limitado à coleções que implementam IEnumerable ou IEnumerable<T>, logo podemos usar um foreach em uma string.

O código abaixo é equivalente ao exemplo anterior.

string texto = "abcde";

foreach (char caractere in texto)
{
//...
}

Espero que agora você tenha o momento A-HÁ, pois imagine usar um "mero" foreach em uma árvore binária, em um grafo ou até em uma coleção com uma estrutura complexa em seu projeto?

Implementando o padrão

Para este post, vou usar uma situação onde temos uma estrutura de tabela e campos de um banco de dados, porém com relacionamentos de tabelas filhas. E o comportamento padrão de iteração sobre esta coleção, é acessar a tabela pai e em seguida cada uma das tabelas filhas.

A estrutura do dado:

class Tabela
{
public string Nome { get; set; }

public List<string> Campos { get; set; }

public List<Tabela> Filhas { get; set; }
}

Para a coleção e o iterator, irei usar uma facilidade do C# que é o yield, comando que permite a criação de iterators de maneira simplificada.

Usando recursividade, temos a seguinte implementação:

class Tabelas : IEnumerable<Tabela>
{
private List<Tabela> itens;

public Tabelas()
{
itens = new List<Tabela>();
}

public void Adiciona(Tabela tabela)
{
itens.Add(tabela);
}

public IEnumerator<Tabela> GetEnumerator()
{
foreach (var tabela in GetTabelas(itens))
{
yield return tabela;
}
}

private IEnumerable<Tabela> GetTabelas(IEnumerable<Tabela> tabelas)
{
foreach(var tabela in tabelas)
{
yield return tabela;

foreach(var filha in GetTabelas(tabela.Filhas))
{
yield return filha;
}
}
}
}

Com este exemplo, podemos criar uma instâncida de Tabelas, adicionar várias instâncias de Tabela e usar um belo foreach percorrendo todas as tabelas e suas filhas.

Conclusão

Como geralmente trabalhamos com coleções de dados, e algumas vezes estas coleções dependem de um algoritmo específico para percorrer os elementos, conhecer o padrão Iterator nos dá uma poderosa ferramenta na hora de escrever códigos enxutos.

E ai, em qual estrutura você pode aplicar o padrão Iterator hoje?

Até mais.