Mão na massa: Iterator

O padrão de hoje é o Iterator!

Problema

Imagine que você trabalha em uma empresa de Tv a Cabo.Você recebeu a tarefa de mostrar a lista de canais que a empresa oferece. Ao procurar os desenvolvedores dos canais você descobre que existe uma separação entre os desenvolvedores que cuidam dos canais de esportes e os que cuidam dos canais de filmes. O problema começa quando você percebe que, apesar de ambos utilizarem uma lista de canais, os desenvolvedores dos canais de filmes utilizaram Matriz para representar a lista de canais e os desenvolvedores dos canais de esportes utilizaram ArrayList.

Você não quer padronizar as listas pois todo o resto do código dos sistema que cada equipe fez utiliza sua própria implementação (ArrayList ou Matriz). Como construir o programa que vai exibir o nome dos canais?

A solução mais simples é pegar a lista de canais e fazer dois loops, um para percorrer o ArrayList e outro para percorrer a Matriz, e em cada loop exibir o nome dos canais. A impressão dos canais seria algo desse tipo:

ArrayList<Canal> arrayListDeCanais = new ArrayList<Canal>();
Canal[] matrizDeCanais = new Canal[5];

for (Canal canal : arrayListDeCanais) {
	System.out.println(canal.nome);
}

for (int i = 0; i < matrizDeCanais.length; i++) {
	System.out.println(matrizDeCanais[i].nome);
}

No entanto é fácil perceber os problemas desta implementação sem ao menos ver o código, pois, caso outra equipe utilize outra estrutura para armazenar a lista de canais você deverá utilizar outro loop para imprimir. Pior ainda é se você precisar realizar outra operação com as listas de canais terá que implementar o mesmo método para cada uma das listas.

Ok, então vamos ver agora uma boa solução para esse problema.

Iterator

Vamos analisar qual a intenção do padrão Iterator:

“Fornecer um meio de acessar, sequencialmente, os elementos de um objeto agregado sem expor sua representação subjacente” [1]

Então utilizando o padrão Iterator nós poderemos acessar os elementos de um conjunto de dados sem conhecer sua implementação, ou seja, sem a necessidade de saber se será utilizado ArrayList ou Matriz. No nosso exemplo os objetos agregados seriam as listas de canais (ArrayList e Matriz).

Inicialmente vamos criar uma interface comum à todos os objetos agregados, ou seja uma lista genérica:

public interface AgregadoDeCanais {
	IteradorInterface criarIterator();
}

Por ser genérica a nossa classe não possui nenhum detalhe da implementação da lista, ou seja, não possui uma ArrayList ou uma Matriz de Canais. A interface define apenas que todas as classes agregadas devem implementar um método de criação de Iterator (será discutido mais adiante).

Na nossa classe concreta nós utilizamos a lista de dados com uma implementação própria e implementamos o método de criação de Iterator, veja a seguir o exemplo da classe de canais que utiliza o ArrayList:

public class CanaisEsportes implements AgregadoDeCanais {

	protected ArrayList<Canal> canais;

	public CanaisEsportes() {
		canais = new ArrayList<Canal>();
		canais.add(new Canal("Esporte ao vivo"));
		canais.add(new Canal("Basquete 2011"));
		canais.add(new Canal("Campeonato Italiano"));
		canais.add(new Canal("Campeonato Espanhol"));
		canais.add(new Canal("Campeonato Brasileiro"));
	}

	@Override
	public IteradorInterface criarIterator() {
		return new IteradorListaDeCanais(canais);
	}
}

O método de criação do Iterator retorna um iterador de Lista, que tem como conjunto de dados o ArrayList canais. Na classe que utiliza uma matriz para guardar os canais, o método de criação do Iterator retorna um iterador de Matriz.

	@Override
	public IteradorInterface criarIterator() {
		return new IteradorMatrizDeCanais(canais);
	}

Pronto, conseguimos encapsular os diferentes conjuntos de dados numa interface comum, agora qualquer nova lista que apareça precisa apenas implementar a interface que agrega canais. Vamos ver agora como será a implementação dos iteradores.

Da mesma maneira que criamos uma interface comum aos agregados vamos criar uma interface comum aos iteradores, assim podemos garantir que todo iterador tenha o mínimo de operações necessárias para percorrer o conjunto de dados.

public interface IteradorInterface {
	void first();

	void next();

	boolean isDone();

	Canal currentItem();
}

De maneira bem simples esta interface segue a recomendação do GoF [1]. Ou seja todo iterador possui um método que inicia o iterador (first), avança o iterador (next), verifica se já encerrou o percurso (isDone) e o que retorna o objeto atual (currentItem).

A implementação desses métodos será feita no iterador concreto, levando em consideração o tipo do conjunto de dados. Vamos mostrar primeiro a implementação do iterador do ArrayList. Apesar de já existir o Iterador de um ArrayList nativo do Java, vamos criar o nosso próprio Iterator.

public class IteradorListaDeCanais implements IteradorInterface {

	protected ArrayList<Canal> lista;
	protected int contador;

	protected IteradorListaDeCanais(ArrayList<Canal> lista) {
		this.lista = lista;
		contador = 0;
	}

	public void first() {
		contador = 0;
	}

	public void next() {
		contador++;
	}

	public boolean isDone() {
		return contador == lista.size();
	}

	public Canal currentItem() {
		if (isDone()) {
			contador = lista.size() - 1;
		} else if (contador < 0) {
			contador = 0;
		}
		return lista.get(contador);
	}
}

A implementação do iterador é bem simples também. Os métodos alteram o contador do iterador, que marca qual o elemento está sendo visitado e, no método que retorna o objeto nós verificamos se o contador está dentro dos limites válidos e retornamos o objeto corrente.

A implementação do iterador de matriz também é bem simples e segue a mesma estrutura. Veja o código a seguir:

public class IteradorMatrizDeCanais implements IteradorInterface {
	protected Canal[] lista;
	protected int contador;

	public IteradorMatrizDeCanais(Canal[] lista) {
		this.lista = lista;
	}

	@Override
	public void first() {
		contador = 0;
	}

	@Override
	public void next() {
		contador++;
	}

	@Override
	public boolean isDone() {
		return contador == lista.length;
	}

	@Override
	public Canal currentItem() {
		if (isDone()) {
			contador = lista.length - 1;
		} else if (contador < 0) {
			contador = 0;
		}
		return lista[contador];
	}
}

Agora nós conseguimos encapsular também uma maneira de percorrer a lista de dados. Se um novo conjunto de dados for inseridos nós poderemos reutilizar iteradores (caso a estrutura do conjunto de dados seja a mesma), ou criar um novo iterador que implemente a interface básica dos iteradores.

O código cliente seria bem mais simples, veja:

	public static void main(String[] args) {
		AgregadoDeCanais canaisDeEsportes = new CanaisEsportes();
		System.out.println("Canais de Esporte:");
		for (IteradorInterface it = canaisDeEsportes.criarIterator(); !it
				.isDone(); it.next()) {
			System.out.println(it.currentItem().nome);
		}

		AgregadoDeCanais canaisDeFilmes = new CanaisFilmes();
		System.out.println("\nCanais de Filmes:");
		for (IteradorInterface it = canaisDeFilmes.criarIterator(); !it
				.isDone(); it.next()) {
			System.out.println(it.currentItem().nome);
		}
	}

O nosso código utiliza apenas as classes Interfaces, pois assim ficamos independentes de implementações concretas (obedecendo ao princípio de Design Orientado a Objetos Dependency inversion principle [2]).

Um pouco de teoria

A primeira observação a ser feita sobre o Iterator é que ele possui duas formas de implementação. A forma apresentada aqui é chamada de Iterator Externo, pois o cliente (código que utiliza a estrutura do Iterator) é responsável por fazer o percurso. No código mostrado no método main nós utilizamos um for para definir o percurso e as operações no conjunto de dados.

A outra implementação do Iterator é chamada de Interna, pois o código cliente não precisa se preocupar com o ciclo de vida do Iterator, apenas informa qual operação deve ser realizada. Essa implementação utiliza a seguinte ideia: Um classe abstrata implementa o método de percurso e realiza uma operação com cada um dos elementos do conjunto de dados.

public abstract class IteradorInterno {

	IteradorInterface it;

	public void percorrerLista() {
		for (it.first(); !it.isDone(); it.next()) {
			operacao(it.currentItem());
		}
	}

	protected abstract void operacao(Canal canal);
}

Para informar qual operação deve ser executada nós criamos uma subclasse de IteradorInterno e nela implementamos a operação desejada.

public class IteradorPrint extends IteradorInterno {

	public IteradorPrint(IteradorInterface it) {
		this.it = it;
	}

	@Override
	protected void operacao(Canal canal) {
		System.out.println(canal.nome);
	}

}

A execução do iterador interno apenas chama o método percorrerLista() e o iterador fica responsável por executar a operação com todos os objetos do conjunto de dados.

Cada uma das implementações possui efeitos colaterais diferentes, por exemplo, no Iterador Externo o cliente fica responsável por remover o iterador depois que ele for utilizado. No caso da linguagem Java, que possui um garbage colector, este não é um problema tão grande, mas em C++ por exemplo, precisamos tomar o cuidado de excluir o Iterador após seu uso.

Outro problema com o Iterator é que devemos ter uma atenção especial em qual operação o Iterator realizará, pois, caso ele altere, adicione ou remova objetos do conjunto de dados, temos que garantir que essa operação não invalidará os dados do conjunto. Como exemplo imagine que dois iterator são utilizados em paralelo, um deles vai mostrando os dados e o outro procura um elemento específico para removê-lo, o que acontece quando um Iterator acessa um objeto que outro removeu?

Código fonte completo

O código completo pode ser baixado no seguinte repositório Git: https://github.com/MarcosX/Padr-es-de-Projeto.

Os arquivos estão como um projeto do eclipse, então basta colocar no seu Workspace e fazer o import.

Se gostou do post compartilhe com seus amigos e colegas, senão, comente o que pode ser melhorado. Encontrou algum erro no código? Comente também. Possui alguma outra opinião ou alguma informação adicional? Comenta ai! 😀

Referências:

[1] GAMMA, Erich et al. Padrões de Projeto: Soluções reutilizáveis de software orientado a objetos.
[2] WIKIPEDIA. SOLID. Disponível em: http://en.wikipedia.org/wiki/SOLID_(object-oriented_design). Acesso em: 15 set. 2011.