Quando usar Strategy: um ponto de variação que muda com frequência
O padrão Strategy encapsula um algoritmo (ou regra de negócio) em um objeto separado e permite trocar o comportamento em tempo de execução sem alterar o código do objeto que o utiliza. Na prática, você identifica um ponto de variação (algo que muda conforme contexto) e o transforma em uma estratégia com várias implementações.
O objetivo mais comum é eliminar blocos grandes de if/else ou switch que escolhem “qual regra aplicar”, substituindo-os por polimorfismo: o cliente injeta a estratégia adequada e o código principal apenas delega.
Sinais de que Strategy é uma boa escolha
- Há um método com muitos
if/elsebaseados em tipo, categoria, canal, região, forma de pagamento etc. - Novas regras surgem com frequência e exigem modificar o mesmo método repetidamente.
- Você quer testar cada regra isoladamente, sem montar o sistema inteiro.
- Você quer escolher a regra em tempo de execução (por configuração, input do usuário, A/B test, feature flags).
Exemplo prático: cálculo de frete como ponto de variação
Vamos modelar um cenário comum: um checkout precisa calcular frete, mas o algoritmo muda conforme o tipo de entrega (normal, expresso, retirada). Em vez de if/else no checkout, criaremos estratégias.
Passo 1: definir o contrato da estratégia
O contrato deve ser pequeno e focado: recebe os dados necessários e retorna o resultado. Evite “estratégias genéricas” que recebem o mundo inteiro.
public interface FreteStrategy { Dinheiro calcularFrete(Pedido pedido);}Para manter o exemplo simples, considere Dinheiro como um value object (já visto anteriormente no curso) que representa valores monetários com segurança.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Passo 2: criar implementações concretas
Cada algoritmo vira uma classe. Isso facilita testes e evolução independente.
public final class FreteNormalStrategy implements FreteStrategy { @Override public Dinheiro calcularFrete(Pedido pedido) { // Exemplo: base + proporcional ao peso Dinheiro base = Dinheiro.reais(15); Dinheiro porKg = Dinheiro.reais(4).multiplicar(pedido.getPesoEmKg()); return base.somar(porKg); }}public final class FreteExpressoStrategy implements FreteStrategy { @Override public Dinheiro calcularFrete(Pedido pedido) { // Exemplo: mais caro e com mínimo Dinheiro base = Dinheiro.reais(30); Dinheiro porKg = Dinheiro.reais(7).multiplicar(pedido.getPesoEmKg()); Dinheiro total = base.somar(porKg); return total.max(Dinheiro.reais(45)); }}public final class FreteRetiradaNaLojaStrategy implements FreteStrategy { @Override public Dinheiro calcularFrete(Pedido pedido) { return Dinheiro.reais(0); }}Passo 3: integrar via composição (injeção por construtor)
O objeto que precisa do comportamento variável (o “contexto”) recebe a estratégia no construtor. Ele não decide qual estratégia usar; apenas delega.
public final class CheckoutService { private final FreteStrategy freteStrategy; public CheckoutService(FreteStrategy freteStrategy) { this.freteStrategy = freteStrategy; } public ResumoCheckout gerarResumo(Pedido pedido) { Dinheiro frete = freteStrategy.calcularFrete(pedido); Dinheiro total = pedido.getSubtotal().somar(frete); return new ResumoCheckout(pedido.getSubtotal(), frete, total); }}Passo 4: como o código cliente fica mais simples
O cliente escolhe a estratégia (por input, configuração, factory, etc.) e injeta no serviço. O serviço não cresce com novas regras.
Pedido pedido = ...;FreteStrategy strategy = new FreteExpressoStrategy();CheckoutService checkout = new CheckoutService(strategy);ResumoCheckout resumo = checkout.gerarResumo(pedido);Se amanhã surgir FreteAgendadoStrategy, você adiciona uma nova classe e passa a instância onde fizer sentido, sem tocar no CheckoutService.
Escolha de estratégia em tempo de execução (sem poluir o contexto)
É comum existir um ponto do sistema responsável por decidir qual estratégia usar. Para não reintroduzir um “switch gigante” espalhado, centralize a seleção em um lugar (por exemplo, uma factory simples ou um registry).
Opção A: seleção com Factory (centraliza a variação)
public enum TipoEntrega { NORMAL, EXPRESSO, RETIRADA }public final class FreteStrategyFactory { public FreteStrategy criar(TipoEntrega tipo) { return switch (tipo) { case NORMAL -> new FreteNormalStrategy(); case EXPRESSO -> new FreteExpressoStrategy(); case RETIRADA -> new FreteRetiradaNaLojaStrategy(); }; }}Note que o switch não desaparece do mundo, mas fica confinado em um ponto de criação/seleção. O restante do código trabalha com o contrato FreteStrategy.
Opção B: registry (mapa de estratégias) para reduzir alterações
Quando as estratégias são configuráveis, um registry evita editar um switch a cada nova estratégia.
public final class FreteStrategyRegistry { private final java.util.Map<TipoEntrega, FreteStrategy> estrategias; public FreteStrategyRegistry(java.util.Map<TipoEntrega, FreteStrategy> estrategias) { this.estrategias = java.util.Map.copyOf(estrategias); } public FreteStrategy obter(TipoEntrega tipo) { FreteStrategy strategy = estrategias.get(tipo); if (strategy == null) { throw new IllegalArgumentException("Tipo de entrega sem estratégia: " + tipo); } return strategy; }}FreteStrategyRegistry registry = new FreteStrategyRegistry(java.util.Map.of( TipoEntrega.NORMAL, new FreteNormalStrategy(), TipoEntrega.EXPRESSO, new FreteExpressoStrategy(), TipoEntrega.RETIRADA, new FreteRetiradaNaLojaStrategy()));FreteStrategy strategy = registry.obter(tipoEntrega);CheckoutService checkout = new CheckoutService(strategy);Refatoração orientada: de if/else para estratégias testáveis
A seguir, um roteiro prático para transformar um método com condicionais em Strategy. O exemplo será de desconto, mas o processo é o mesmo para validação, cálculo de imposto, roteamento, etc.
Cenário inicial (code smell): desconto com if/else
public final class CalculadoraDeDesconto { public Dinheiro calcular(Pedido pedido, String cupom) { if (cupom == null || cupom.isBlank()) { return Dinheiro.reais(0); } if (cupom.equalsIgnoreCase("VIP10")) { return pedido.getSubtotal().multiplicar(0.10); } else if (cupom.equalsIgnoreCase("FRETEGRATIS")) { // aqui o "desconto" representa abatimento equivalente ao frete return pedido.getFrete(); } else if (cupom.equalsIgnoreCase("QUEIMA20") && pedido.getSubtotal().maiorQue(Dinheiro.reais(200))) { return pedido.getSubtotal().multiplicar(0.20); } return Dinheiro.reais(0); }}Problemas típicos: método cresce, regras se misturam, testes ficam verbosos, e qualquer novo cupom exige editar a mesma classe.
Passo a passo da refatoração
Passo 1: nomear o ponto de variação e criar a interface
A variação é “como calcular desconto”. Defina um contrato.
public interface DescontoStrategy { Dinheiro calcular(Pedido pedido);}Passo 2: extrair uma estratégia por regra
Transforme cada ramo relevante em uma classe. Comece pelas regras mais estáveis ou mais usadas.
public final class DescontoVip10Strategy implements DescontoStrategy { @Override public Dinheiro calcular(Pedido pedido) { return pedido.getSubtotal().multiplicar(0.10); }}public final class DescontoQueima20Strategy implements DescontoStrategy { @Override public Dinheiro calcular(Pedido pedido) { if (pedido.getSubtotal().maiorQue(Dinheiro.reais(200))) { return pedido.getSubtotal().multiplicar(0.20); } return Dinheiro.reais(0); }}public final class DescontoFreteGratisStrategy implements DescontoStrategy { @Override public Dinheiro calcular(Pedido pedido) { return pedido.getFrete(); }}Se existir uma regra “sem desconto”, crie uma estratégia nula para evitar null e condicionais:
public final class SemDescontoStrategy implements DescontoStrategy { @Override public Dinheiro calcular(Pedido pedido) { return Dinheiro.reais(0); }}Passo 3: criar um resolvedor (mapeia cupom -> estratégia)
O objetivo é tirar do cálculo a responsabilidade de “descobrir qual regra aplicar”.
public final class DescontoStrategyResolver { private final java.util.Map<String, DescontoStrategy> porCupom; public DescontoStrategyResolver(java.util.Map<String, DescontoStrategy> porCupom) { // normaliza chaves para evitar equalsIgnoreCase espalhado java.util.Map<String, DescontoStrategy> normalizado = new java.util.HashMap<>(); for (var e : porCupom.entrySet()) { normalizado.put(e.getKey().toUpperCase(), e.getValue()); } this.porCupom = java.util.Map.copyOf(normalizado); } public DescontoStrategy resolver(String cupom) { if (cupom == null || cupom.isBlank()) { return new SemDescontoStrategy(); } return porCupom.getOrDefault(cupom.toUpperCase(), new SemDescontoStrategy()); }}Passo 4: reescrever o cliente para delegar
public final class CalculadoraDeDesconto { private final DescontoStrategyResolver resolver; public CalculadoraDeDesconto(DescontoStrategyResolver resolver) { this.resolver = resolver; } public Dinheiro calcular(Pedido pedido, String cupom) { DescontoStrategy strategy = resolver.resolver(cupom); return strategy.calcular(pedido); }}Agora, adicionar um novo cupom pode significar apenas: criar uma nova classe de estratégia e registrar no resolver (ou no mapa de configuração).
Testabilidade: estratégias pequenas, testes pequenos
Com Strategy, você testa cada regra sem precisar simular todos os cupons no mesmo teste. Exemplos com JUnit:
class DescontoVip10StrategyTest { @org.junit.jupiter.api.Test void aplicaDezPorCento() { Pedido pedido = PedidoFake.comSubtotal(Dinheiro.reais(100)); DescontoStrategy s = new DescontoVip10Strategy(); org.junit.jupiter.api.Assertions.assertEquals(Dinheiro.reais(10), s.calcular(pedido)); }}class DescontoQueima20StrategyTest { @org.junit.jupiter.api.Test void naoAplicaAbaixoDoMinimo() { Pedido pedido = PedidoFake.comSubtotal(Dinheiro.reais(199)); DescontoStrategy s = new DescontoQueima20Strategy(); org.junit.jupiter.api.Assertions.assertEquals(Dinheiro.reais(0), s.calcular(pedido)); } @org.junit.jupiter.api.Test void aplicaAcimaDoMinimo() { Pedido pedido = PedidoFake.comSubtotal(Dinheiro.reais(250)); DescontoStrategy s = new DescontoQueima20Strategy(); org.junit.jupiter.api.Assertions.assertEquals(Dinheiro.reais(50), s.calcular(pedido)); }}Boas práticas e armadilhas comuns ao aplicar Strategy
| Ponto | Recomendação prática |
|---|---|
| Contrato da estratégia | Mantenha o método pequeno e com parâmetros necessários. Se precisar de muitos dados, considere um objeto de contexto (ex.: DadosDeFrete) em vez de dezenas de parâmetros. |
| Evitar “Strategy Deus” | Não crie uma única estratégia com vários modos internos (isso recria o switch dentro da estratégia). Prefira uma classe por regra. |
| Seleção da estratégia | Centralize a escolha (factory/registry). O contexto deve delegar, não decidir. |
| Estratégia padrão | Use uma estratégia “nula” (ex.: SemDescontoStrategy) para evitar null e condicionais. |
| Explosão de classes | Se as regras forem muito simples, você pode usar lambdas com interface funcional, mantendo o mesmo desenho. |
Variação: Strategy com lambda (quando fizer sentido)
Se o contrato tiver um único método, você pode usar uma interface funcional e passar comportamento como lambda, sem perder a ideia do padrão.
@FunctionalInterfacepublic interface ValidadorStrategy { void validar(Pedido pedido);}ValidadorStrategy validaPeso = p -> { if (p.getPesoEmKg() > 50) throw new IllegalArgumentException("Peso excedido");};ValidadorStrategy validaEndereco = p -> { if (p.getEnderecoEntrega() == null) throw new IllegalArgumentException("Endereço obrigatório");};Mesmo com lambdas, mantenha a disciplina: nomeie bem, teste regras críticas e evite lambdas gigantes.