Como o app de mapas escolhe a rota
Mesmo destino, mas você troca entre 'mais rápida', 'sem pedágio' ou 'a pé'. Cada opção é um algoritmo diferente calculando o mesmo resultado (a rota). Você troca a estratégia sem trocar o mapa. Strategy é isso: algoritmos intercambiáveis atrás da mesma interface.
O problema
Uma tarefa tem várias variações de algoritmo (frete por transportadora, desconto por cliente...). Concentrar tudo numa classe vira um emaranhado de if/else que cresce a cada variação.
E isso viola o Aberto/Fechado: cada novo comportamento obriga a mexer no código que já funciona.
A raiz do problema é misturar o que é estável com o que varia. A estrutura da tarefa ('calcular o total do carrinho') quase não muda; a regra interna ('como calcular o frete') muda toda hora. Enquanto os dois moram na mesma classe, cada mudança na parte volátil arrisca a parte estável — e o condicional que escolhe a variação se duplica em todo lugar que precisa da mesma decisão.
A alternativa clássica é herança: criar CarrinhoComSedex, CarrinhoComPac... Funciona até precisar variar uma segunda dimensão (desconto, embalagem) — aí as subclasses explodem em combinações, e o comportamento fica fixado em tempo de compilação. Strategy troca herança por composição: o comportamento é um objeto que se injeta e se troca em runtime, e cada dimensão de variação vira uma família de estratégias independente.
Uma nuance de linguagem: em TypeScript, Python e afins, uma estratégia pode ser só uma função — passar calcularFrete como parâmetro já é Strategy em espírito. A classe vale a pena quando a estratégia tem dependências próprias, várias operações relacionadas ou precisa de configuração; o padrão é o contrato estável, não a cerimônia de classes.
- Cliente
- injeta e chamaContexto
- delega executar()Interface Estrategia
- despacha paraEstratégia concreta
Com Strategy: uma classe por regra
- FreteSedex, FretePac, FreteRetirada implementam a mesma interface
- modalidade nova é arquivo novo — o cálculo existente não é tocado
- cada regra é testável isolada, sem montar o contexto
- quem injeta decide, em tempo de execução
O custo se desloca para a indireção: mais arquivos e um salto a mais para ler o fluxo. Vale quando as regras mudam com frequência — não quando são duas e estáveis.
Troque a estratégia e veja recalcular
Mexa nos controles: a teoria acima vira comportamento observável.
Frete (Normal · 5 dias)
R$ 11.00
O cálculo (algoritmo) é trocável em runtime. O cliente só conhece a interfacecalcularFrete(peso).
- 1
Definir a interface
Uma interface comum descreve o algoritmo (ex.: calcularFrete).
- 2
Implementar variações
Cada algoritmo vira uma classe que implementa a interface.
- 3
Injetar no contexto
O contexto recebe uma estratégia e guarda a referência.
- 4
Delegar
O contexto chama a estratégia — e você pode trocá-la em runtime.
Navegue pelas camadas
Camada 1/4Cliente
use ← →Camada 1
Cliente
escolhe e injeta a estratégia
É o único que conhece as estratégias concretas. Decide qual usar — por escolha do usuário, configuração, feature flag — e injeta no contexto. Concentrar a escolha aqui mantém o resto do sistema ignorante das variações.
No código
const carrinho = new Carrinho(new Pac());
carrinho.usarFrete(new Sedex()); // troca em runtimeSe violar: se a escolha se espalhar por vários pontos do código, cada estratégia nova exige caçar todos os lugares que decidem — o condicional voltou, só que distribuído.
Estrutura (diagrama de classes)
Código
interface Frete {
calcular(pesoKg: number): number;
}
class Sedex implements Frete {
calcular(pesoKg: number) { return 20 + pesoKg * 3; }
}
class Pac implements Frete {
calcular(pesoKg: number) { return 12 + pesoKg * 1.5; }
}
class Carrinho {
constructor(private frete: Frete) {}
usarFrete(f: Frete) { this.frete = f; }
total(subtotal: number, pesoKg: number) {
return subtotal + this.frete.calcular(pesoKg);
}
}
const carrinho = new Carrinho(new Pac());
console.log(carrinho.total(100, 2)); // 115
carrinho.usarFrete(new Sedex());
console.log(carrinho.total(100, 2)); // 126from typing import Protocol
class Frete(Protocol):
def calcular(self, peso_kg: float) -> float: ...
class Sedex:
def calcular(self, peso_kg: float) -> float:
return 20 + peso_kg * 3
class Pac:
def calcular(self, peso_kg: float) -> float:
return 12 + peso_kg * 1.5
class Carrinho:
def __init__(self, frete: Frete):
self._frete = frete
def usar_frete(self, frete: Frete):
self._frete = frete
def total(self, subtotal: float, peso_kg: float) -> float:
return subtotal + self._frete.calcular(peso_kg)
carrinho = Carrinho(Pac())
print(carrinho.total(100, 2)) # 115.0
carrinho.usar_frete(Sedex())
print(carrinho.total(100, 2)) # 126.0Casos de uso
Cálculo de frete por transportadora
Cenário
Um e-commerce cotiza com Correios, transportadora própria e retirada na loja — cada uma com tabela, prazo e regra de peso diferentes, e novas parcerias chegando todo trimestre.
Como o padrão entra
Interface Frete com calcular(); uma classe por transportadora; o checkout injeta a estratégia conforme a escolha do usuário. Parceria nova = classe nova, sem tocar no fluxo do pedido.
Trade-off: Alguém ainda precisa mapear escolha→estratégia; na prática esse papel vai para uma factory, e o par Strategy + Factory vira o desenho completo.
Precificação por tipo de cliente
Cenário
Varejo, atacado e assinante premium pagam preços diferentes pelo mesmo catálogo, e o comercial inventa política nova a cada campanha.
Como o padrão entra
Uma PoliticaPreco por perfil, selecionada no login ou na sessão. Cada política é testada isoladamente com casos de borda próprios — e a campanha nova entra sem regressão nas demais.
Trade-off: Quando as dimensões se multiplicam (perfil × campanha × região), uma estratégia por combinação explode; passa a ser preciso compor estratégias, e composição tem custo próprio de design.
Compressão e serialização plugáveis
Cenário
Um serviço exporta relatórios em JSON, CSV ou Parquet, e comprime a resposta com gzip ou brotli conforme o que o cliente aceita.
Como o padrão entra
Interfaces Serializador e Compressor; a escolha vem do content-type e do header Accept-Encoding. O pipeline de exportação é escrito uma vez e nunca sabe qual formato está gerando.
Trade-off: Formatos têm capacidades desiguais (streaming, tipos ricos, schema) — a interface comum nivela por baixo, e recursos exclusivos de um formato ficam de fora do contrato.
A Strategy que continua sendo um if/else
Compila, roda, passa no code review. E quebra depois.
As classes de estratégia existem, implementam a interface, tudo parece certo — mas quem escolhe qual usar é um switch dentro da própria classe que deveria ignorar a escolha. O polimorfismo foi adiado, não aplicado.
interface Frete { calcular(peso: number): number; }
class Sedex implements Frete { calcular(p: number) { return p * 2.5; } }
class Pac implements Frete { calcular(p: number) { return p * 1.2; } }
class Carrinho {
// A interface existe... e a decisao continua aqui dentro.
private estrategia(tipo: string): Frete {
switch (tipo) {
case "sedex": return new Sedex();
case "pac": return new Pac();
default: throw new Error("tipo desconhecido");
}
}
total(peso: number, tipo: string) {
return this.estrategia(tipo).calcular(peso); // <- Carrinho conhece TODAS
}
}
// Transportadora nova = editar Carrinho. O Aberto/Fechado continua violado,
// so que agora com tres arquivos em vez de um.O que quebra
- Ao adicionar varianteÉ preciso editar a classe que deveria ser fechada para modificação — exatamente o que o padrão prometia evitar.
- No testeTestar
Carrinhoexige conhecer todas as estratégias reais, porque não há como injetar uma falsa. - Na revisãoO
switchse multiplica: aparece de novo em cada lugar que precisa escolher, e eles saem de sincronia.
A saída: A estratégia entra de fora — por construtor ou parâmetro. Se ainda for preciso mapear string para implementação, esse mapa vive na composição da aplicação (ou numa Factory), e não dentro de quem usa a estratégia.
Do cheiro ao padrão
Um passo por vez, com o motivo de cada um.
O cheiro: Um switch que cresce a cada regra nova de frete, dentro da classe que só queria somar o carrinho.
Como está. Um switch que cresce a cada regra nova de frete, dentro da classe que só queria somar o carrinho.
class Carrinho {
total(peso: number, tipo: string) {
let frete = 0;
if (tipo === "sedex") frete = peso * 2.5;
else if (tipo === "pac") frete = peso * 1.2;
else if (tipo === "retirada") frete = 0;
else throw new Error("tipo desconhecido");
return this.soma() + frete;
}
}Armadilhas
Strategy para duas variações estáveis
Interface + duas classes + injeção para um comportamento que tem duas ramificações e não muda há dois anos é overkill: um if legível ganha em clareza e custo. O padrão paga o próprio preço quando a variação é real e crescente — aplique quando a terceira variação aparecer no horizonte, não antes (YAGNI).
O if/else gigante só mudou de lugar
Se o cliente escolhe a estratégia com uma cadeia enorme de condicionais, o emaranhado migrou do contexto para o cliente — o problema trocou de endereço, não sumiu. Combine com Factory: um mapa de nome→estratégia reduz a escolha a um lookup e concentra o cadastro de variações num único lugar.
Estratégias com estado compartilhado
Estratégia que acumula estado entre chamadas ou mexe em estado do contexto deixa de ser intercambiável: trocar no meio de uma operação corrompe dados, e uma instância compartilhada entre requests vira condição de corrida. Prefira estratégias stateless que recebem tudo por parâmetro — o que também as torna triviais de testar.
Quando usar × quando evitar
Use quando
3- Há várias maneiras de fazer a mesma coisa e você quer escolher uma em tempo de execução.
- Uma classe está cheia de condicionais que selecionam comportamentos parecidos.
- Você quer isolar um algoritmo volátil do restante do código.
Evite quando
2- Só existem poucas variações estáveis que raramente mudam — a indireção não compensa.
- O cliente teria de conhecer as diferenças entre estratégias para escolher.
Testar o que ficou
3 armadilhas de Strategy viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
O comparador de Array.sortO algoritmo de ordenação é fixo; o critério de comparação entra por parâmetro.Strategies do PassportO nome é literal: local, OAuth e JWT são estratégias plugáveis atrás de uma interface só.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// A estrategia entra de fora. So isso.
const total = (itens, calcularFrete) =>
soma(itens) + calcularFrete(peso(itens));O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Uma interface e uma classe (ou função) por variante
- A escolha de qual estratégia usar migra para quem compõe — e precisa morar em algum lugar
Não vale se: existem duas variantes e elas nunca mudam. Um if é mais honesto que uma hierarquia.