Como Implementar Design Patterns em TypeScript: Decorator
O que é o Design Pattern Decorator?
Decorator é um padrão de design estrutural que permite adicionar comportamento a objetos individuais dinamicamente, em tempo de execução, sem afetar outras instâncias da mesma classe. É uma forma de estender a funcionalidade de um objeto de forma flexível e escalável. O padrão é um dos padrões de design Gang of Four, introduzido pelo livro “Design Patterns: Elements of Reusable Object-Oriented Software”.
Objetivos do padrão Decorator:
- Anexe responsabilidades adicionais a um objeto dinamicamente.
- Forneça uma alternativa flexível à subclasse para estender a funcionalidade.
- Permite a adição de novos comportamentos a objetos individuais sem afetar outros objetos da mesma classe.
Os principais componentes do padrão Decorator são:
Componente: É a interface ou classe abstrata que representa a interface do objeto que terá responsabilidades adicionadas a ela. Ele define as operações comuns que os componentes e decoradores de concreto devem implementar.
Componente Concreto: Esta é a classe que implementa a interface Componente. Ele define o comportamento básico ao qual responsabilidades adicionais podem ser adicionadas.
Decorator: Esta é a classe abstrata que também implementa a interface Component, mas mantém uma referência a um objeto Component. Esta referência permite aos decoradores envolver ou “decorar” componentes de concreto.
Concrete Decorator: São as classes que estendem o Decorator e adicionam responsabilidades adicionais aos componentes.
O padrão Decorator oferece várias vantagens que o tornam uma ferramenta valiosa no projeto e desenvolvimento de software. Algumas das principais vantagens do padrão Decorator são:
Extensão Flexível: O padrão Decorator permite adicionar novas funcionalidades ou responsabilidades a objetos dinamicamente em tempo de execução. Essa flexibilidade vem da capacidade de envolver objetos com decoradores (decorators) e combiná-los de várias maneiras. Evita a necessidade de criar inúmeras subclasses para obter diferentes combinações de recursos, tornando a base de código mais fácil de manter e extensível.
Princípio Aberto-Fechado: O padrão Decorator adere ao Princípio Aberto-Fechado, que afirma que as classes devem ser abertas para extensão, mas fechadas para modificação. Usando decoradores (decorators), você pode estender o comportamento de objetos sem modificar seu código existente. Isso promove a reutilização do código e minimiza o risco de introdução de bugs ao fazer alterações.
Princípio de Responsabilidade Única: O padrão permite dividir as responsabilidades entre várias classes pequenas (decoradores/decorators) em vez de ter uma única classe monolítica com todas as funcionalidades. Cada decorador tem uma responsabilidade específica, tornando a base de código mais organizada e fácil de gerenciar.
Combinação de comportamentos: os decoradores (decorators) podem ser empilhados e combinados de várias maneiras, fornecendo controle refinado sobre o comportamento de um objeto. Isso permite que você crie diferentes configurações de objetos em tempo de execução com base em requisitos específicos. Promove a criação de objetos sob medida e compostos sem a necessidade de uma explosão de classe.
Separação de interesses: O padrão Decorator separa os interesses de adição de funcionalidade e a implementação principal do objeto. O componente principal e seus decoradores (decorators) podem se concentrar em suas tarefas específicas sem se acoplar fortemente entre si.
Fácil de manter: em comparação com as abordagens tradicionais baseadas em herança, o padrão Decorator facilita a manutenção e a extensão do código. Adicionar novos recursos ou modificar os existentes pode ser feito criando novos decoradores ou alterando a ordem dos decoradores sem afetar outras partes do código.
Gerenciamento de Complexidade: Em vez de ter uma única classe com uma infinidade de recursos e combinações opcionais, o padrão Decorator permite que você gerencie a complexidade dividindo as responsabilidades em classes menores e gerenciáveis. Isso resulta em um design mais modular e sustentável.
Promove a composição ao invés de herança: o padrão Decorator enfatiza o uso da composição do objeto ao invés de herança, que geralmente é considerada uma abordagem melhor para obter flexibilidade e evitar as limitações de hierarquias de classes profundas.
Como implementar Decorator?
Vamos considerar um exemplo prático de como o padrão Decorator pode ser aplicado em um projeto TypeScript. Implementaremos um sistema simples de compras online onde teremos diferentes tipos de produtos (e.g. eletrônicos, livros, roupas) e ofereceremos embrulhos opcionais e descontos para determinados produtos.
Primeiro, definimos a interface básica do produto que representa a funcionalidade principal de um produto:
interface Product {
name: string;
price: number;
getDescription(): string;
getPrice(): number;
}
Em seguida, criaremos uma implementação concreta da interface Product
para um item eletrônico:
class ElectronicProduct implements Product {
constructor(public name: string, public price: number) {}
getDescription(): string {
return this.name;
}
getPrice(): number {
return this.price;
}
}
Agora, vamos criar um decorador para embrulho de presente:
class GiftWrappingDecorator implements Product {
constructor(private product: Product) {}
getDescription(): string {
return this.product.getDescription() + " (Gift Wrapped)";
}
getPrice(): number {
return this.product.getPrice() + 5; // Assuming gift wrapping costs $5
}
}
A seguir, vamos criar um decorador para aplicação de descontos em determinados produtos:
class DiscountDecorator implements Product {
constructor(private product: Product, private discountPercentage: number) {}
getDescription(): string {
return this.product.getDescription() + ` (Discount ${this.discountPercentage}%)`;
}
getPrice(): number {
const discount = (this.discountPercentage / 100) * this.product.getPrice();
return this.product.getPrice() - discount;
}
}
Agora, vamos ver como podemos usar esses decorators em nosso projeto:
const electronicProduct: Product = new ElectronicProduct("Smartphone", 500);
const giftWrappedProduct: Product = new GiftWrappingDecorator(electronicProduct);
const discountedAndGiftWrappedProduct: Product = new DiscountDecorator(
new GiftWrappingDecorator(electronicProduct),
10 // 10% discount
);
console.log(electronicProduct.getDescription()); // Output: "Smartphone"
console.log(electronicProduct.getPrice()); // Output: 500
console.log(giftWrappedProduct.getDescription()); // Output: "Smartphone (Gift Wrapped)"
console.log(giftWrappedProduct.getPrice()); // Output: 505
console.log(discountedAndGiftWrappedProduct.getDescription()); // Output: "Smartphone (Gift Wrapped) (Discount 10%)"
console.log(discountedAndGiftWrappedProduct.getPrice()); // Output: 454.5 (500 + $5 gift wrapping - 10% discount)
Neste exemplo, implementamos o padrão Decorator para adicionar funcionalidades de embalagem de presente e desconto aos nossos objetos Product
. A principal vantagem aqui é que podemos combinar diferentes decoradores para criar várias configurações de produtos sem modificar sua implementação principal. Isso torna o código mais flexível e fácil de manter, permitindo adicionar ou remover recursos facilmente sem afetar outras partes do sistema.
Assim, o padrão Decorator promove um design mais flexível e modular, permitindo a adição dinâmica de recursos a objetos em tempo de execução.