Padrão Strategy em Java OOP: comportamento intercambiável e eliminação de condicionais

Capítulo 15

Tempo estimado de leitura: 8 minutos

+ Exercício

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/else baseados 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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

PontoRecomendação prática
Contrato da estratégiaMantenha 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égiaCentralize a escolha (factory/registry). O contexto deve delegar, não decidir.
Estratégia padrãoUse uma estratégia “nula” (ex.: SemDescontoStrategy) para evitar null e condicionais.
Explosão de classesSe 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.

Agora responda o exercício sobre o conteúdo:

Em um sistema de checkout que precisa calcular frete com regras diferentes (normal, expresso, retirada), qual abordagem melhor aplica o padrão Strategy para reduzir condicionais e permitir troca de comportamento em tempo de execução?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Strategy encapsula o algoritmo em objetos intercambiáveis. O contexto (CheckoutService) recebe uma FreteStrategy e apenas delega, enquanto a seleção fica centralizada (factory/registry), evitando crescimento de if/else/switch no código principal.

Próximo capitúlo

Padrão Singleton em Java OOP: uso criterioso, implementação e alternativas

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
88%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.