Padrão Comportamental (GoF)

Strategy

Define uma família de algoritmos, encapsula cada um deles e os torna intercambiáveis — permitindo que o algoritmo varie independentemente dos clientes que o utilizam e seja trocado em tempo de execução.

Intenção

Encapsular cada variação de algoritmo em uma classe (ou função) separada, tornando-as intercambiáveis. O contexto que usa o algoritmo não sabe qual implementação está ativa — ele apenas chama o método da interface. Isso permite trocar o comportamento em runtime sem alterar o contexto.

O Strategy pertence à categoria de padrões comportamentais no catálogo do GoF (Gamma, Helm, Johnson e Vlissides, 1994). É um dos padrões mais aplicados na prática — frequentemente sem que o desenvolvedor perceba que está seguindo um padrão nomeado — e é o mecanismo central por trás de muitos frameworks de validação, ordenação e roteamento.

Problema

Considere um sistema de e-commerce que calcula o frete de um pedido. No início, havia apenas uma transportadora. Com o crescimento, surgiram várias opções: Correios (PAC e SEDEX), transportadoras privadas, retirada na loja e entrega expressa. A implementação ingênua concentra toda a lógica de cálculo numa única classe com condicionais crescentes:

// Abordagem ingênua — NÃO faça isso:
calcularFrete(tipo: string, peso: number, distancia: number): number {
  if (tipo === "pac") {
    return peso * 0.05 + distancia * 0.01;
  } else if (tipo === "sedex") {
    return peso * 0.12 + distancia * 0.02 + 5;
  } else if (tipo === "expressa") {
    return peso * 0.20 + distancia * 0.03 + 15;
  } else if (tipo === "retirada") {
    return 0;
  }
  throw new Error(`Tipo desconhecido: ${tipo}`);
}

Cada nova transportadora exige modificar esse método: risco de regressão, dificuldade de testar cada caso de forma isolada, e violação do Princípio Aberto/Fechado. Além disso, a lógica de cálculo não pode ser reutilizada de forma independente — ela está presa dentro da condicional.

O padrão Strategy resolve isso extraindo cada algoritmo de cálculo para sua própria classe, que implementa uma interface comum. O contexto (pedido, carrinho) recebe a estratégia por injeção e a chama sem saber qual é.

Solução

O Strategy organiza o código em três participantes:

  1. Strategy (interface): declara o método que todos os algoritmos devem implementar. Ex.: EstrategiaFrete com o método calcular(peso, distancia).
  2. ConcreteStrategy: implementa o algoritmo específico. Ex.: FreteCorreiosPac, FreteCorreiosSedex, FreteExpresso, RetiradaNaLoja.
  3. Context: mantém uma referência a um objeto Strategy e delega o trabalho a ele. O contexto pode permitir troca de estratégia em tempo de execução via um setter. Ex.: classe Pedido que armazena a estratégia de frete e a invoca ao calcular o total.

Strategy em TypeScript: funções como estratégias

Em TypeScript (e JavaScript), funções são cidadãs de primeira classe — é possível passar uma função diretamente como estratégia, sem necessidade de criar uma classe extra. Esta variante é mais leve e idiomática em TS para estratégias sem estado:

// Em vez de uma interface com classe, a estratégia é simplesmente um tipo de função:
type EstrategiaFrete = (peso: number, distancia: number) => number;

const pacStrategy: EstrategiaFrete   = (p, d) => p * 0.05 + d * 0.01;
const sedexStrategy: EstrategiaFrete = (p, d) => p * 0.12 + d * 0.02 + 5;

// O contexto aceita qualquer função do tipo correto:
class Pedido {
  constructor(private estrategiaFrete: EstrategiaFrete) {}

  calcularFrete(peso: number, distancia: number): number {
    return this.estrategiaFrete(peso, distancia);
  }
}

Em PHP, funções de primeira classe também existem (callables, closures, arrow functions), mas para estratégias com estado ou que precisam de dependências injetadas, interfaces com classes concretas são mais idiomáticas e recomendadas pela tipagem estrita do PHPStan/Psalm.

Estrutura

       «interface»
      EstrategiaFrete
  ┌──────────────────────────────────┐
  │ + calcular(peso, dist): number   │
  └──────────────────────────────────┘
              ▲
   ┌──────────┼──────────────────────┐
   │          │                      │
FreteCorreiosPac  FreteCorreiosSedex  RetiradaNaLoja
(Concrete)        (Concrete)          (Concrete)


          Pedido (Context)
  ┌───────────────────────────────────────┐
  │ - estrategia: EstrategiaFrete         │
  │ + setEstrategia(e: EstrategiaFrete)   │
  │ + calcularFrete(peso, dist): number   │
  └───────────────────────────────────────┘
              │ usa (delega para)
              ▼
         EstrategiaFrete


Fluxo de troca em runtime:

  pedido.setEstrategia(new FreteCorreiosPac())
  pedido.calcularFrete(2, 50)  →  FreteCorreiosPac.calcular(2, 50)

  // Usuário muda para entrega expressa:
  pedido.setEstrategia(new FreteExpresso())
  pedido.calcularFrete(2, 50)  →  FreteExpresso.calcular(2, 50)
  // Sem alterar Pedido; sem if/else.

Exemplos de código

Exemplo 1 — Estratégias de cálculo de frete

Implementação completa com interface tipada, concretes e contexto. A estratégia pode ser trocada a qualquer momento via setter.

// ── Strategy interface ────────────────────────────────────────
interface EstrategiaFrete {
  calcular(pesoKg: number, distanciaKm: number): number;
  descricao(): string;
}

// ── ConcreteStrategies ────────────────────────────────────────
class FreteCorreiosPac implements EstrategiaFrete {
  calcular(pesoKg: number, distanciaKm: number): number {
    return pesoKg * 0.05 + distanciaKm * 0.01;
  }
  descricao(): string { return "Correios PAC (econômico)"; }
}

class FreteCorreiosSedex implements EstrategiaFrete {
  calcular(pesoKg: number, distanciaKm: number): number {
    return pesoKg * 0.12 + distanciaKm * 0.02 + 5;
  }
  descricao(): string { return "Correios SEDEX (expresso)"; }
}

class FreteTransportadora implements EstrategiaFrete {
  constructor(private readonly taxaBase: number) {}

  calcular(pesoKg: number, distanciaKm: number): number {
    return this.taxaBase + pesoKg * 0.08 + distanciaKm * 0.015;
  }
  descricao(): string { return `Transportadora (taxa base: R$${this.taxaBase})`; }
}

class RetiradaNaLoja implements EstrategiaFrete {
  calcular(_pesoKg: number, _distanciaKm: number): number { return 0; }
  descricao(): string { return "Retirada na loja (grátis)"; }
}

// ── Context ───────────────────────────────────────────────────
class Pedido {
  private estrategia: EstrategiaFrete;

  constructor(estrategia: EstrategiaFrete) {
    this.estrategia = estrategia;
  }

  // Permite trocar a estratégia em runtime (ex.: usuário muda opção de entrega).
  setEstrategiaFrete(estrategia: EstrategiaFrete): void {
    this.estrategia = estrategia;
  }

  calcularFrete(pesoKg: number, distanciaKm: number): number {
    return this.estrategia.calcular(pesoKg, distanciaKm);
  }

  resumo(pesoKg: number, distanciaKm: number): void {
    const valor = this.calcularFrete(pesoKg, distanciaKm);
    console.log(
      `${this.estrategia.descricao()}: R$${valor.toFixed(2)}`
    );
  }
}

// ── Uso ──────────────────────────────────────────────────────
const pedido = new Pedido(new FreteCorreiosPac());
pedido.resumo(3, 200);
// → Correios PAC (econômico): R$2.15

pedido.setEstrategiaFrete(new FreteCorreiosSedex());
pedido.resumo(3, 200);
// → Correios SEDEX (expresso): R$9.36

pedido.setEstrategiaFrete(new FreteTransportadora(10));
pedido.resumo(3, 200);
// → Transportadora (taxa base: R$10): R$13.24

pedido.setEstrategiaFrete(new RetiradaNaLoja());
pedido.resumo(3, 200);
// → Retirada na loja (grátis): R$0.00

Exemplo 2 — Estratégia como função (TypeScript) vs callable (PHP)

Para estratégias simples sem estado próprio, funções são mais leves do que classes. TypeScript trata funções como tipos de primeira classe; PHP usa callable ou closures. A diferença é relevante para a testabilidade e para a injeção de dependências.

// Estratégia como tipo de função — sem classes extras.
type Comparador<T> = (a: T, b: T) => number;

// Estratégias concretas: simples arrow functions.
const porPrecoAsc: Comparador<{ preco: number }>  = (a, b) => a.preco - b.preco;
const porPrecoDesc: Comparador<{ preco: number }> = (a, b) => b.preco - a.preco;
const porNome: Comparador<{ nome: string }>       = (a, b) => a.nome.localeCompare(b.nome);

// Contexto genérico: aceita qualquer estratégia compatível com o tipo T.
function ordenar<T>(itens: T[], comparador: Comparador<T>): T[] {
  return [...itens].sort(comparador);
}

// ── Uso ──────────────────────────────────────────────────────
const produtos = [
  { nome: "Caneta",   preco: 2.50 },
  { nome: "Caderno",  preco: 18.90 },
  { nome: "Borracha", preco: 1.20 },
];

console.log(ordenar(produtos, porPrecoAsc).map(p => p.nome));
// ["Borracha", "Caneta", "Caderno"]

console.log(ordenar(produtos, porPrecoDesc).map(p => p.nome));
// ["Caderno", "Caneta", "Borracha"]

console.log(ordenar(produtos, porNome).map(p => p.nome));
// ["Borracha", "Caderno", "Caneta"]

// Estratégia anônima inline — para casos pontuais sem reuso:
const porNomeDesc = ordenar(produtos, (a, b) => b.nome.localeCompare(a.nome));
console.log(porNomeDesc.map(p => p.nome));
// ["Caneta", "Caderno", "Borracha"]

Quando usar

  • Quando há múltiplas variações de um algoritmo que precisam ser intercambiáveis — cálculo de frete, desconto, validação, ordenação, compressão, autenticação, serialização.
  • Quando o algoritmo precisa variar em runtime: o usuário escolhe o método de pagamento, a opção de entrega, o formato de exportação. Com Strategy, a troca é simples — um setter ou uma injeção na construção.
  • Para eliminar condicionais repetitivas: se você tem um if/else ou switch que cresce a cada nova variação e está espalhado por vários métodos, Strategy extrai cada ramo para sua própria classe testável.
  • Para isolar algoritmos em testes unitários: cada estratégia pode ser testada de forma independente, sem precisar do contexto completo.

Quando evitar

  • Quando há apenas dois casos simples e estáveis: um if/else direto no código é mais legível do que criar uma interface, duas classes e um mecanismo de injeção para algo que nunca mudará.
  • Quando o algoritmo não tem variação real: se a "estratégia" é sempre a mesma e nenhuma alternativa está prevista, o padrão adiciona abstração desnecessária.
  • Quando o contexto precisa conhecer detalhes de cada estratégia: se o contexto precisa fazer downcasting para saber qual estratégia está ativa e agir diferente em cada caso, a abstração foi mal aplicada — a lógica condicional voltou disfarçada.

Prós e contras

Prós

  • Permite trocar algoritmos em runtime sem alterar o contexto.
  • Isola cada variação de algoritmo em sua própria classe — mais fácil de testar, entender e manter.
  • Elimina condicionais longas (if/else ou switch) que crescem a cada nova variação.
  • Segue o Princípio Aberto/Fechado — novas estratégias não exigem modificar o contexto.
  • Favorece composição sobre herança — o comportamento é injetado, não herdado.

Contras

  • Aumenta o número de classes/objetos no sistema — overengineering para casos triviais.
  • O cliente precisa conhecer as estratégias disponíveis para escolher e injetar a correta.
  • Estratégias sem estado poderiam ser funções simples — criar uma classe completa para isso é verboso, especialmente em PHP.
  • Se as estratégias precisam de muitos dados do contexto, a interface pode se tornar acoplada ao contexto, reduzindo a flexibilidade.

Armadilhas comuns

1. Overengineering para dois casos triviais

O erro mais frequente: aplicar Strategy quando há exatamente dois casos e nenhuma perspectiva de crescimento. Se o sistema sempre terá "frete grátis para compras acima de R$100" e "frete fixo de R$15 para as demais", um simples ternário é mais legível do que uma interface com duas classes. Reserve Strategy para quando a variação é o ponto de extensão real do design.

Regra prática: se você se pergunta "vale a pena extrair isso para Strategy?", pergunte primeiro se há três ou mais variações — ou se uma terceira é uma certeza no horizonte. Com duas variações fixas, a resposta geralmente é não.

2. Vazamento de contexto para a estratégia

A interface da estratégia deve receber apenas os dados de que o algoritmo precisa — não o contexto inteiro. Passar o objeto Pedido completo para EstrategiaFrete.calcular(pedido) cria acoplamento bidirecional: a estratégia passa a depender da estrutura interna do contexto. Prefira parâmetros primitivos ou DTOs específicos: calcular(pesoKg, distanciaKm).

3. Estado compartilhado entre chamadas

Se a estratégia acumula estado entre chamadas (ex.: contagem de requisições, cache interno), ela deixa de ser segura para uso em ambientes concorrentes (Node.js multi-request, servidores multi-thread) ou para reutilização em testes. Estratégias devem ser stateless sempre que possível — ou, se precisam de estado, esse estado deve ser passado explicitamente na chamada do método.

4. Strategy vs Template Method

Ambos resolvem a variação de comportamento, mas de formas opostas. O Template Method usa herança: a classe base define o esqueleto e as subclasses preenchem os passos variáveis — o algoritmo é fixo, apenas partes dele variam. O Strategy usa composição: o algoritmo inteiro é trocável, o contexto é estável. Prefira Strategy quando o algoritmo completo varia; prefira Template Method quando apenas partes do algoritmo variam e a estrutura geral é fixa.

Padrões relacionados

O Strategy interage com vários outros padrões comportamentais e de criação:

O Factory Method frequentemente é usado para criar e injetar a estratégia correta no contexto: o creator seleciona qual ConcreteStrategy instanciar com base em configuração ou tipo, entregando-a pronta para o Context. O padrão State é estruturalmente idêntico ao Strategy — ambos encapsulam comportamento variável em objetos intercambiáveis — mas com intenções diferentes: Strategy troca algoritmos (o contexto não sabe qual está ativo e não se importa), enquanto State gerencia transições de estado (o objeto se comporta diferente em cada estado e as transições são parte do design). O Template Method é o "primo de herança" do Strategy: usa herança para variar partes do algoritmo, enquanto Strategy usa composição para variar o algoritmo inteiro.