JavaScript Avançado

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

6 min de leitura

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

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()} -

<h2>Strategy Pattern: Flexibilidade no Comportamento</h2>

<p>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.</p>

<p>Imagine um sistema de processamento de pagamentos. Em vez de usar um <code>if/else</code> gigante, criamos estratégias independentes:</p>

<pre><code class="language-javascript">// 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(&#039;1234-5678-9012-3456&#039;, &#039;123&#039;)

);

processor.processPayment(100); // Pagando $100 com cartão...

processor.setStrategy(new PayPalStrategy(&#039;user@example.com&#039;));

processor.processPayment(50); // Pagando $50 via PayPal...</code></pre>

<p>O benefício real é a manutenibilidade: adicionar novo método de pagamento não requer modificar <code>PaymentProcessor</code>, apenas criar uma nova estratégia. Isso respeita o princípio Open/Closed do SOLID.</p>

<h2>Decorator Pattern: Adicionando Funcionalidades Dinamicamente</h2>

<p>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.</p>

<p>Considere um sistema de cafeteria onde você constrói bebidas com adições:</p>

<pre><code class="language-javascript">// Componente base

class Coffee {

cost() {

return 5;

}

description() {

return &#039;Café preto&#039;;

}

}

// 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() + &#039;, com leite&#039;;

}

}

class CaramelDecorator extends CoffeeDecorator {

cost() {

return this.coffee.cost() + 1.5;

}

description() {

return this.coffee.description() + &#039;, com calda de caramelo&#039;;

}

}

class WhippedCreamDecorator extends CoffeeDecorator {

cost() {

return this.coffee.cost() + 1;

}

description() {

return this.coffee.description() + &#039;, com chantilly&#039;;

}

}

// 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</code></pre>

<p>A vantagem é clara: você compõe funcionalidades em tempo de execução sem criar classes explosivas como <code>CoffeeWithMilkAndCaramelAndWhippedCream</code>. Cada decorator é independente e reutilizável.</p>

<h2>Composite Pattern: Estruturas Hierárquicas Simplificadas</h2>

<p>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.</p>

<p>Um exemplo prático é um menu de aplicação com submenus:</p>

<pre><code class="language-javascript">// 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 =&gt; i !== item);

return this;

}

execute() {

console.log(\n=== ${this.name} ===);

this.items.forEach(item =&gt; item.execute());

}

}

// Uso

const mainMenu = new Menu(&#039;Menu Principal&#039;);

const fileMenu = new Menu(&#039;Arquivo&#039;);

fileMenu

.add(new Command(&#039;Novo&#039;, () =&gt; console.log(&#039; Criando novo documento...&#039;)))

.add(new Command(&#039;Abrir&#039;, () =&gt; console.log(&#039; Abrindo arquivo...&#039;)))

.add(new Command(&#039;Salvar&#039;, () =&gt; console.log(&#039; Salvando...&#039;)));

const editMenu = new Menu(&#039;Editar&#039;);

editMenu

.add(new Command(&#039;Copiar&#039;, () =&gt; console.log(&#039; Copiando...&#039;)))

.add(new Command(&#039;Colar&#039;, () =&gt; console.log(&#039; Colando...&#039;)));

mainMenu.add(fileMenu).add(editMenu);

mainMenu.execute();

// === Menu Principal ===

// === Arquivo ===

// Criando novo documento...

// Abrindo arquivo...

// Salvando...

// === Editar ===

// Copiando...

// Colando...</code></pre>

<p>O ganho é elegante: tratamos <code>Menu</code> e <code>Command</code> pela mesma interface. Adicionar novos comandos ou submenus não altera o código existente. A recursão natural da árvore torna tudo simplista.</p>

<h2>Conclusão</h2>

<p>Dominei três patterns fundamentais que transformam seu código:</p>

<ol>

<li><strong>Strategy</strong> elimina condicionais para seleção de algoritmos, tornando o código extensível sem modificação.</li>

</ol>

<ol>

<li><strong>Decorator</strong> substitui herança profunda por composição, adicionando funcionalidades de forma granular e reutilizável.</li>

</ol>

<ol>

<li><strong>Composite</strong> simplifica trabalho com estruturas hierárquicas, tratando partes e todo de forma uniforme.</li>

</ol>

<p>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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://refactoring.guru/design-patterns/book" target="_blank" rel="noopener noreferrer">Design Patterns: Elements of Reusable Object-Oriented Software</a> — Gamma, Helm, Johnson, Vlissides</li>

<li><a href="https://refactoring.guru/design-patterns/javascript" target="_blank" rel="noopener noreferrer">Refactoring Guru - Design Patterns JavaScript</a></li>

<li><a href="https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Classes" target="_blank" rel="noopener noreferrer">MDN Web Docs - Classes JavaScript</a></li>

<li><a href="https://github.com/getify/You-Dont-Know-JS" target="_blank" rel="noopener noreferrer">You Don&#039;t Know JS: Object Prototypes</a></li>

<li><a href="https://www.patterns.dev/posts/classic-design-patterns/" target="_blank" rel="noopener noreferrer">Pattern for JavaScript: Addy Osmani</a></li>

</ul>

Comentários

Mais em JavaScript Avançado

Como Usar Testes End-to-End com Playwright: Page Object Model e CI Integration em Produção
Como Usar Testes End-to-End com Playwright: Page Object Model e CI Integration em Produção

O que é Playwright e Por Que Page Object Model? Playwright é um framework de...

Guia Completo de Next.js Avançado: SSR, SSG, ISR e App Router com Server Components
Guia Completo de Next.js Avançado: SSR, SSG, ISR e App Router com Server Components

Renderização no Next.js: Entendendo SSR, SSG e ISR A escolha da estratégia de...

O que Todo Dev Deve Saber sobre Web Workers: Paralelismo Real no Navegador com JavaScript
O que Todo Dev Deve Saber sobre Web Workers: Paralelismo Real no Navegador com JavaScript

Web Workers: Paralelismo Real no Navegador com JavaScript Web Workers represe...