Mão na massa: Composite

Desta vez vamos falar sobre o padrão Composite!

Problema

Imagine que você está fazendo um sistema de gerenciamento de arquivos. Como você já sabe é possível criar arquivos concretos (vídeos, textos, imagens, etc.) e arquivos pastas, que armazenam outros arquivos. O problema é o mesmo, como fazer um design que atenda estes requerimentos?

Uma solução?

Podemos criar uma classe que representa arquivos que são Pastas, estas pastas teriam uma lista de arquivos concretos e uma lista de arquivos de pastas. Então poderíamos adicionar pastas e arquivos em uma pasta e, a partir dela navegar pelas suas pastas e seus arquivos.

O problema com este design é a interface que esta classe deverá ter. Como são duas listas diferentes precisaríamos de métodos específicos para tratar cada uma dela, ou seja, um método para inserir arquivos e outro para inserir pastas, um método para excluir pastas e outro para excluir arquivos, e assim vai.

Sempre que quisermos inserir uma nova funcionalidade no gerenciador precisaremos criar a mesma funcionalidade para arquivos e pastas. Além disso, sempre que quisermos percorrer uma pasta será necessário percorrer as duas listas, mesmo que vazias.

Bom, vamos pensar em outra solução. E se utilizássemos uma classe base Arquivo para todos os arquivos, assim precisaríamos apenas de uma lista e de um conjunto de funções. Vejamos abaixo:

Pronto, resolvido o problema dos métodos duplicados. E agora, será que está tudo bem? Como faríamos a diferenciação entre um Arquivo e uma Pasta? Poderíamos utilizar o “instace of” e verificar qual o tipo do objeto, o problema é que seria necessário fazer isso SEMPRE, pois não poderíamos confiar que, dado um objeto qualquer, ele é um arquivo ou uma pasta! Sempre teríamos que fazer isso:

if(arquivo instanceof Arquivo){
	// Código para tratar arquivos de video
} else if(arquivo instanceof Pasta){
	// Código para tratar arquivos de audio
}

Ok, então vamos ver uma boa solução para o problema: o padrão Composite!

Composite

Vamos ver então qual a intenção do padrão Composite:

“Compor objetos em estruturas de árvore para representar hierarquia partes-todo. Composite permite aos clientes tratarem de maneira uniforme objetos individuais e composições de objetos.” [1]

Bom, desta vez a intenção não está tão bem clara. A estrutura de árvore será explicada mais adiante, no momento o que interessa é a segunda parte da inteção: tratar de maneira uniforme objetos individuais.

Como o nosso problema era uniformizar o acesso aos arquivos e pastas, provavelmente o Composite seja uma boa solução.

A ideia do Composite é criar uma classe base que contém toda a interface necessária para todos os elementos e criar um elemento especial que agrega outros elementos. Vamos trazer para o nosso exemplo inicial para tentar esclarecer:

A classe base Arquivo implementa todos os métodos necessários para arquivos e pastas, no entanto considera como implementação padrão a do arquivo, ou seja, caso o usuário tente inserir um arquivo em outro arquivo uma exceção será disparada. Veja o código da classe abaixo:

public abstract class ArquivoComponent {

	String nomeDoArquivo;

	public void printNomeDoArquivo() {
		System.out.println(this.nomeDoArquivo);
	}

	public String getNomeDoArquivo() {
		return this.nomeDoArquivo;
	}

	public void adicionar(ArquivoComponent novoArquivo) throws Exception {
		throw new Exception("Não pode inserir arquivos em: "
				+ this.nomeDoArquivo + " - Não é uma pasta");
	}

	public void remover(String nomeDoArquivo) throws Exception {
		throw new Exception("Não pode remover arquivos em: "
				+ this.nomeDoArquivo + " -Não é uma pasta");
	}

	public ArquivoComponent getArquivo(String nomeDoArquivo) throws Exception {
		throw new Exception("Não pode pesquisar arquivos em: "
				+ this.nomeDoArquivo + " - Não é uma pasta");
	}
}

Uma vez que tudo foi definido nesta classe, para criar um arquivo de vídeo por exemplo, basta implementar o construtor:

public class ArquivoVideo extends ArquivoComponent {

	public ArquivoVideo(String nomeDoArquivo) {
		this.nomeDoArquivo = nomeDoArquivo;
	}
}

Já na classe que representa a Pasta nós sobrescrevemos o comportamento padrão e repassamos a chamada para todos os arquivos, sejam arquivos ou pastas, como podemos ver a seguir:

public class ArquivoComposite extends ArquivoComponent {

	ArrayList<ArquivoComponent> arquivos = new ArrayList<ArquivoComponent>();

	public ArquivoComposite(String nomeDoArquivo) {
		this.nomeDoArquivo = nomeDoArquivo;
	}

	@Override
	public void printNomeDoArquivo() {
		System.out.println(this.nomeDoArquivo);
		for (ArquivoComponent arquivoTmp : arquivos) {
			arquivoTmp.printNomeDoArquivo();
		}
	}

	@Override
	public void adicionar(ArquivoComponent novoArquivo) {
		this.arquivos.add(novoArquivo);
	}

	@Override
	public void remover(String nomeDoArquivo) throws Exception {
		for (ArquivoComponent arquivoTmp : arquivos) {
			if (arquivoTmp.getNomeDoArquivo() == nomeDoArquivo) {
				this.arquivos.remove(arquivoTmp);
				return;
			}
		}
		throw new Exception("Não existe este arquivo");
	}

	@Override
	public ArquivoComponent getArquivo(String nomeDoArquivo) throws Exception {
		for (ArquivoComponent arquivoTmp : arquivos) {
			if (arquivoTmp.getNomeDoArquivo() == nomeDoArquivo) {
				return arquivoTmp;
			}
		}
		throw new Exception("Não existe este arquivo");
	}

}

Com isto não é necessário conhecer a implementação dos objetos concretos, muito menos fazer cast. Veja como poderíamos utilizar o código do Composite:

	public static void main(String[] args) {
		ArquivoComponent minhaPasta = new ArquivoComposite("Minha Pasta/");
		ArquivoComponent meuVideo = new ArquivoVideo("meu video.avi");
		ArquivoComponent meuOutroVideo = new ArquivoVideo("serieS01E01.mkv");

		try {
			meuVideo.adicionar(meuOutroVideo);
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}

		try {
			minhaPasta.adicionar(meuVideo);
			minhaPasta.adicionar(meuOutroVideo);
			minhaPasta.printNomeDoArquivo();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}

		try {
			System.out.println("\nPesquisando arquivos:");
			minhaPasta.getArquivo(meuVideo.getNomeDoArquivo())
					.printNomeDoArquivo();
			System.out.println("\nRemover arquivos");
			minhaPasta.remover(meuVideo.getNomeDoArquivo());
			minhaPasta.printNomeDoArquivo();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

Agora podemos visualizar a tal estrutura de árvore, suponha que temos pastas dentro de pastas com arquivos, a estrutura seria parecida com a de uma árvore, veja a seguir:

Como uma estrutura de árvore temoas Nós e Folhas. No padrão Composite os arquivos concretos do nosso exemplo são chamados de Folhas, pois não possuem filhos e os arquivos pasta são chamados de Nós, pois possuem filhos e fornecem operações sobre esses filhos.

Um pouco de teoria

Bom, vimos como o padrão pode ser utilizado, vamos agora explorar mais um pouco sua teoria.

A primeira vantagem, e talvez a mais forte, seja o fato de os clientes do código Composite serem bem simplificados, pois podem tratar todos os objetos da mesma maneira. No nosso exemplo utilizei exceções para que ficasse mais evidente quando um método de uma Pasta é chamado em um Arquivo, mas suponha que os métodos não fizessem nada, a utilização seria mais simplificada ainda, pois não precisaríamos de blocos try e catch.

No entanto, o mal tratamento destas exceções podem gerar problemas de segurança e ai surge uma outra forma de implementar o padrão, restringindo a interface comum dos objetos. Para isto basta remover os métodos de gerenciamento de arquivos (adicionar, remover, etc) da classe base, assim apenas os arquivos pastas teriam estes métodos.

Em contrapartida o usuário do código precisa ter certeza se um dado objeto é Pasta para realizar um cast e chamar os métodos da pasta. Veja o método main que utiliza esta implementação:

public static void main(String[] args) {
		ArquivoComponent meuVideo = new ArquivoVideo("meu video.rmvb");
		ArquivoComponent meuOutroVideo = new ArquivoVideo("novo video.rmvb");
		ArquivoComponent minhaPasta = new ArquivoComposite("minha pasta/");

		((ArquivoComposite) minhaPasta).adicionar(meuVideo);
		((ArquivoComposite) minhaPasta).adicionar(meuOutroVideo);
		minhaPasta.printNomeDoArquivo();
	}

Perceba que precisamos utilizar um cast para fazer a chamada aos métodos da pasta. Caso o objeto não fosse uma pasta de fato teríamos problemas. De acordo com suas necessidades você deve optar qual implementação utilizar.

No repositório do Git (link abaixo) você encontrará as duas implementações, em package separados.

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.

Anúncios

7 comentários sobre “Mão na massa: Composite

  1. Amigo, muito legal seu material. Parabéns.
    apenas fiquei com uma dúvida:
    Seguindo a ideia que você deu de Pastas e Arquivos, como ficaria a implementação para eu criar subpastas com arquivos, utilizando o mesmo print?
    p. ex.:
    Minha Pasta\
    Minha Pasta\MeuVideo.rmvb
    Minha Pasta\MeuOutroVideo.rmvb
    Minha Pasta\ Minha Pasta2\
    Minha Pasta\MeuVideo2.rmvb
    Minha Pasta\MeuOutroVideo2.rmvb

    Aguardo retorno,
    Abraço.

  2. Desculpe, as ultimas 2 linhas seriam:
    Minha Pasta\ Minha Pasta2\
    Minha Pasta\ Minha Pasta2\MeuVideo2.rmvb
    Minha Pasta\ Minha Pasta2\MeuOutroVideo2.rmvb

    Abraço.

    • Obrigado pelos elogios. No caso do seu comentário, a diferença seria em como o cliente iria utilizar a estrutura. O padrão Composite permite criar uma estrutura de árvore, então basta utilizar essa estrutura para montar a árvore como você quiser.
      Esse exemplo do post foi só um exemplo mesmo, talvez não seja a melhor solução, mas a ideia está ai. Espero que te ajude.

  3. Gotei, muito me ajudou a entender esse Padrão parabéns.

  4. Muito bom mesmo, como todos os artigos Mão na massa escrito pelo Marcos Brizeno, estou lendo um por um, todos de grande utilidade para o entendimentos de alunos na área de TI.

  5. Uma dúvida, uma vez que um objeto é composto por objetos de outra classe, não seria correto usar o símbolo de composição (losango preenchido) em vez do símbolo de agregação (losango vazio) na associação que indica tal situação?

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s