Como Implementar Design Patterns em TypeScript: Decorator

Amir Elemam
4 min readJul 23, 2023

--

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.

--

--