Strategy Pattern: Flexibilidade no Comportamento
O Strategy Pattern encapsula diferentes algoritmos em classes separadas, permitindo que o cliente escolha qual usar em tempo de execução. É especialmente útil quando você tem múltiplas formas de resolver o mesmo problema e quer evitar condicionais espalhados pelo código.
Imagine um sistema de processamento de pagamentos. Em vez de usar um if/else gigante, criamos estratégias independentes:
// Estratégias de pagamento
class PaymentStrategy {
pay(amount) {}
}
class CreditCardStrategy extends PaymentStrategy {
constructor(cardNumber, cvv) {
super();
this.cardNumber = cardNumber;
this.cvv = cvv;
}
pay(amount) {
console.log(`Pagando $${amount} com cartão ${this.cardNumber}`);
return true;
}
}
class PayPalStrategy extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
console.log(`Pagando $${amount} via PayPal (${this.email})`);
return true;
}
}
class CryptoCurrencyStrategy extends PaymentStrategy {
constructor(walletAddress) {
super();
this.walletAddress = walletAddress;
}
pay(amount) {
console.log(`Pagando ${amount} BTC para ${this.walletAddress}`);
return true;
}
}
// Contexto
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
return this.strategy.pay(amount);
}
}
// Uso
const processor = new PaymentProcessor(
new CreditCardStrategy('1234-5678-9012-3456', '123')
);
processor.processPayment(100); // Pagando $100 com cartão...
processor.setStrategy(new PayPalStrategy('user@example.com'));
processor.processPayment(50); // Pagando $50 via PayPal...
O benefício real é a manutenibilidade: adicionar novo método de pagamento não requer modificar PaymentProcessor, apenas criar uma nova estratégia. Isso respeita o princípio Open/Closed do SOLID.
Decorator Pattern: Adicionando Funcionalidades Dinamicamente
O Decorator permite adicionar responsabilidades a um objeto dinamicamente, sem usar herança. É como envolver presentes: cada camada de papel adicionada é um decorator que mantém a funcionalidade anterior e acrescenta a sua.
Considere um sistema de cafeteria onde você constrói bebidas com adições:
// Componente base
class Coffee {
cost() {
return 5;
}
description() {
return 'Café preto';
}
}
// Decoradores
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 2;
}
description() {
return this.coffee.description() + ', com leite';
}
}
class CaramelDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1.5;
}
description() {
return this.coffee.description() + ', com calda de caramelo';
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}
description() {
return this.coffee.description() + ', com chantilly';
}
}
// Uso
let coffee = new Coffee();
console.log(`${coffee.description()} - $${coffee.cost()}`);
// Café preto - $5
coffee = new MilkDecorator(coffee);
console.log(`${coffee.description()} - $${coffee.cost()}`);
// Café preto, com leite - $7
coffee = new CaramelDecorator(coffee);
console.log(`${coffee.description()} - $${coffee.cost()}`);
// Café preto, com leite, com calda de caramelo - $8.5
coffee = new WhippedCreamDecorator(coffee);
console.log(`${coffee.description()} - $${coffee.cost()}`);
// Café preto, com leite, com calda de caramelo, com chantilly - $9.5
A vantagem é clara: você compõe funcionalidades em tempo de execução sem criar classes explosivas como CoffeeWithMilkAndCaramelAndWhippedCream. Cada decorator é independente e reutilizável.
Composite Pattern: Estruturas Hierárquicas Simplificadas
O Composite permite compor objetos em estruturas de árvore, tratando objetos individuais e composições uniformemente. Perfeito para menus, estruturas de arquivos ou qualquer hierarquia.
Um exemplo prático é um menu de aplicação com submenus:
// Interface comum
class MenuItem {
execute() {}
}
// Leaf (folha)
class Command extends MenuItem {
constructor(name, action) {
super();
this.name = name;
this.action = action;
}
execute() {
console.log(`Executando: ${this.name}`);
this.action();
}
}
// Composite (ramo)
class Menu extends MenuItem {
constructor(name) {
super();
this.name = name;
this.items = [];
}
add(item) {
this.items.push(item);
return this;
}
remove(item) {
this.items = this.items.filter(i => i !== item);
return this;
}
execute() {
console.log(`\n=== ${this.name} ===`);
this.items.forEach(item => item.execute());
}
}
// Uso
const mainMenu = new Menu('Menu Principal');
const fileMenu = new Menu('Arquivo');
fileMenu
.add(new Command('Novo', () => console.log(' Criando novo documento...')))
.add(new Command('Abrir', () => console.log(' Abrindo arquivo...')))
.add(new Command('Salvar', () => console.log(' Salvando...')));
const editMenu = new Menu('Editar');
editMenu
.add(new Command('Copiar', () => console.log(' Copiando...')))
.add(new Command('Colar', () => console.log(' Colando...')));
mainMenu.add(fileMenu).add(editMenu);
mainMenu.execute();
// === Menu Principal ===
// === Arquivo ===
// Criando novo documento...
// Abrindo arquivo...
// Salvando...
// === Editar ===
// Copiando...
// Colando...
O ganho é elegante: tratamos Menu e Command pela mesma interface. Adicionar novos comandos ou submenus não altera o código existente. A recursão natural da árvore torna tudo simplista.
Conclusão
Dominei três patterns fundamentais que transformam seu código:
-
Strategy elimina condicionais para seleção de algoritmos, tornando o código extensível sem modificação.
-
Decorator substitui herança profunda por composição, adicionando funcionalidades de forma granular e reutilizável.
-
Composite simplifica trabalho com estruturas hierárquicas, tratando partes e todo de forma uniforme.
Esses padrões não são fins em si mesmos—são ferramentas. Use-os quando o problema os justificar, não por usar design patterns.