Um dos princípios mais importantes no desenvolvimento de software é o princípio de design aberto-fechado. Esse princípio de design enfatiza que as classes devem ser abertas para extensão, mas fechadas para modificação. O padrão de projeto decorador incorpora o princípio de projeto aberto-fechado.
Com o padrão de projeto decorador, você pode facilmente estender uma classe dando a ela um novo comportamento sem alterar seu código existente. O padrão decorator faz isso dinamicamente em tempo de execução, usando composição. Esse padrão de design é conhecido como uma alternativa flexível ao uso de herança para estender o comportamento.
Como funciona o padrão de design Decorator?
Embora o padrão decorator seja uma alternativa para herança de classe, ele incorpora alguns aspectos de herança em seu design. Um aspecto chave do padrão decorador é que todas as suas classes estão relacionadas, direta ou indiretamente.
Um típico padrão de projeto decorador tem a seguinte estrutura:
No diagrama de classes acima, você pode ver que o padrão decorator tem quatro classes principais.
Componente: esta é uma classe abstrata (ou interface), que serve como supertipo para o padrão decorator.
Componente de concreto: esses são os objetos que você pode decorar com diferentes comportamentos em tempo de execução. Eles herdam da interface do componente e implementam suas funções abstratas.
Decorador: esta classe é abstrata e tem o mesmo supertipo do objeto que irá decorar. No diagrama de classes, você verá dois relacionamentos entre as classes de componente e decorador. A primeira relação é de herança; cada decorador é um componente. A segunda relação é de composição; cada decorador tem um (ou envolve um) componente.
Decorador de Concreto: esses são os decoradores individuais que dão a um componente um comportamento específico. Você deve observar que cada decorador de concreto possui uma variável de instância que contém uma referência a um componente.
Implementando o Padrão de Design Decorator em Java
Um exemplo de aplicativo de pedido de pizza pode demonstrar adequadamente como usar o padrão decorador para desenvolver aplicativos. Este aplicativo de amostra de pizza permite que os clientes peçam pizzas com vários recheios. A primeira classe do padrão decorator é a interface pizza:
públicointerfacepizza{
públicoabstrato Corda descrição();
públicoabstratodobrocusto();
}
A interface Pizza é a classe do componente. Assim, você pode criar uma ou mais classes concretas a partir dele. A pizzaria faz dois tipos principais de pizzas, com base na massa. Um tipo de pizza tem massa de fermento:
públicoaulaLeveduraCrustPizzaimplementospizza{
@Sobrepor
público Corda descrição(){
retornar"Massa de pizza feita com fermento";
}
@Sobrepor
públicodobrocusto(){
retornar18.00;
}
}
O YeastCrustPizza é o primeiro concreto classe Java da interface Pizza. O outro tipo de pizza disponível é o pão achatado:
públicoaulaFlatbreadCrustPizzaimplementospizza{
@Sobrepor
público Corda descrição(){
retornar"Massa de pizza feita com pão sírio";
}
@Sobrepor
públicodobrocusto(){
retornar15.00;
}
}
A classe FlatbreadCrustPizza é o segundo componente concreto e, como a classe YeastCrustPizza, implementa todas as funções abstratas da interface Pizza.
Os decoradores
A classe decorator é sempre abstrata, então você não pode criar uma nova instância diretamente dela. Mas é preciso estabelecer uma relação entre os diferentes decoradores e os componentes que irão decorar.
públicoabstratoaulaToppingDecoratorimplementospizza{
público Corda descrição(){
retornar"Cobertura Desconhecida";
}
}
A classe ToppingDecorator representa a classe do decorador neste aplicativo de exemplo. Agora a pizzaria pode criar vários recheios (ou decoradores) diferentes, usando a classe ToppingDecorator. Digamos que uma pizza pode ter três tipos diferentes de coberturas, ou seja, queijo, calabresa e cogumelo.
Cobertura De Queijo
públicoaulaQueijoestendeToppingDecorator{
privado Pizza de pizza;públicoQueijo(Pizza de pizza){
esse.pizza = pizza;
}@Sobrepor
público Corda descrição(){
retornar pizza.descrição() + ", Recheio De Queijo";
}
@Sobrepor
públicodobrocusto(){
retornarpizza.custo() + 2.50;
}
}
Cobertura De Pepperoni
públicoaulacalabresaestendeToppingDecorator{
privado Pizza de pizza;públicocalabresa(Pizza de pizza){
esse.pizza = pizza;
}@Sobrepor
público Corda descrição(){
retornar pizza.descrição() + ", Cobertura De Pepperoni";
}
@Sobrepor
públicodobrocusto(){
retornarpizza.custo() + 3.50;
}
}
Cobertura De Cogumelos
públicoaulaCogumeloestendeToppingDecorator{
privado Pizza de pizza;públicoCogumelo(Pizza de pizza){
esse.pizza = pizza;
}
@Sobrepor
público Corda descrição(){
retornar pizza.descrição() + ", Cobertura De Cogumelos";
}
@Sobrepor
públicodobrocusto(){
retornarpizza.custo() + 4.50;
}
}
Agora você tem um aplicativo simples implementado usando o padrão de design decorator. Se um cliente pedir uma pizza com massa de fermento com queijo e calabresa, o código de teste para esse cenário será o seguinte:
públicoaulaPrincipal{
públicoestáticovazioprincipal(String[] argumentos){
Pizza pizza1 = novo YeastCrustPizza();
pizza1 = novo Calabresa (pizza1);
pizza1 = novo Queijo (pizza1);
System.out.println (pizza1.description() + " $" + pizza1.custo());
}
}
A execução desse código produzirá a seguinte saída no console:
Como você pode ver, a saída indica o tipo de pizza junto com seu custo total. A pizza começou como uma pizza com massa de fermento por $ 18,00, mas com o padrão decorador, o aplicativo foi capaz de adicionar novos recursos e seu custo adequado à pizza. Assim, dando à pizza um novo comportamento sem alterar o código existente (a pizza com massa de fermento).
Com o padrão decorador, você também pode aplicar o mesmo comportamento a um objeto quantas vezes quiser. Se um cliente pedir uma pizza com tudo e um pouco de queijo extra, você pode atualizar a classe principal com o seguinte código para refletir isso:
Pizza pizza2 = novo YeastCrustPizza();
pizza2 = novo Calabresa (pizza2);
pizza2 = novo Queijo (pizza2);
pizza2 = novo Queijo (pizza2);
pizza2 = novo Cogumelo (pizza2);System.out.println (pizza2.description() + " $" + pizza2.custo());
O aplicativo atualizado produzirá a seguinte saída no console:
As vantagens de usar o padrão de projeto Decorator
As duas principais vantagens de usar o padrão de projeto decorador são segurança e flexibilidade. O padrão decorador permite que você desenvolva um código mais seguro, não interferindo no código seguro pré-existente. Em vez disso, estende o código existente por meio da composição. Evitando efetivamente a introdução de novos bugs ou efeitos colaterais não intencionais.
Devido à composição, um desenvolvedor também tem muita flexibilidade ao usar o padrão decorator. Você pode implementar um novo decorador a qualquer momento para adicionar um novo comportamento, sem alterar o código existente e interromper o aplicativo.