O livro do GoF (Gang of Four) descreve o padrão Decorator da seguinte forma: "O padrão Decorator anexa responsabilidades adicionais para um objeto dinamicamente". Um exemplo muito citado nas bibliografias é a utilização do padrão Decorator nas APIs de interfaces gráficas com o usuário que são utilizadas em diversos sistemas operacionais e aplicativos de software comerciais. Normalmente é necessário adicionar novos estilos ou comportamentos para uma interface com usuário, essas adições dinâmicas frequentemente são realizadas através da implementação do padrão Decorator. O GoF também afirma que o padrão Decorator é um padrão estrutural (padrões que tratam das associações entre classes e objetos).

A proposta do padrão Decorator é de encapsular um objeto de destino para que se possam adicionar dinamicamente novas responsabilidades em tempo de execução. Cada Decorator pode encapsular um no outro, o que permite um número teoricamente ilimitado de Decorators de objetos destino.

Embora este comportamento em tempo de execução seja muito mais flexível do que a herança através de subclasses, tem-se um nível de complexidade maior, principalmente para determinar os tipos e comportamentos dos objetos.

Os Decorators podem ser utilizados em quase todas as linguagens e plataformas de desenvolvimento de software, desde a parte de interface gráfica até o backend.

A maioria dos frameworks utilizam o padrão de projeto Decorator para adicionar flexibilidade e um comportamento específico em tempo de execução.

Na plataforma Java EE o padrão Decorator pode ser implementado através de configurações realizadas no arquivo XML chamado bean.xml.

No restante do artigo veremos como implementar o padrão como um POJO (Plain Old Java Objects). Também será visto como o padrão Decorator pode ser implementado na plataforma Java EE.

Padrão Decorator

O padrão Decorator é ilustrado através do diagrama de classes da Figura 1.

Diagrama de classe do padrão de projeto Decorator

Figura 1. Diagrama de classe do padrão de projeto Decorator.

Pode-se observar no diagrama de classe acima que o padrão introduz uma interface compartilhada entre a classe de destino e o Decorator. O Decorator também deve ter uma referência para uma instância desta interface.

O restante do artigo mostra de forma prática o funcionamento do padrão Decorator.

Implementando um Decorator em Código Puro (POJO)

Se o desenvolvimento de um sistema ainda está no estágio de projeto, a adição de Decorators não é um problema, no entanto, se um Decorator está sendo implementado em um sistema que já existe será preciso realizar a refatoração de algumas classes.

Segue na Listagem 1 um exemplo de um sistema de pizza em que o padrão Decorator é utilizado para simplificar o sistema. Para este exemplo, cada pizza será decorada com coberturas extras, como queijo duplo, pimentão, entre outros.

Listagem 1. Interface para um pedido.

  public interface Pedido {
    public double getPreco();
    public String getLabel();
  }

Primeiramente é declarada a interface de um pedido conforme o exemplo mostrado.

Na Listagem 2 é mostrada uma classe que representa uma Pizza.

Listagem 2. Exemplo de uma classe Pizza.

  public class Pizza implements Pedido {
  private String label;
  private double preco;
   
  public Pizza(String label, double preco) {
  this.label=label;
  this.preco=preco;
  }
   
  public double getPreco(){
  return this.preco;
  }
   
  public String getLabel(){
  return this.label;
  }
  }

Segue um exemplo de como criar uma Pizza:

Pedido pizzaDaCasa = new Pizza("Pizza da Casa", 10);

Após isso, é necessário criar os decoradores que vão decorar a pizza com coberturas extras.

Será utilizada uma classe abstrata para que as classes concretas não precisem implementar todos os métodos de negócio de uma interface. Um decorador abstrato irá criar um modelo que outros decoradores podem estender.

Talvez sejam necessários diferentes tipos de coberturas extras como cobertura de queijo, pimentão, abacaxi, morango, e assim por diante. Também se pode imaginar a situação em que o cliente deseja encomendar uma refeição um pouco mais picante, e o restaurante não irá cobrar por esta cobertura extra. Então será necessário um decorador que não adiciona mais valor ao preço da pizza, mas que fornece uma "rotulagem" adequada (por exemplo, que queijo extra foi solicitado na pizza). Além disso, também pode ser que o cliente solicite duas porções extras de queijo. Dessa forma, será necessário outro decorador concreto para permitir a rotulagem adequada de coberturas duplas. Segue na Listagem 3 em que todas essas possibilidades são implementadas.

Listagem 3. Exemplo de um Decorator abstrato que adiciona cobertura extra.

  public abstract class Extra implements Pedido {
   
           protected Pedido pedido;
           protected String label;
           protected double preco;
   
           public Extra(String label, double preco, Pedido pedido) {
                     this.label=label;
                     this.preco=preco;
                     this.pedido=pedido;
           }
   
           // O preço é delegado para a implementação concreta
           public abstract double getPreco();
   
           // Label default é fornecido
           public String getLabel() {
                     return pedido.getLabel()+", "+this.label;
           }
   
  }

Portanto, agora que já existe um Decorator abstrato podem ser adicionados Decorators concretos com comportamentos específicos. Segue na Listagem 4 um exemplo de um decorator UmaCoberturaExtra.

Listagem 4. Exemplo de um Decorator que adiciona uma cobertura extra.

  public class UmaCoberturaExtra extends Extra {
   
           public UmaCoberturaExtra(String label, double preco, Pedido pedido) {
                     super(label, preco, pedido);
           }
   
           public Double getPreco() {
                     return this.preco+pedido.getPreco();
           }
  }

Na Listagem 5 tem-se o exemplo de outro Decorator chamado CoberturaSemCusto que modifica o label da Pizza, mas não adiciona nenhum custo à pizza.

Listagem 5. Exemplo de Decorator que adiciona cobertura extra sem custo.

  public class CoberturaSemCusto extends Extra {
   
           public CoberturaSemCusto(String label, double preco, Pedido pedido) {
                     super(label, preco, pedido);
           }
   
           public Double getPreco() {
                     return pedido.getPreco();
           }
  }

O exemplo na Listagem 6 tem-se o Decorator chamado CoberturaExtraDupla que dobra o preço e adiciona uma palavra chave no label.

Listagem 6. Exemplo de Decorator para adição de cobertura dupla.

  public class CoberturaExtraDupla extends Extra {
   
           public CoberturaExtraDupla(String label, double preco, Pedido pedido) {
                     super(label, preco, pedido);
           }
   
           public Double getPreco() {
                     return (this.preco*2)+pedido.getPreco();
           }
   
           public String getLabel() {
                     return pedido.getLabel()+ ", Dupla " + this.label;
           }
  }

Segue na Listagem 7 o código para testar as implementações.

Listagem 7. Exemplo de como testar as implementações.

  Pedido pizzaDaCasa = new Pizza("Pizza da Casa", 10);
  pizzaDaCasa = new UmaCoberturaExtra("Pepperoni", 4, pizzaDaCasa);
  pizzaDaCasa = new CoberturaExtraDupla("Mozzarella", 2, pizzaDaCasa);
  pizzaDaCasa = new CoberturaSemCusto("Pimenta", 2, pizzaDaCasa);
  System.out.println(pizzaDaCasa.getPreco());
  System.out.println(pizzaDaCasa.getLabel());

A saída é dada a seguir:

18.0
  Pizza, Pepperoni, Dupla Mozzarella, Pimenta

Pode-se verificar que foi criada uma Pizza e adicionadas algumas coberturas extras, algumas com certo custo e outra sem custo algum. Quando o método getPreco e getLabel são chamados começa o processo de desempilhar os valores dos objetos salvos.

Implementando Decorator na plataforma Java EE

Diferente de outros padrões o Decorator pode ser implementado através do arquivo bean.xml, exceto quando anotado com @Priority.

A implementação do padrão Decorator na plataforma Java EE pode ser realizada através de duas novas anotações: @Decorator e @Delegate. A anotação @Decorator anota a classe Decorator, e a anotação @Delegate anota o ponto de injeção onde a classe a ser decorada é injetada.

Como exemplo tem-se uma loja que quer oferecer desconto em alguns dos seus produtos. Dessa forma, é utilizado um Decorator para aplicar este desconto ao produto que está com o preço normal de varejo.

No exemplo da Listagem 8 tem-se uma interface que é utilizada para conectar o Decorator com o objeto que será decorado.

Listagem 8. Exemplo de interface para um produto.

  public interface Produto {
   
           //setters
           public void setLabel(String label);
           public void setPreco(double preco);
           //getters
           public String getLabel();
           public double getPreco();
   
           public String geraLabel();
   
  }

A interface introduz o método geraLabel, onde o decorator será responsável por implementar este método para adicionar o comportamento de desconto.

No exemplo Listagem 9 tem-se a criação de uma classe MesaDeJantar. Este produto é o que se quer decorar.

Listagem 9. Exemplo da classe que será decorada.

  public class MesaDeJantar implements Produto {
   
           private String label = "Mesa de Jantar";
           private double preco = 100.00;
   
           public void setLabel(String label) {
                     this.label = label;
           }
   
           public void setPreco(double preco) {
                     this.preco = preco;
           }
   
           public String getLabel() {
                     return label;
           }
   
           public double getPreco() {
                     return preco;
           }
   
           public String geraLabel() {
                     return preco + ", " + label;
           }
   
  }

Agora deve-se criar o Decorator chamado DescontoDecorator que implementa a interface Produto. Esta classe implementa o método geraLabel e adiciona o comportamento de desconto. O decorator basicamente reduz o preço do produto em 50% e adiciona o texto “(Descontado)” ao label do produto. Para permitir que o container identifique essa classe como um decorator, deve-se anota-la com @Decorator.

O ponto de injeção delegado, ou seja, a instância que será decorada é anotada com @Delegate e deve ser um campo injetado, um parâmetro de método initializer, ou um parâmetro de método construtor de um bean.

O tipo de delegate deve ser a interface implementada pela classe que queremos decorar, ou seja, no exemplo anterior deveria ser Produto.

O container CDI injeta qualquer instância disponível da interface Produto na variável membro produto, conforme exemplo da Listagem 10.

Listagem 10. Exemplo do decorator DescontoDecorator.

  @Decorator
  public class DescontoDecorator implements Produto {
           @Any
           @Inject
           @Delegate
           private Produto produto;
   
           public String geraLabel() {
                     produto.setPreco(produto.getPreco() * 0.5);
                     produto.setLabel(produto.getLabel() + " (Descontado)");
                     return produto.geraLabel();
           }
   
           // Mais metodos aqui…
   
  }

Por fim, o decorator deve ser declarado no arquivo bean.xml. Apesar da maior parte das configurações terem sido realizadas através de anotações ainda assim é necessário adicionar algumas configurações no arquivo XML para que o decorador funcione na plataforma Java EE. No entanto, a configuração é bastante simples e apenas necessária quando for preciso definir a ordem de execução dos decoradores (apenas se houver mais do que um decorator).

Dessa forma, basta adicionarmos as seguintes linhas no arquivo bean.xml:

  <decorators>
  <class>br.com.devmedia.decorator.DescontoDecorator</class>
  </decorators>

Após isso o Decorator já pode ser utilizado, segue na Listagem 11 um exemplo.

Listagem 11. Exemplo de como utilizar o Decorator.

  @Any
  @Inject
  Produto produto;
  public void criaListaPrecos(){
           System.out.println("Label: " + produto.geraLabel());
  }

Uma instância de MesaDeJantar é injetada no membro produto e o método geraLabel é chamado. A saída será a seguir:

Label: 12.5, Mesa de Jantar (Descontado)

Quando uma chamada é realizada para o método geraLabel de qualquer instância de Produto, o container intercepta essa chamada. Assim, a chamada é delegada para o método apropriado do decorator chamado DescontoDecorator, onde é feito o desconto do preço do produto e tem-se a chamada para o destino original, chamando o método geraLabel do objeto MesaDeJantar.

Uma cadeia de chamadas pode ser configurada para incluir todos os decorators que são declarados para decorar as classes que implementam a interface Produto. A ordem que os decorators são chamados é determinado pela ordem que eles são declarados no bean.xml.

O exemplo da Listagem 12 mostra outro decorator chamado DescontoBlackFridayDecorator que implementa a interface Produto e adiciona as anotações @Decorator e @Delegate.

Listagem 12. Exemplo de outro decorator.

  @Decorator
  public class DescontoBlackFridayDecorator extends AbstractDescontoDecorator {
   
           @Any
           @Inject
           @Delegate
           private Produto produto;
   
           public String geraLabel() {
                     produto.setPrice(produto.getPreco() * 0.25);
                     produto.setLabel(produto.getLabel());
                     return produto.geraLabel();
           }
   
           // Mais métodos aqui…
   
  }

Após isso, o arquivo bean.xml é utilizado para declarar a ordem que os decorators devem ser chamados.

No exemplo da Listagem 13 o decorator chamado DescontoDecorator deve ser invocado antes do decorator chamado DescontoBlackFridayDecorator.

Listagem 13. Exemplo de declaração dos decorators no arquivo bean.xml.

  <decorators>
           <class>br.com.devmedia.decorator.DescontoDecorator</class>
           <class>br.com.devmedia.decorator.DescontoBlackFridayDecorator </class>
  </decorators>

Quando geraLabel é invocado uma cadeia de chamadas é configurada incluindo os dois decorators. A chamada a geraLabel é interceptada e delegada para o método geraLabel de DescontoDecorator. Este por sua vez chama getPreco, que será interceptado e delegado para o método getPreco de DescontoBlackFridayDecorator que chama o método getPreco de seu objeto Produto injetado (esta é a mesma instância que foi injetada no decorator DescontoDecorator). Esta invocação não é interceptada porque não existem mais decorators declarados para esta interface, e esta chama o método getPreco no objeto MesaDeJantar. Após a finalização desta chamada, retorna-se para baixo da pilha de chamadas para o primeiro método getPreco que retorna o preço do MesaDeJantar. O decorator reduz o preço em 50% e chama o método setPreco. Esta chamada é delegada para cima na cadeia de chamadas até atingir o objeto MesaDeJantar, onde o novo preço é definido. Em seguida, a chamada retorna para baixo da cadeia de chamada.

O método getLabel é chamado e cria uma cadeia de chamadas semelhante ao que aconteceu com o método getPreco. Por fim, o método geraLabel é invocado e interceptado pelo decorator chamado DescontoBlackFridayDecorator. O preço é descontado em 25% e uma cadeia de chamadas semelhante ao decorator DescontoDecorator é iniciado.

A saída no console é exibida a seguir:

Label: 6.25, Mesa de Jantar (Descontado)

Para a cadeia continuar sem ser interrompida o método geraLabel deve delegar para o método geraLabel da instância delegada injetada, caso contrário, a cadeia é dividida e apenas o primeiro decorador é invocado.

Todas as classes que implementam a mesma interface como o implementado pelo ponto de injeção delegado são decorados, mas apenas se esses decorators são declarados no bean.xml. Isto tem duas grandes implicações: Primeiro que os Decorators podem ser habilitados e desabilitados em tempo de implantação apenas editando o arquivo bean.xml o que oferece bastante flexibilidade. Segundo que um Decorator é automaticamente aplicado a classes que implementam a mesma interface, o que é algo eficiente quando é necessário adicionar novas classes, pois elas são decoradas sem codificação adicional. O único problema dessa segunda abordagem é quando não é necessário decorar algumas classes do mesmo tipo.

Para não decorar todas as classes do mesmo tipo é necessário criar um qualificador customizado e anotar o ponto de injeção delegado e as classes que se quer decorar.

Como exemplo, podemos imaginar a criação de um produto Prato que implementa a interface Produto. Apenas este produto deve ser descontado. Para implementarmos esse requisito deve-se utilizar uma anotação customizada, excluindo assim outros produtos de serem decorados.

Segue na Listagem 14 um exemplo.

Listagem 14. Exemplo de anotação customizada.

  @Qualifier
  @Retention(RUNTIME)
  @Target({FIELD, PARAMETER, TYPE})
  public @interface Liquidacao {}

No exemplo da Listagem 15 criou-se a nova implementação da interface Produto e anota-se ela com o qualificador customizado anteriormente.

Listagem 15. Exemplo de classe decoradora anotada com o qualificador customizado.

  @Liquidacao
  public class Prato implements Produto {
   
           private String label = "Prato";
           private double preco = 80.00;
   
           public void setLabel(String label) {
                     this.label = label;
           }
   
           public void setPreco(double preco) {
                     this.preco = preco;
           }
   
           public String getLabel() {
                     return label;
           }
   
           public double getPreco() {
                     return preco;
           }
   
           public String geraLabel() {
                     return preco + ", " + label;
           }
   
  }

Por fim, anota-se o ponto de injeção delegado no decorator que se quer invocar. Neste caso, escolheu-se o decorator chamado DescontoDecorator, conforme a Listagem 16.

Listagem 16. Exemplo de uso da anotação customizada no ponto de injeção delegado.

  @Liquidacao
  @Any
  @Inject
  @Delegate
  private Produto produto;

Apenas classes que são anotadas com @Liquidacao e implementam a interface Produto são injetadas no ponto de injeção delegado do decorator chamado DescontoDecorator. Portanto, apenas a classe Prato será decorada.

Bibliografia

[1] Erich Gamma, Ricard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).

[2] Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Head First Design Patterns. O'Reilly Media, 2004.

[3] Murat Yener, Alex Theedom. Proffesional Java EE Design Patterns. Wrox, 2015.