26 de maio de 2010

Decorator

Segundo o GoF, "os Decorators fornecem uma alternativa flexível a subclasses para extensão da funcionalidade".

A palavra importante aí é "extensão". Ao se criar um objeto decorador, ele não deverá ter conhecimento da classe que ele vai decorar. Um decorador "barra de rolagem" só deverá saber que ele vai extender a funcionalidade, acrescentando o mecanismo da barra de rolagem. Será utilizado num campo texto? Será utilizado num treeview? Bom, ele não precisa (nem pode) saber!

Por quê eu digo que "nem pode" saber? Porque se ele tiver um conhecimento, uma referência à classe que ele vai decorar, ele vai ficar amarrado a esta classe! Caso você precise agora decorar uma tabela com barra de rolagem, a classe decoradora terá que ser alterada também, para incluir uma nova referência ao objeto tabela. E isso é ruim, muito ruim! Todo novo objeto a ser decorado com barra de rolagem demandará manutenção no código da classe decoradora! Além do mais, isso fere um dos princípios mais básicos das boas práticas da Orientação a Objetos: o Acoplamento, isto é, o grau em que uma classe conhece outra classe. Devemos buscar sempre o
Baixo Acoplamento.

Logo, o objeto decorador "barra de rolagem" só precisa saber que ele vai adicionar este mecanismo ao objeto. Precisa de barra de rolagem num campo texto? Basta fazer campo.add(barraDeRolagem); agora eu preciso em uma tabela... Ok, basta fazer tabela.add(barraDeRolagem); o decorador sofreu alguma alteração no código? Absolutamente
nenhuma!

Utilizando o exemplo do objeto Pizza que deverá ser decorado com Condimentos, obteremos o seguinte: tanto Pizza quanto Condimentos serão produtos alimentícios, dos quais eu vou querer saber o preço e descrição. Teremos:

public interface ProdutoAlimenticio {
public double getCusto();
public String getDescricao();
}
O meu Decorator também será do gênero produto alimentício (queijo, orégano, etc), logo, vai implementar a interface acima. Se eu pedir a descrição dos condimentos da pizza, precisarei imprimir "Queijo, Orégano, Calabreza, etc", ou seja, sobrescrevo o getDescricao() para concatenar com vírgulas. Logo, teremos:
public abstract class CondimentoDecorator
implements ProdutoAlimenticio {

public abstract String getDescricaoDoComplemento();
public String getDescricao() {
return ", " + getDescricaoDoComplemento();
}
}
A classe Pizza deveria implementar a interface ProdutoAlimenticio, porém, vou adicionar uma camada intermediária, entre Pizza e ProdutoAlimenticio, com o intuito de flexibilizar a adição de novos pratos além de Pizza, como por exemplo HotDog. Vou chamar esta classe de Refeição, e veremos na prática como isto vai facilitar o reaproveitamento de código, o que é, aliás, uma das grandes vantagens da Orientação a Objetos.

Portanto, a classe Refeição deverá implementar a interface ProdutoAlimenticio, mas ainda será uma classe abstrata, pois ela ainda não sabe informar o custo da refeição - getCusto() - o que vai ser responsabilidade de suas subclasses, até porque este custo vai ser um para Pizza e outro para HotDog, etc.

Note que na classe Refeição não há qualquer referência a decoradores ou qualquer outro objeto que não interessa a ela. Tudo o que a classe Refeição sabe é que ela é um Produto Alimentício, que refeições têm um custo e uma descrição, e que refeições podem adicionar complementos - que, por sua vez, também serão Produtos Alimentícios. Logo, a classe Refeicao.java fica assim:
public abstract class Refeicao
implements ProdutoAlimenticio{

private String descricao;
private double custo;
protected abstract double getPrecoDaRefeicao();

// Construtor da classe com String como parâmetro
protected Refeicao(String descricao){
this.descricao = descricao;
this.custo = getPrecoDaRefeicao();
}
public double getCusto() {
return custo;
}
public String getDescricao() {
return descricao;
}
public void add(ProdutoAlimenticio complemento) {
custo += complemento.getCusto();
descricao += complemento.getDescricao();
}
}
O que o método add acima faz, é simplesmente incrementar o custo da própria refeição com o custo de seu complemento, e da mesma forma, concatenar sua descrição com a descrição do complemento. Note que este método recebe um ProdutoAlimenticio como parâmetro; é tudo o que a refeição precisa saber - que ela pode adicionar complementos alimentícios. E só!

Agora vamos implementar uma classe concreta de Decorator, por exemplo, Queijo. Você concorda comigo que Queijo poderá decorar tanto Pizzas quanto HotDogs? E que poderá decorar pratos que ainda nem foram criados? Que, se amanhã o cliente inventar um novo prato, e precisar decorá-lo com Queijo, este decorador deverá ser capaz de atendê-lo? Ou seja, está bastante evidente que na classe Queijo NÃO poderá haver referências a pratos, nem a pizzas, nem a refeições. Até porque, na classe CondimentoDecorator.java acima, a regra de concatenar sua descrição com vírgula já foi implementada. Logo, para a classe Queijo.java sobrou apenas informar o seu custo e sua descrição - responsabilidades pertinentes apenas à classe Queijo e à mais ninguém! Portanto, Queijo.java fica apenas assim:
public class Queijo extends CondimentoDecorator {
public double getCusto() {
return 5;
}
public String getDescricaoDoComplemento() {
return "Queijo";
}
}
Ou seja, a classe Queijo é um CondimentoDecorator; e Queijo informa seu custo e sua descrição. Você sequer precisa descobrir quais métodos implementar na classe Queijo! Como Queijo extends CondimentoDecorator, a própria compilação te informa que você precisa implementar o método abstrato getDescricaoDoComplemento(); e que você precisa implementar o método abstrato getCusto() , uma vez que CondimentoDecorator não o implementou de ProdutoAlimenticio. Isto é, não tem como errar! É tão fácil adicionar novos condimentos que iremos adicionar mais um: Calabreza.java :
public class Calabreza extends CondimentoDecorator {
public double getCusto() {
return 7;
}
public String getDescricaoDoComplemento() {
return "Calabreza";
}
}
Agora vamos implementar a classe Pizza. Adivinha o que sobrou para ser implementado em Pizza? Exato! Apenas a responsabilidade do que é de Pizza, ou seja, assim como Queijo e Calabreza, a classe Pizza só precisa informar seu custo e sua descrição! Aqui eu vou delegar ao construtor da classe Pizza a atribuição da descrição, certo? Só para permitir ao Cliente fazer: Pizza marguerita = new Pizza("Marguerita") ou Pizza simples = new Pizza("Mussarela"), etc. Logo, a classe Pizza.java fica apenas assim:
public class Pizza extends Refeicao {
//Construtor atribui a descrição
public Pizza(String descricao) {
super(descricao);
}
public double getPrecoDaRefeicao() {
return 10;
}
}
Da mesma forma, Pizza é uma Refeição. Aqui também a compilação te informa o quê é preciso implementar. Caso você se esqueça de algo (ou algum colega novato na equipe...) será possível identificar o problema em tempo de compilação - o que torna o nosso código muito mais resistente a falhas! Importante perceber também que Pizza.java não tem referência a nenhum decorador. Claro! Nem poderia! Iria ter referência a qual decorador? Queijo? Aqui, da forma como está feito, caso o Cliente crie um novo decorador (orégano, por exemplo), tudo o que eu preciso fazer é criar uma nova classe Oregano.java. Só isso! A classe Pizza.java (nem qualquer outra) não vai sofrer nenhuma alteração.

Bom, vamos testar o projeto numa classe Executavel.java, que contenha um método main():
public class Executavel {
public static void main(String[] args) {
Pizza p = new Pizza("Pizza Completa");
p.add(new Queijo());
p.add(new Calabreza());
System.out.println("Descricao: "+p.getDescricao());
System.out.println("Total: R$ "+p.getCusto());
}
}
Pronto! Tudo o que eu preciso fazer para decorar o objeto Pizza com um novo comportamento é passar o Decorator como parâmetro ao método add(). Note que, como este método precisa de um ProdutoAlimenticio, e Pizza é um ProdutoAlimenticio, eu poderia fazer:

Pizza pizza = new Pizza("Pizza Dupla");
pizza.add(new Pizza(""));

Como exercício, fica pra você a tarefa de criar um novo prato HotDog.java e um novo decorador para o complemento Oregano.java, e acrescentar orégano à Pizza já existente, e também reaproveitar Queijo e Calabreza para adicionar ao HotDog. (Abaixo está a solução)
Tudo se resume a isso:

public class HotDog extends Refeicao {
public HotDog(String descricao) {
super(descricao);
}
public double getPrecoDaRefeicao() {
return 9;
}
}
public class Oregano extends CondimentoDecorator {
public double getCusto() {
return 2;
}
public String getDescricaoDoComplemento() {
return "Orégano";
}
}
public class Executavel {
public static void main(String[] args) {
Pizza p = new Pizza("Pizza");
p.add(new Queijo());
p.add(new Calabreza());
p.add(new Oregano());

HotDog dog = new HotDog("Hot Dog");
dog.add(new Queijo());
dog.add(new Calabreza());

System.out.println("Descricao: "+p.getDescricao());
System.out.println("Total: R$ "+p.getCusto());

System.out.println("Descricao: "+dog.getDescricao());
System.out.println("Total: R$ "+dog.getCusto());
}
}
Bem, espero que tenham gostado! Qualquer dúvida deixe uma mensagem que terei o maior prazer em respondê-la! Também escrevam para enviar sugestões. Pretendo colocar exemplos bem explicativos de todos os 23 padrões do GoF, que é um assunto obrigatório para quem quiser programar orientado a objetos, e onde ainda vejo muita deficiência por aí... Abraços.

7 comentários:

  1. Olá Marcone, obrigado por seguir o meu blog! Espero que aproveite os outros posts também! Muito legal a sua idéia em abordar os 23 padrões, será muito útil como um guia de consulta ao desenvolvedor O.O.

    Este primeiro post sobre o padrão Decorator ficou muito bom, direto ao ponto como uma explicação sobre Design Patterns deve ser! :)


    Caso você tenha alguma idéia sobre algum outro post relacionado a Engenharia de Software em geral, fique à vontade para me sugerir também!

    Obrigado.

    Abraço!


    Eduardo Negrão - http://portalengenhariadesoftware.blogspot.com

    ResponderExcluir
  2. Olá.
    Só tem um probleminha. Caso seja necessário mudar o comportamento do cálculo do preço de outras refeições, o reaproveitamento não será flexível. O padrão Decorator clássico leva um atributo que faz referência aos objetos Decorator, delegando a tarefa de calcular o preço para os decoradores e não para a refeição.
    Abraços,
    Marcelo.

    ResponderExcluir
  3. Oi, Marcelo, obrigado pela sua participação.

    Na verdade, quem tem a responsabilidade de conhecer o preço da refeição é o objeto Refeição. Não podemos delegar o preço da refeição para os decoradores, pois se o Queijo mudar sua forma de calcular, vai interferir em todos os pratos que usam Queijo - o que estaria errado.

    Se você tem uma Refeição que calcula o seu preço diferente, basta criar uma nova classe que implemente a interface ProdutoAlimenticio. Os Decoradores não sofreriam alteração, visto que eles informam apenas o seu custo!

    Grande abraço.

    ResponderExcluir
  4. Parabéns...vc explicou no seu Blog melhor que meu professor explica...rsrsrsrs

    Abraço

    ResponderExcluir
  5. Muito Obrigado:
    http://ozirispc.wordpress.com/2011/02/20/entao-vamos-decorando/

    ResponderExcluir
  6. Parabéns pela didática, esclarecimento simples e direto!!! Tenho uma dúvida, e se puder ajudar, agradeço antecipadamente:

    - No caso acima, está bem definido que o desejado é "decorar" um "produto alimentício", obtendo seu custo final, utilizando-se um ou diversos "condimentos", mas numa situação de "montagem" de uma GUI onde o usuário define que objetos êle gostaria de ter à sua disposição para trabalhar, e a quantidade dos mesmos, tal como:

    - uma janela de edição de texto + uma de exibição de vídeo;
    - outro usuário deseja duas janelas de edição de texto;
    - um terceiro usuário, uma janela de edição de texto + uma de exibição de vídeo + outra de exibição de imagem!

    Decorator seria o padrão adequado, ou haveria outro que permita a mesclagem em número e variedade de objetos em um mesmo JFrame?

    Agradecendo qualquer ajuda,

    Abraços

    Augusto Cesar

    ResponderExcluir
    Respostas
    1. Olá, Augusto Cesar, obrigado pelos comentários!

      Vamos lá: no exemplo apresentado, note que a quantidade de decoradores é definida pelo usuário, e não pré-definida no código. Ao usuário é permitido fazer

      pizza.add(new Queijo());

      quantas vezes ele quiser. A quantidade de decoradores é incrementada com a quantidade de instâncias criadas, e não de classes! É claro que a sua dúvida é mais complexa que o exemplo do post, e certamente será bem complicado esclarecer todos os pontos por aqui..

      Pergunto: você já tem o livro da GoF?

      Recomendo fortemente a leitura do livro. Só pra dar uma ideia, na página 47 do livro, ele mostra um exemplo justamente de um editor de textos! O Decorator seria sim, adequado para este propósito - mas não apenas ele. Neste exemplo do livro, ele utiliza nada menos do que oito padrões!

      Em suma: "editor de texto" poderia ser um decorador. Quantos você poderia ter no frame? Quantos você quiser, com:

      frame.add(new Editor()); //etc

      Enfim, se entendi direito, a questão da quantidade não parece ser algo muito engessado. Quaisquer dúvidas, envie mais mensagens, estou sempre à disposição. Mas, se for possível, procure consultar o livro da GoF, as questões são abordadas a fundo.

      Obrigado mais uma vez pela participação,
      Forte abraço,
      Marcone

      Excluir