Padrão de Projeto: Decorator (Decorador)

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Padrão de Projeto: Decorator (Decorador)

Padrão de Projeto: Decorator (Decorador)

Vinícius Thiengo27/08/2016
(1473) (1) (8) (8)
Go-ahead
"Na falta de um foco externo, a mente se volta para dentro de si mesma e cria problemas para resolver, mesmo que os problemas são indefinidos ou sem importância. Se você encontrar um foco, uma meta ambiciosa que parece impossível e força-o a crescer, essas dúvidas desaparecem."
Tim Ferriss
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
Black Week
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas+ 124
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas934
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Acessar Livro
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, blz?

O padrão de projeto Decorator é bem simples e de fácil entendimento. Se você é programador Java e já utilizou classes de entrada e saída de dados como a BufferedInputStream, então você, mesmo que de forma implícita, utilizou o Decorator, mais precisamente um objeto decorador.

O método de refatoração Mover Embelezamento para Decorator tem como pré-requisito esse padrão.

Tópicos presentes no artigo:

Apresentação

Vamos de início acessar a definição do padrão:

O padrão Decorator adiciona funcionalidades a objetos de forma dinâmica (em tempo de execução), permitindo assim a expansão do objeto de maneira mais flexível.

Resumo da definição acima: utilizando esse padrão nós podemos expandir uma instância sem precisar utilizar herança, somente composição, algo que é não somente recomendado na orientação a objetos como também um princípio.

Confuso ainda? Com o decorrer dos códigos, explicações e diagramas ficará tranquilo o entendimento.

Nesse artigo vamos utilizar o domínio do problema de uma cafeteria, mais precisamente um conjunto de classes referentes aos cafés da cafeteria.

Primeiro vamos começar com a classe base dessa parte do projeto, a classe Bebida:

abstract public class Bebida {
protected String descricao;

protected Bebida(){
descricao = "";
}

public String getDescricao(){
return descricao;
}

abstract public double getPreco();
}

 

Agora vamos as classes de duas bebidas do cardápio da cafeteria, primeiro o café Expresso:

public class Expresso extends Bebida {
public Expresso(){
descricao = "Café Expresso";
}

@Override
public double getPreco() {
return 1.50;
}
}

 

Então o café MisturaDaCasa:

public class MisturaDaCasa extends Bebida {
public MisturaDaCasa(){
descricao = "Café Mistura da Casa";
}

@Override
public double getPreco() {
return 2.80;
}
}

 

Até aqui nada demais, certo?

Obviamente que há muitas outras opções de café, porém somente as duas apresentadas anteriormente já serão suficientes para nos ajudar no estudo do padrão Decorator.

A partir dessas classes temos de adicionar ao projeto a possibilidade de os clientes colocarem condimentos nas bebidas.

Condimentos?

Sim, condimentos como: creme, moca e caramelo.

Bom, entendido o desafio de expansão, uma possibilidade é criar novas classes como: ExpressoComMoca, ExpressoComMocaCreme, ExpressoComMocaCremeCaramelo, MisturaDaCasaComMoca, MisturaDaCasaComMocaCreme, MisturaDaCasaComMocaCremeCaramelo, ...

Ou seja, adicionar muitas outras classes, isso também porque cada condimento coloca um valor a mais no preço final da bebida (além ter a própria descrição dele).

Com a solução de adicionar pequenas outras classes nosso diagrama atual do projeto sairia disso:

Para algo similar a isso:

Are you kidding me?

Fique tranquilo, o diagrama acima é um paliativo do que seria o verdadeiro caso a solução proposta fosse adotada. Note que muitas classes com cafés Expresso ou MisturaDaCasa e condimentos foram omitidas.

Com a solução anterior, para qualquer novo café ou condimento adicionado ao cardápio o número de classes no domínio do problema cresceria absurdamente.

Tendo em mente também que a solução apresentada até agora é pouco flexível, prioriza herança ao invés de composição.

Você provavelmente deve estar pensando em trabalhar com atributos, atributos para os condimentos. Será que essa é uma boa escolha? Vamos ao código de Bebida para a versão com atributos:

abstract public class Bebida {
protected String descricao;
protected ArrayList<Condimento> condimentos; /* NEW */

protected Bebida(){
descricao = "";
condimentos = new ArrayList<Condimento>(); /* NEW */
}

public String getDescricao(){
return descricao;
}

public void addCondimento( Condimento condimento ){ /* NEW */
condimentos.add( condimento );
}

public double getPreco(){ /* NEW - AO MENOS A IMPLEMENTAÇÃO */
double preco = 0;
for( Condimento c : condimentos ){
preco += c.getPreco();
}
return preco;
}
}

 

Note que não mais teríamos o método getPreco() como abstrato. Vou omitir aqui como ficaria a classe Condimento, pois essa é somente uma possível solução que estamos apresentando e que tem problemas quando comparada a versão com o padrão Decorator.

Problemas? Essa solução é perfeita!

Na verdade não é. Em nossa cafeteria, caso tenhamos alguns tipos de bebidas similares a café e que não aceitem condimentos, essas bebidas estariam herdando atributos e métodos que nunca seriam utilizados, essa é uma prática ruim.

Na classe base, a classe Bebida, deve haver somente os atributos e métodos que são compartilhados por todas as subclasses. Uma classe Cha, por exemplo, não teria condimentos.

Deve haver?

Ok, sabemos que o mundo ideal é diferente do mundo real e que de vez em quando são necessárias algumas adaptações, mas é aquilo: o máximo que você puder manter nos trilhos, mantenha, a recompensa (muitas vezes) vem no acesso futuro ao código, evolução do projeto.

Utilizar os condimentos como lista em Bebida não é um problema tão sério assim (Será?), mas temos uma solução ainda melhor, utilizando classes decoradoras, então vamos a ela.

Diagrama

Primeiro vamos a diagrama do Decorator:

Acima, nossa classe componente abstrato para o projeto da cafeteria seria a classe Bebida. As classes que representam os componentes concretos seriam as classes Expresso e MisturaDaCasa.

As classes que representam os decoradores abstrato e concretos serão desenvolvidas no decorrer do artigo.

Antes de prosseguir você provavelmente deve estar se perguntando: qual a necessidade do decorator abstrato? Não poderíamos diretamente herdar de componente abstrato?

Sim, poderíamos, mas o decorator abstrato tem a finalidade de ser, além da classe container da variável de instância do tipo de componente abstrato (evitando repetição desse trecho de código nas subclasses), ser também a classe que forçará as subclasses decoradoras a implementarem métodos que são implementados por classes componentes concretos e que têm já definições na classe componente abstrato.

Por que temos em decorator abstrato uma variável de instância do tipo de componente abstrato? Qual a real finalidade dela?

Essa variável, desse tipo, é necessária para que seja possível trabalharmos com composição permitindo a expansão de qualquer classe componente concreto em tempo de execução, ou seja, a decoração dessas classes.

Ok, você fala tanto de expansão por composição, mas ainda há o trabalho com herança nas classes decoradoras. Explique isso?

Na verdade a necessidade da herança nas classes decoradoras é devido a compatibilidade de tipos e não a expansão das funcionalidades das classes componentes concretos. Com essa finalidade não há problemas quando utilizando o padrão Decorator.

Lembrando que você é livre para implementar variações em qualquer padrão para que se encaixe melhor a seu projeto, mas sempre há a solução comum.

Note também que nas classes decoradoras é possível ter ainda mais métodos e atributos. E essas classes com códigos específicos, além dos códigos comuns, não influenciarão nos algoritmos clientes, uma das vantagens na utilização do Decorator.

Importante: aqui vamos estar utilizando sempre classes abstratas, mas uma versão com a classes componente abstrato e decorator abstrato sendo Interfaces é perfeitamente aceita.

Bom, com isso podemos seguir com o código de exemplo.

Código de exemplo

Partindo da versão original do projeto apresentado logo no início do artigo, podemos começar acrescentando as novas classes referentes ao padrão Decorator. Começando com a classe abstrata DecoratorBebida:

abstract public class DecoratorBebida extends Bebida {
protected Bebida bebida;

protected DecoratorBebida( Bebida bebida ){
this.bebida = bebida;
}

abstract public String getDescricao();
}

 

Note que as classes decoradoras e classes componentes concreto devem ser do mesmo tipo, ou seja, tipo Bebida, nesse projeto. Isso é que vai ajudar a permitir que os códigos clientes do projeto, aqui o da cafeteria, não precisem ser alterados.

Ajudar a permitir? Achei que isso fosse definitivo para permitir a não alteração dos códigos clientes.

Na verdade é ainda necessário que as classes decoradoras implementem os métodos herdados da classe Bebida e também implementados pelas classes de componente s concreto, Expresso e MisturaDaCasa.

Veja a importância de sempre estar restringindo o escopo de atuação de algumas partes da classe. O construtor acima precisa de ter no máximo o tipo de acesso protected, pois somente as subclasses de DecoratorBebida é que vão precisar utilizá-lo.

Sobre a variável de instância bebida, a explicação já foi dada na seção Diagrama, mas não custa lembrar que essa variável terá alguma subclasse de Bebida (classe componente concreto ou decoradora) para poder ser ainda mais expandida, incluídas mais funcionalidades.

Por que tornar o método getDescricao() abstrato?

Boa pergunta! Lembra da importância da classe decorator abstrato discutida na seção Diagrama?

Então, isso é necessário, pois os métodos de nossa classe componente abstrato (aqui a classe Bebida) que são implementados por ao menos uma das classes de componente concreto (Expresso e MisturaDaCasa) devem ser implementados também pelas classes decoradoras.

Como getDescricao() é implementado por todas as subclasses de Bebida, mas mesmo assim é um método não abstrato, na classe DecoratorBebida forçamos isso.

Com o código das classes decoradoras, que implementaremos no decorrer do conteúdo, ficará mais claro.

Nesse projeto as classes decoradoras serão as classes referentes aos condimentos, então temos: Moca, Creme e Caramelo.

Começando com a classe Moca:

public class Moca extends DecoratorBebida {
public Moca( Bebida bebida ){
super( bebida );
}

@Override
public String getDescricao() {
return getDescricao()+", Moca";
}

@Override
public double getPreco() {
return bebida.getPreco() + 0.25;
}
}

 

Acredito que com o código acima muita coisa ficou mais clara. Veja que os métodos de uma classe decoradora invocam os mesmos métodos (algoritmo conhecido como: delegação) da instância de bebida.

Não há uma regra quanto a isso, você escolhe se o conteúdo específico da classe decoradora vem antes, depois, sozinho ou se somente delega a chamada para a instância do tipo componente abstrato, Bebida.

Lembrando que a variável de instância do tipo Bebida permite que tenhamos como conteúdo outras classes decoradoras, permitindo que ainda mais condimentos sejam adicionados as bebidas.

Ok, com isso vamos as outras classes decoradoras, dessa vez a classe Creme:

public class Creme extends DecoratorBebida {
public Creme(Bebida bebida ){
super( bebida );
}

@Override
public String getDescricao() {
return getDescricao()+", Creme";
}

@Override
public double getPreco() {
return bebida.getPreco() + 0.50;
}
}

 

E então a classe Caramelo:

public class Caramelo extends DecoratorBebida {
public Caramelo(Bebida bebida ){
super( bebida );
}

@Override
public String getDescricao() {
return getDescricao()+", Caramelo";
}

@Override
public double getPreco() {
return bebida.getPreco() + 0.85;
}
}

 

Para finalizar a apresentação do padrão Decorator vamos a um código cliente. Os comentários estão representando os dados impressos no prompt de comando:

public class Cafeteria {

public static void main( String args[] ){
Bebida expresso = new Expresso();
System.out.println( expresso.getDescricao()+" | R$ "+expresso.getPreco() );
/* Café Expresso | R$ 1.5 */

Bebida creme = new Creme( expresso );
System.out.println( creme.getDescricao()+" | R$ "+creme.getPreco() );
/* Café Expresso, Creme | R$ 2.0 */

Bebida caramelo = new Caramelo( creme );
Bebida moca = new Moca( caramelo );
System.out.println( moca.getDescricao()+" | R$ "+moca.getPreco() );
/* Café Expresso, Creme, Caramelo, Moca | R$ 3.1 */
}
}

 

Estendemos o domínio do problema em tempo de execução, os códigos clientes que já utilizavam as instâncias de classes de componentes concretos não precisam mudar, pois a interface das classes decoradoras é a mesma.

Nossas classes Bebida, Expresso e MisturaDaCasa não sofreram nenhuma alteração, o domínio do problema foi somente expandido e mais opções adicionadas ao cardápio.

Com isso terminamos com o padrão Decorator.

Opa, espere. Você provavelmente deve estar se perguntando:

E se algum dos códigos clientes estivesse utilizando diretamente um tipo de componente concreto como o Expresso ou MisturaDaCasa, por exemplo. Isso devido a eu ter colocado em alguma dessas classes um método específico. E nesse caso, como ficaria?

Bom, nesse caso a melhor opção é verificar se realmente valeria a implementação do padrão Decorator, pois se a alteração desse código cliente não fosse possível você teria de ter o padrão decorator implementado para a superclasse Bebida e para a classe componente concreto que tivesse mais subclasses e métodos específicos.

Dessa forma seu projeto provavelmente começaria a ficar ainda mais complexo, perdendo quase todo o benefício do padrão Decorator.

Por que você diz "quase todo" ao invés de dizer logo "todo"?

Porque ainda vai persistir o benefício da linguagem universal empregada pelo uso de padrões, mas nesse caso do Decorator e em qualquer outro caso de uso de padrão, somente esse ganho não vale a implementação.

Busque sempre utilizar classes decoradoras de forma independente de ordem, para que qualquer classe decoradora possa ser utilizada sem se preocupar se uma outra já foi ou não trabalhada como invólucro do objeto componente concreto.

Esse problema de ordem é comum quando se tem funcionalidades de busca e criptografia no conjunto de classes decoradoras, onde a busca sempre deve vir primeiro.

Pontos negativos

  • Maior número de classes utilizadas no projeto quando comparado com a versão que tinha somente mais atributos e métodos na classe base para a implementação das funcionalidades extras;
  • Pode prejudicar a leitura e manutenção do projeto caso muitas classes a mais forem necessárias;
  • Pode diminuir a eficiência do projeto caso muitas classes decoradoras forem necessárias para um componente abstrato com muitos métodos públicos (lembrando que classes decoradoras têm que ter a mesma interface pública que classes componente concreto).

Pontos positivos

  • Utilização de padrão faz com que o projeto adote uma linguagem universal facilitando ainda mais a compreensão do código por parte de outros developers;
  • O código cliente do projeto não precisa ser alterado, tendo em mente que as classes decoradoras são do mesmo tipo que as classes componente concreto;
  • A expansão das funcionalidades é dinâmica, em tempo de execução, e não estática, em tempo de compilação. Isso da maior flexibilidade a evolução ao código;
  • Permite que as classes de componente concreto continuem simples movendo o embelezamento (novas funcionalidades) para classes decoradoras.

Conclusão

Quando detectado um padrão de classes que permite a aplicação do Decorator, implemente-o. Mesmo que aparentemente pareça algo ruim ter ainda mais classes no projeto, implemente, pois ele tende a aumentar a legibilidade e consequentemente o lucro  e evolução do software.

Isso pois o tempo e dinheiro gastos na leitura de código é maior que o investido na evolução dele.

Caso o projeto apresente uma queda de desempenho considerável, utilize um medidor de desempenho para encontrar os gargalos do projeto. Não tome decisões baseadas no "achismo", utilize um medidor. Caso seja mesmo o código do Decorator o problema, rollback. Volte com o código anterior ou melhore sua implementação.

Fontes

Use a Cabeça! Padrões de Projetos.

Refatoração Para Padrões.

Vlw.

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Padrão de Projeto: AdapterPadrão de Projeto: AdapterAndroid
Padrão de Projeto: Objeto NuloPadrão de Projeto: Objeto NuloAndroid
Use a Cabeça! Padrões de ProjetosUse a Cabeça! Padrões de ProjetosLivros
Padrão de Projeto: State (Estado)Padrão de Projeto: State (Estado)Android

Compartilhar

Comentários Facebook (1)

Comentários Blog

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...