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:
-
Strategy (interface): declara o método que todos os
algoritmos devem implementar. Ex.:
EstrategiaFretecom o métodocalcular(peso, distancia). -
ConcreteStrategy: implementa o algoritmo específico.
Ex.:
FreteCorreiosPac,FreteCorreiosSedex,FreteExpresso,RetiradaNaLoja. -
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
Pedidoque 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
<?php
// ── Strategy interface ────────────────────────────────────────
interface EstrategiaFrete
{
public function calcular(float $pesoKg, float $distanciaKm): float;
public function descricao(): string;
}
// ── ConcreteStrategies ────────────────────────────────────────
class FreteCorreiosPac implements EstrategiaFrete
{
public function calcular(float $pesoKg, float $distanciaKm): float
{
return $pesoKg * 0.05 + $distanciaKm * 0.01;
}
public function descricao(): string
{
return 'Correios PAC (econômico)';
}
}
class FreteCorreiosSedex implements EstrategiaFrete
{
public function calcular(float $pesoKg, float $distanciaKm): float
{
return $pesoKg * 0.12 + $distanciaKm * 0.02 + 5;
}
public function descricao(): string
{
return 'Correios SEDEX (expresso)';
}
}
class FreteTransportadora implements EstrategiaFrete
{
public function __construct(private readonly float $taxaBase) {}
public function calcular(float $pesoKg, float $distanciaKm): float
{
return $this->taxaBase + $pesoKg * 0.08 + $distanciaKm * 0.015;
}
public function descricao(): string
{
return "Transportadora (taxa base: R\${$this->taxaBase})";
}
}
class RetiradaNaLoja implements EstrategiaFrete
{
public function calcular(float $pesoKg, float $distanciaKm): float
{
return 0.0;
}
public function descricao(): string
{
return 'Retirada na loja (grátis)';
}
}
// ── Context ───────────────────────────────────────────────────
class Pedido
{
public function __construct(private EstrategiaFrete $estrategia) {}
// Permite trocar a estratégia em runtime.
public function setEstrategiaFrete(EstrategiaFrete $estrategia): void
{
$this->estrategia = $estrategia;
}
public function calcularFrete(float $pesoKg, float $distanciaKm): float
{
return $this->estrategia->calcular($pesoKg, $distanciaKm);
}
public function resumo(float $pesoKg, float $distanciaKm): void
{
$valor = $this->calcularFrete($pesoKg, $distanciaKm);
echo sprintf(
"%s: R$%.2f\n",
$this->estrategia->descricao(),
$valor
);
}
}
// ── Uso ──────────────────────────────────────────────────────
$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"]
<?php
// Em PHP, callable e closures funcionam como estratégias funcionais.
// Para PHPStan/Psalm, docblocks de tipo ajudam na análise estática.
/** @param array<array{nome: string, preco: float}> $itens */
function ordenar(array $itens, callable $comparador): array
{
usort($itens, $comparador);
return $itens;
}
// Estratégias como arrow functions (PHP 7.4+):
$porPrecoAsc = fn($a, $b) => $a['preco'] <=> $b['preco'];
$porPrecoDesc = fn($a, $b) => $b['preco'] <=> $a['preco'];
$porNome = fn($a, $b) => strcmp($a['nome'], $b['nome']);
// ── Uso ──────────────────────────────────────────────────────
$produtos = [
['nome' => 'Caneta', 'preco' => 2.50],
['nome' => 'Caderno', 'preco' => 18.90],
['nome' => 'Borracha', 'preco' => 1.20],
];
$resultado = ordenar($produtos, $porPrecoAsc);
echo implode(', ', array_column($resultado, 'nome')) . PHP_EOL;
// Borracha, Caneta, Caderno
$resultado = ordenar($produtos, $porPrecoDesc);
echo implode(', ', array_column($resultado, 'nome')) . PHP_EOL;
// Caderno, Caneta, Borracha
$resultado = ordenar($produtos, $porNome);
echo implode(', ', array_column($resultado, 'nome')) . PHP_EOL;
// Borracha, Caderno, Caneta
// Estratégia inline anônima:
$resultado = ordenar($produtos, fn($a, $b) => strcmp($b['nome'], $a['nome']));
echo implode(', ', array_column($resultado, 'nome')) . PHP_EOL;
// 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/elseouswitchque 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/elsedireto 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.