Salve salve galera! Hoje vou apresentar pra vocês um padrão de projeto muito útil quando o assunto é variação de objetos e dinamismo de criação: O design pattern Decorator. Com este padrão, podemos “envolver” (você verá que realmente parece algo envolvendo outra coisa) um determinado objeto com determinado comportamento distinto em tempo de execução.
Decorators são muito úteis quando realmente precisamos “decorar” alguma coisa sabendo que a quantidade ou variabilidade podem mudar. Vamos dar um exemplo bem típico. Você está desenvolvendo uma janela “na mão”, para visualização de texto. Então você percebe que nem sempre precisará de barras de rolagem horizontais ou verticais em sua janela, e sim, apenas quando necessário. Como colocá-las e removê-las, fazendo com que desapareçam e reapareçam apenas quando o texto ultrapassar os limites da janela. Decorator se encaixa perfeitamente aqui.
Decorator design pattern de acordo com a Gang of Four:
“Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to sub-classing for extended functionality.”
“Anexar responsabilidades adicionais a um objeto dinamicamente. Decorators fornecem uma alternativa flexível de herança para funcionalidades estendidas.”
Exemplo
Mas como encaixamos decoradores no exemplo da janela citado acima? Por instância, vamos imaginar que as janelas deste exemplo serão bem simples e sem nada demais, apenas uma apresentação do conteúdo para algum cliente. Dependendo da largura e altura de todo o texto, precisaremos das barras de rolagem para visualizá-lo completamente.
Começamos a construção de um objeto decorado sempre com seus decoradores (no caso ConcreteDecoratorA [barra vertical] e/ou ConcreteDecoratorB [barra horizontal]) e terminamos com seu ConcreteComponent (a janela de visualização). An? Como assim? Vou explicar de forma conceitual, mas a maneira mais fácil de entender a lógica da solução, é debugando o código do exemplo prático que passarei a seguir e visualizando cada passo da aplicação (senão entender muito agora, não se assuste. Apenas releia a parte teórica novamente após executar o exercício prático).
Antes de tudo, temos que criar uma interface em comum (AComponent). Em nosso caso, é requisito que todos os componentes saibam se desenhar (doStuff()). Criamos então nosso componente concreto (ConcreteComponent) a ser decorado (Janela, o personagem principal). Com este objeto criado, passamos esta referencia ao construtor de algum dos decoradores, não importando qual deles virá primeiro. Cada um saberá exatamente o que terá que fazer com o objeto recebido. Vamos supor que o passamos para a classe BarraVertical. BarraVertical receberá então em seu construtor o componente concreto recém criado (Janela) e inicializará seu pai (Decorator) com o objeto passado. Decorator então armazena uma Janela. Note que temos um objeto BarraVertical armazenando uma Janela em seu pai (que implementa o método de desencadeamento dos decoradores). Vamos agora adicionar a BarraHorizontal. Para isto devemos passar a referência da BarraVertical (que tem uma referência a uma Janela) para o construtor de BarraHorizontal. Dessa forma fazemos o encadeamento de nossos objetos. Temos então uma BarraHorizontal que armazena um objeto do tipo BarraVertical que armazena um objeto Janela.
Decorator, pai de ConcreteDecoratorA e ConcreteDecoratorB, guarda o estado de construção do objeto passado, e implementa o método desencadeador (neste caso, doStuff(), que chama doStuff() dos demais decoradores).
Ao criar um Decorator, a primeira classe instanciada deve ser o componente concreto, seguido de seus demais decoradores. O interessante é que não importa a ordem em que iremos chamar estes decoradores. Cada um sabe devidamente o que deve ser feito e aonde deve ser feito. Um exemplo de encadeamento de construção:
// AComponent == Componente
// Decorator == Decorator
// ConcreteComponent == Janela
// ConcreteDecoratorA == BarraVertical
// ConcreteDecoratorB == BarraHorizontal
// Obs: A classe Decorator, armazena o próximo objeto a ser chamado na seqüência e implementa o método
// polimórfico responsável.
Componente c = new BarraHorizontal(new BarraVertical(new Janela()));
Outra forma de se chamar o código acima seria:
Componente c;
c = new Janela();
c = new BarraVertical(c);
c = new BarraHorizontal(c);
c.doStuff();
O que permite o tipo de chamada encadeada no primeiro trecho, são os construtores das classes que armazenam quaisquer componentes do tipo AComponent. Quando chegamos à chamada c.doStuff(), primeiramente chamamos o método de BarraHorizontal que faz seu trabalho e chama seu pai, que chama doStuff() em BarraVertical (armazenado no pai de BarraHorizontal), que faz seu trabalho e chama seu pai (que armazena uma Janela), que chama Janela, que se desenha e “volta”, revertendo a seqüência anterior (agora BarraVertical e em seguida BarraHorizontal), executando tarefas (se existirem) ainda não executadas.
Um exemplo prático:
Vamos imaginar a seguinte situação. Você é um desenvolvedor e precisa de uma tabela dinâmica que trabalhe de acordo com o tipo de usuário logado. Assim, para cada tipo de tabela você terá um detalhe diferente. Bom, poderíamos resolver este problema facilmente adicionando uma simples flag para cada tipo de usuário e apenas perguntar com um bloco if qual tipo de tabela será impresso na tela do cliente. Aqui seria interessante fazer uma pergunta: E se os requerimentos mudarem? E se estes detalhes mudarem de posição, lugar ou quantidade? Como em nosso caso, os tipos de usuário são vários, e novos tipos podem ser adicionados, seria legal se tivéssemos uma forma de desacoplar essa criação e evitarmos uma seqüência terrível de ifs ou switches. Assim, manteríamos uma tabela global original e facilmente adicionaríamos novas funcionalidades de acordo com o necessitado, de acordo com o tipo de usuário. Para isso podemos utilizar o Decorator pattern. Assim toda vez que novos tipos de usuários forem adicionados, você precisará apenas de uma nova cadeia de construção de objetos para a montagem do objeto final. Se novos recursos para os usuários forem criados, estes são facilmente adicionados, pois, você apenas precisará criar uma nova classe para a nova funcionalidade e não precisará mexer em nenhuma linha de seu código antigo. Isso preserva seu aplicativo de erros lógicos, freqüentes quando estamos trabalhando com cadeias de instruções condicionais, e também torna a manutenção uma brincadeira de criança. Sabemos que programar baseando-se em design patterns torna o desenvolvimento inicial um pouco mais trabalhoso. Mas imagine-se em um grande projeto, onde você acaba de entrar na equipe, e, então no primeiro dia você se depara com cadeias imensas de instruções condicionais. Ou então com módulos extremamente acoplados, onde a lógica se espalha em vários lugares distintos, causando redundância desnecessária e arquivos com códigos imensos (e ainda por cima com a lógica “batida” junto com esse “bolo”). Com certeza não seria uma boa recepção para mais um tripulante a bordo.
Com os padrões de projeto, definimos não só uma organização estruturada e padrão, mas como também uma forma poderosíssima de reutilização de objetos e manutenção de código. Quando você mencionar para outro programador “esta classe faz um Bridge com esta outra classe”, o entendimento do negócio (essencial para um bom projeto) será muito rápido, senão imediato. Assim toda a equipe falará apenas uma língua, e o desenvolvimento ocorrerá de maneira extremamente linda (risos) e eficiente.
Para o nosso exemplo iremos ter inicialmente três tipos de usuários. Os visitantes, os usuários logados e os administradores logados (observação: todos os clientes não logados serão tratados com o perfil de visitantes). Os visitantes deverão visualizar além informações da tabela, um cabeçalho comum com o texto “Olá visitante!”. Os usuários registrados poderão ver além da tabela, um cabeçalho comum com o texto (“Olá ” + varNomeDoUsuarioLogado + “!”) e um rodapé especial com a quantidade de registros encontrados. Por último, os administradores, verão os citados acima, e, em adição, um novo cabeçalho com opções de administração da consulta (como ordenação e etcetera) e abaixo do rodapé, uma pequena área de texto (como um segundo rodapé) com observações (se elas existirem) em relação à consulta realizada. Temos então a regra de nossa tabela:
Tabela
Vamos começar definindo nossa interface. Definimos apenas um método padrão para desenho, pois, este será o método polimórfico que todos os componentes da hierarquia irão realizar.
package decorator;
interface CompTabela
{
void desenha();
}
Agora definiremos nosso componente concreto que será decorado (a tabela).
package decorator;
public class Tabela implements CompTabela
{
public void desenha() {
System.out.println("Tabela.");
}
}
Até aqui tudo bem. Temos uma interface definindo um comportamento padrão e uma implementação básica. A classe Tabela utiliza o método desenha() para desenhar a si mesma. Vamos agora criar a interface de nossos decoradores da tabela.
package decorator;
abstract public class Decorators implements CompTabela
abstract public void desenha();
CompTabela componente;
public Decorators(CompTabela umComponente) {
componente = umComponente;
}
public void desencadeia() {
if(componente != null)
componente.desenha();
}
}
A classe Decorators é o pai dos objetos decoradores. Ela é abstrata, portanto, não pode ser instanciada, além de ter uma variável de instância responsável por armazenar o objeto repassado ao filho. Repare que o método desenha(), foi definido como abstrato, diferente do diagrama UML que vimos acima. Tiramos dele a responsabilidade dada a desencadeia() justamente para defini-lo como abstract e obrigar as novas versões de decoradores concretos herdeiros de Decorators, a implementarem o método desenha() (particularmente, prefiro esta abordagem). Assim, teremos certeza de que todos os novos decorators implementarão o método correto para desenho (desencadeia() poderia ser desenha()). Repare no método desencadeia(). Ele guarda todo o segredo do desencadeamento (acalme-se, estamos chegando lá).
Agora que definimos nossa interface decoradora podemos então criar os objetos desejados para nossa tabela. Esses objetos podem ser qualquer coisa que você desejar e aonde você desejar. Você poderia criar qualquer forma de decoração em sua tabela que ela seria facilmente aplicada ao layout. O código de nosso primeiro decorador concreto:
package decorator;
public class CabecalhoVisitante extends Decorators
{
public CabecalhoVisitante(CompTabela componente) {
super(componente);
}
public void desenha() {
System.out.println("Olá visitante!");
super.desencadeia();
}
}
Muita atenção de agora em diante. Veja primeiramente o construtor da classe e repare que ela repassa diretamente o objeto recebido para o pai. Este é um comportamento obrigatório da linguagem Java e você sempre deve inicializar o pai com o objeto recebido no filho. Esta chamada deve sempre ser a primeira dentro do construtor. Se você tentar colocar algo como System.out.println(“Qualquer saída!”); antes da chamada super(), um erro de compilação aconteceria (call to super must be first statement in constructor). Outra coisa a se observar é que mesmo os construtores vazios, implicitamente chamam este método. A chamada aos pais é obrigatória, pois, ao criar um objeto concreto, qualquer objeto repassado ao seu construtor subirá primeiramente à super classe “paizão”, e só depois descerá a hierarquia novamente executando as chamadas após as chamadas a super().
Repare agora o método desenha(). Primeiro desenhamos o cabeçalho de nossa tabela e logo em seguida invocamos o método super.desencadeia(). Como a classe Decorators guarda o componente decorador que repassado a CabecalhoVisitante, quando chamarmos este método, chamaremos na verdade o método desenha() do outro objeto decorador armazenado em Decorators. Este outro objeto decorador se desenhará (ou não, ainda) e mais uma vez chamará o método super.desencadeia() que invocará novamente desenha(), mas agora, para outro objeto que este decorador estará armazenando em seu pai (e assim por diante até chegarmos ao componente concreto [Tabela]). Dentro do método desencadeia(), primeiramente checamos se a variável é nula, senão, continuamos com a seqüência de desencadeamento.
Veja o código de nossos outros decoradores (poderiamos modificar CabecalhoVisitante e perguntar se o usuário está logado, mas, não desejamos misturar regras de login com objetos de decoração. Deixamos esta responsabilidade para outro objeto):
package decorator;
public class CabecalhoVisitanteLogado extends Decorators
{
public CabecalhoVisitanteLogado(CompTabela componente) {
super(componente);
}
public void desenha() {
System.out.println(
new StringBuilder("Olá ").append(/*varNomeDoUsuarioLogado*/ "varNomeDoUsuarioLogado").append("!"));
super.desencadeia();
}
}
package decorator;
public class CabecalhoAdmLogado extends Decorators
{
public CabecalhoAdmLogado(CompTabela componente) {
super(componente);
}
public void desenha() {
System.out.println("Cabeçaçho de opções da consulta...");
super.desencadeia();
}
}
package decorator;
public class Rodape extends Decorators
{
public Rodape(CompTabela componente) {
super(componente);
}
public void desenha() {
super.desencadeia();
System.out.println("Quantidade de registros encontrados: X");
}
}
package decorator;
public class RodapeDetalhes extends Decorators
{
public RodapeDetalhes(CompTabela componente) {
super(componente);
}
public void desenha() {
super.desencadeia();
System.out.println("Detalhes da consulta...");
}
}
Classe Main:
package decorator;
public class Main
{
public static void main(String[] args) {
System.out.println("\nVisitantes:");
CompTabela c = new CabecalhoVisitante(new Tabela());
c.desenha();
System.out.println("\nVisitantes logados:");
c = new CabecalhoVisitanteLogado(new Rodape(new Tabela()));
c.desenha();
System.out.println("\nAdministradores logados:");
c = new CabecalhoVisitanteLogado(new CabecalhoAdmLogado(new RodapeDetalhes(new Rodape(new Tabela()))));
c.desenha();
}
}
Saída:
Visitantes:
Olá visitante!
Tabela
Visitantes logados:
Olá varNomeDoUsuarioLogado!
Tabela
Quantidade de registros encontrados: X
Administradores logados:
Olá varNomeDoUsuarioLogado!
Cabeçalho de opções da consulta…
Tabela
Quantidade de registros encontrados: XDetalhes da consulta…
Recomendo que você crie o projeto com o código acima e o compile em modo de debug (F7 atrás de F7 caso esteja utilizando o NetBeans). Assim você verá passo a passo por onde seu aplicativo passará durante o processo de criação das decorações.
O padrão Decorator realmente não é fácil e exigirá um pouco mais de estudo e dedicação para que você possa entende-lo perfeitamente. Ele passa por diversos construtores e classes além de utilizar enfaticamente os recursos de orientação a objeto. Mas não se esqueça que com isto ficou muito mais fácil adicionar novos recursos a tabela. Basta criarmos um novo objeto herdeiro de Decorators e extender a funcionalidade exigida pela interface. Depois podemos controlar a nova criação de objetos facilmente, apenas adicionando um novo operador new a um objeto decorador selecionado. Ele também é muito versátil. Observe os trechos abaixo:
System.out.println("\nTrecho 1:");
CompTabela c = new CabecalhoVisitante(new CabecalhoVisitante(new Rodape(new Rodape(new Tabela()))));
c.desenha();/
System.out.println("\nTrecho 2:");
c = new Rodape(new Rodape(new CabecalhoVisitante(new CabecalhoVisitante(new Tabela()))));
c.desenha();
Não importa a ordem de chamada dos objetos. A saída será sempre
Trechos:
Olá visitante!
Olá visitante!
Tabela
Quantidade de registros encontrados: X
Quantidade de registros encontrados: X
a não ser se, por exempo, estivermos trabalhando com dois cabeçalhos diferentes concorrendo a mesma posição de desenho.
System.out.println("\nTrecho 1:");
CompTabela c = new CabecalhoVisitante(new CabecalhoAdmLogado(new Rodape(new Tabela())));
c.desenha();
System.out.println("\nTrecho 2:");
c = new CabecalhoAdmLogado(new CabecalhoVisitante(new Rodape(new Tabela())));
c.desenha();
Saída:
Trecho 1:
Olá visitante!
Cabeçaçho de opções da consulta…
Tabela
Quantidade de registros encontrados: X
Trecho 2:
Cabeçaçho de opções da consulta…
Olá visitante!
Tabela
Quantidade de registros encontrados: X
Percebeu o poder que objetos decoradores podem nos dar? Desacoplamos totalmente lógica da criação e construimos componentes extremamente fáceis de se manusear, modificar e excluir. Imagine-se em um mar de possibilidades, e então de repente, seu cliente resolve fazer algumas modificações (acredite, isto acontece com freqüência). Pode ter certeza que essas pequenas modificações podem lhe dar muito trabalho lógico. Utilizando decorators, você apenas adicionaria algum novo tipo de dado ou modificaria algum tipo já existentes, sem mexer em regras de negócio que não se relacionam com os objetos em questão. Como mandam os princípios dos padrões de projetos, sempre veja primeiro a “grande foto” e separe todas as responsabilidades. Desacople. Programe de forma a estender funcionalidade, não de modificar classes já existentes. E é para isto que utilizamos este conjunto de ótimas soluções chamadas design patterns. Com eles, estamos prevenidos de diversas situações e ganhamos uma meio excelente de reutilização de componentes. Se você ainda estiver um pouco perdido não se assuste (se tiver entendido, excelente). Estude outros padrões e se acostume com a verdadeira e poderosa orientação a objetos. Uma boa semana a todos. Abração!
“Tudo o que temos que decidir é o que será feito com o tempo que nos foi dado.”
J.R.R. Tolkien