Padrão Iterator
08 Mar 2018Frequentemente 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:
- Abstrair o acesso aos elementos de coleções, ou seja, impedir a exposição da estrutura interna da coleção
- Prover uma interface "universal" para que seja possível criar e usar novos algoritmos de navegação sem quebrar a estrutura atual
Componentes
Os componentes deste padrão são:
- Iterator
- Define uma interface para acessar e navegar pelos elementos
- IteratorConcreto
- Implementa a interface Iterator
- Implementação do algoritmo de acesso os elementos de uma coleção
- Mantém referência para o elemento atual
- Colecao
- Representa uma coleção
- Define uma interface para acessar o Iterator de uma coleção
- ColecaoConcreta
- Implementa a interface Colecao
- Implementa a criação do Iterator específico para a coleção, como o IteratorConcreto por exemplo
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.