Desvendando o Padrão Decorator: Elegância na Composição de Funcionalidades

O Decorator é um padrão do tipo estrutural, sua principal finalidade é adicionar novos comportamentos a objetos já existentes, de forma dinâmica e que deixe a implementação mais flexível, tudo isso sem alterar a estrutura original.

Esse tipo de design pattern é perfeito para implementações incrementais, sua implementação é simplesmente criar decoradores que envolvem classes e adicionam funcionalidades.

Implementação

Para implementar esse pattern vamos precisar entender alguns conceitos, e papéis para que você consiga encaixar esse padrão na sua lógica de negócio, a seguir vamos exemplificar cada um dos papéis e mostrar uma implementação de fato.

Componente (Component)

Para que seja possível a implementação vamos precisar de um componente ou component, essa interface que será o responsável por definir o comportamento de nossas classes, aqui deverão constar as assinaturas que definirão o contrato que nossos objetos deverão seguir, vamos ao exemplo:

public interface Cafe {

	Double getValor();
	
	String getDescricao();

}
Cafe.java

Componente Concreto (Concret Component)

Essa é de fato a implementação que seguirá o contrato pré estabelecido do nosso componente, essa parte é bem simples basta implementar a interface do componente e cada um de seus métodos obrigatórios.

public class CafeSimples implements Cafe {

	@Override
	public Double getValor() {
		return 10.0;
	}

	@Override
	public String getDescricao() {
		return "Café simples";
	}

}
CafeSimples.java

Decorador (Decorator)

Agora vamos à implementação que de fato dá nome ao pattern, essa se trata de criar uma interface capaz de encapsular o nosso objeto concreto e que implementa nosso componente, aqui também vamos fazer o uso dos métodos do componente concreto encapsulando-os com nossos próprios métodos.

public abstract class CafeDecorator implements Cafe {

	protected Cafe decoretedCafe;

	public CafeDecorator(Cafe decoretedCafe) {
		super();
		this.decoretedCafe = decoretedCafe;
	}
	
	@Override
	public Double getValor() {
		return decoretedCafe.getValor();
	}
	
	@Override
	public String getDescricao() {
		return decoretedCafe.getDescricao();
	}
}
CafeDecorator.java

E aqui cabe uma observação, toda vez que falamos em interface e estamos falando de Java, pode-se entender que é obrigatório o uso do recurso interface presente no Java, e não se trata disso, o objetivo aqui é proporcionar um jeito desacoplado de ter um contrato com alguma a implementação já concreta, por isso estamos utilizando uma classe abstrata, que na linguagem Java é o que nos proporciona de uma maneira fácil esse tipo de recurso, lembrando que por ser uma classe abstrata não é possível construir um objeto a partir dela, essa classe só poderá ser estendida, forçando assim uma implementação do decorator.

Decorador concreto (Concrete decorator)

Essa é de fato a implementação do decorator, onde vamos adicionar novos comportamentos ao que já foi implementado antes. Essa classe tem que obedecer o contrato do Decorator que por sua vez já encapsula as responsabilidades do nosso componente.

public class CafeComLeite extends CafeDecorator {

	public CafeComLeite(Cafe decoretedCafe) {
		super(decoretedCafe);
	}

	@Override
	public Double getValor() {
		return super.getValor() + 0.90;
	}
	
	@Override
	public String getDescricao() {
		return super.getDescricao() + " com leite";
	}
}
CafeComLeite.java

Testando a implementação

Agora vamos testar a nossa implementação, instanciando as nossas classes e observando o comportamento.

public class ExemploDecorator {

	public static void main(String[] args) {
	
		CafeSimples cafe = new CafeSimples();
		System.out.println(String.format("Preço: R$ %.2f, Descrição: %s", cafe.getValor(), cafe.getDescricao()));
		
		CafeComLeite cafeComLeite = new CafeComLeite(cafe);
		System.out.println(String.format("Preço: R$ %.2f, Descrição: %s", cafeComLeite.getValor(), cafeComLeite.getDescricao()));
	}
}
ExemploDecorator.java

Analisando agora a implementação de fato, note que após definir um Decorator, podemos definir inúmeros decoradores concretos, com base no nosso decorator, é isso que torna esse pattern dinâmico e flexível, pois cada implementação nova, conseguimos adicionar novas funcionalidades, a uma implementação já existente, sem ferir contrato ou atrapalhando alguma funcionalidades do nosso sistema.

Observações sobre o padrão

Como todo design pattern existem prós e contras no seu uso, a flexibilidade e a implementação incremental sem dúvidas são muito bons para o desenvolvimento no dia a dia, mas por outro lado existem contras importantes a serem observados no uso desse padrão.

Importante observar que a alteração no componente decorado afeta o seu decorador, já que o mesmo utiliza seus métodos e acrescenta funcionalidades, por isso é importante observar se na regra de negócio que você está trabalhando esse tipo de acoplamento cabe.

O uso desse padrão quando em maior escala pode adicionar uma complexidade a mais, quanto mais e mais decorados você implementar, mais complexo pode ficar o seu código, e mais difícil de se entender.

Outro ponto a se observar é que o nível de hierarquia dos decoradores pode ser um ponto problemático se você possui um empilhamento de decoradores, pode ser problemático manter o comportamento adequado de alguns objetos.

Se você empilhar demais os decoradores, pode acabar com uma quantidade muito excessiva de instâncias de objetos, importante observar se isso não pode comprometer o desempenho das suas aplicações assim como o uso excessivo de recursos como a memória.

Por último mas não menos importante é importante lembrar, que o uso excessivo pode complicar quando houver a necessidade de remover algum componente, pois de certa forma esses estão acoplados uns aos outros, e a remoção de um pode comprometer a funcionalidade de outro.

Conclusão

O uso desse padrão é de fato bem interessante, principalmente quando já temos implementações e precisamos incrementar adicionando novos comportamentos. Em um sistema grande pode acabar sendo a melhor escolha, para implementações incrementais, a depender do modo como a implementação foi organizada. Vale ressaltar alguns problemas que esse padrão pode nos trazer, como um acoplamento não desejado, ou uma complexidade excessiva, mas de fato é mais um conceito muito bom e se utilizado da melhor forma, torna-se muito poderoso nas mãos de qualquer desenvolvedor.

Links uteis

Mauricio Lima
Mauricio Lima

Bacharel em Ciência da Computação, profissional dedicado ao desenvolvimento de software e entusiasta da tecnologia.

Artigos: 65