Design Patterns em JavaScript: Strategy, Decorator e Composite: Do Básico ao Avançado Já leu

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 gigante, criamos estratégias independentes: Pagando $${amount} com cartão ${this.cardNumber} Pagando $${amount} via PayPal (${this.email}) Pagando ${amount} BTC para ${this.walletAddress} O benefício real é a manutenibilidade: adicionar novo método de pagamento não requer modificar , 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: ${coffee.description()} - $${coffee.cost()} ${coffee.description()} - $${coffee.cost()} ${coffee.description()} -

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:

  1. Strategy elimina condicionais para seleção de algoritmos, tornando o código extensível sem modificação.

  2. Decorator substitui herança profunda por composição, adicionando funcionalidades de forma granular e reutilizável.

  3. 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.

Referências


Artigos relacionados