Encapsulando algoritmos em Java — do if/else à extensão sem atrito
"Soluções reutilizáveis e nomeadas para problemas que aparecem repetidamente no design de sistemas orientados a objetos."
Não é código pronto — é uma receita pra resolver um tipo de problema.
Pra fixar a ideia, três analogias do mundo real. Cada uma mostra uma propriedade diferente do que é um padrão — e no final, tudo volta pra uma ideia só: estrutura nomeada, reutilizada, adaptada ao contexto.
→ próximos três slides
Estilo de jogo onde os jogadores estão constantemente se movendo e tocando a bola entre si.
Em vez de listar 11 posições e uma filosofia de jogo, o treinador fala:
Opção A — lance-a-lance
Opção B — o nome
Consagrados em 1994 pelo livro Design Patterns dos quatro autores conhecidos como Gang of Four (GoF): Gamma, Helm, Johnson e Vlissides. Os 23 padrões clássicos se dividem em três grandes famílias segundo o tipo de problema que resolvem.
Strategy é comportamental — vamos focar nele hoje.
import "Strategy" de lugar nenhum. Padrão é código que você escreve — não dependência que você adiciona.Em qualquer projeto real, você passa mais tempo lendo código do que escrevendo.
Padrão é, antes de tudo, uma decisão social dentro de um time de engenharia.
Spring muda, Quarkus muda, o próprio Java muda. Mas o conceito de "encapsular algoritmo em uma classe que implementa uma interface comum" é verdade em 1994, em 2026, e vai ser em 2040.
"Define uma família de algoritmos, encapsula cada um e os torna intercambiáveis. Strategy permite que o algoritmo varie independentemente dos clientes que o usam."
Gamma, Helm, Johnson & Vlissides · Design Patterns (1994)
Em linguagem humana: quando você tem várias formas de fazer a mesma coisa, cada forma vira uma classe separada, e o código que usa escolhe qual usar sem saber os detalhes.
Comportamental = foca em como objetos colaboram, não em como são criados ou compostos.
public class CalculadoraFrete { public double calcular(String transp, double pesoKg, Endereco dest) { if (transp.equals("CORREIOS")) { correios.connect(); correios.authenticate(config.getCorreiosKey()); Cotacao c = correios.cotar(pesoKg, dest.getCep()); correios.disconnect(); metrics.increment("frete.correios"); return c.getValor(); } else if (transp.equals("JADLOG")) { JadlogResponse r = jadlog.cotar(pesoKg, dest); if (r.isFailed()) retry(transp, pesoKg, dest); return r.getValor(); } else if (transp.equals("LOGGI")) { String token = config.getLoggiToken(); if (token == null) throw new IllegalStateException("sem token Loggi"); return loggi.calcular(token, pesoKg, dest); } else if (transp.equals("MERCADO_ENVIOS")) { Long sellerId = user.getMercadoLivreId(); return mercadoEnvios.cotar(sellerId, pesoKg, dest); } else if (transp.equals("RETIRADA")) { // adicionado na sprint passada return 0.0; } else if (transp.equals("DRONE")) { // adicionado ontem — o time comercial pediu return drone.calcular(dest.getCoords(), pesoKg); } throw new IllegalArgumentException("Transportadora: " + transp); } }
sellerId em Long. Cada nova transportadora traz um vocabulário próprio.if/else hell nasce: uma transportadora por vez."correios" minúsculo, boom em produção. Sem teste unitário dá pra perceber?"correios" em minúsculo quebra silenciosamente — o compilador não avisa.else no lugar errado, um refactor mal pensado, e você quebra uma funcionalidade que estava funcionando. No mundo real: bug em produção, ticket aberto, rollback de madrugada.CalculadoraFrete inteira — com todas as dependências.class CalculadoraFrete { calcular(tipo, peso) { if (tipo == "CORREIOS") ... if (tipo == "JADLOG") ... if (tipo == "RETIRADA") ... // (15 ifs depois) } }
class Carrinho { FreteStrategy frete; } interface FreteStrategy { calcular(peso); } class FreteCorreios implements FreteStrategy { ... } class FreteJadlog implements FreteStrategy { ... } class FreteRetirada implements FreteStrategy { ... }
Carrinho.new FreteJadlog().calcular(2.0). Sem montar sistema inteiro.Carrinho não conhece as implementações — só a interface.Fluxo: Cliente escolhe → injeta no Context → Context chama strategy.execute() sem saber qual é → pra trocar em runtime, passa outra instância.
Strategy é a aplicação prática de um dos princípios mais clássicos do GoF. Herança amarra o comportamento à identidade da classe em tempo de compilação. Composição pluga comportamento em runtime.
class CarrinhoCorreios extends CalculadoraCorreios { // preso a Correios // em tempo de compilação } class CarrinhoJadlog extends CalculadoraJadlog { ... }
class Carrinho { FreteStrategy frete; // tem-um FreteStrategy // plugado em runtime } carrinho.setFrete(new FreteLoggi());
Carrinho, N estratégias plugáveis. Trocar em runtime é setFrete(...).Herança amarra. Composição liberta.
Muita gente aprende polimorfismo em POO e depois aprende Strategy, e pensa "mas isso é só polimorfismo". É — mas com uma intenção nomeada.
O valor do padrão é comunicar: quando você diz FreteStrategy, qualquer leitor sabe imediatamente por que o polimorfismo está sendo usado ali.
public interface FreteStrategy { double calcular(double pesoKg); }
public class FreteCorreios implements FreteStrategy { @Override public double calcular( double pesoKg) { return pesoKg * 2.5 + 10.0; } }
public class FreteJadlog implements FreteStrategy { @Override public double calcular( double pesoKg) { return pesoKg * 3.0 + 5.0; } }
public class FreteRetirada implements FreteStrategy { @Override public double calcular( double pesoKg) { return 0.0; } }
Cada classe cuida apenas da sua regra. O tipo é a classe — sem strings mágicas. E cada uma é testável isolada: new FreteJadlog().calcular(2.0) → 11.0.
Carrinhoimport java.util.Objects; public class Carrinho { private FreteStrategy frete; public Carrinho(FreteStrategy frete) { this.frete = Objects.requireNonNull(frete, "Estratégia de frete é obrigatória"); } public void setFrete(FreteStrategy frete) { this.frete = Objects.requireNonNull(frete, "Estratégia de frete é obrigatória"); } public double totalComFrete(double subtotal, double pesoKg) { return subtotal + frete.calcular(pesoKg); } }
Carrinho só conhece a interface — nunca as classes concretas.setFrete permite trocar em runtime.requireNonNull?totalComFrete() num carrinho sem estratégia derruba a app com NPE de stack inútil. Com ele, falha rápido, com mensagem clara, no momento da injeção. O Carrinho não existe sem frete.Em vez de rejeitar null, você pode criar uma estratégia neutra que representa "nenhum frete". O sistema nunca quebra por ausência de estratégia.
public class FreteIndefinido implements FreteStrategy { @Override public double calcular(double pesoKg) { return 0.0; // ou lançar exceção de negócio descritiva } } // No Carrinho, construtor sem args vira válido: public Carrinho() { this.frete = new FreteIndefinido(); }
Em frete, a regra exige escolha explícita → fail-fast. Em logger que pode estar desabilitado → Null Object.
public class App { public static void main(String[] args) { Carrinho carrinho = new Carrinho(new FreteCorreios()); System.out.println(carrinho.totalComFrete(100.0, 2.0)); carrinho.setFrete(new FreteJadlog()); System.out.println(carrinho.totalComFrete(100.0, 2.0)); carrinho.setFrete(new FreteRetirada()); System.out.println(carrinho.totalComFrete(100.0, 2.0)); } }
Adicionar Loggi amanhã? FreteLoggi implements FreteStrategy. Zero alteração no Carrinho — OCP na prática.
// FreteStrategy tem 1 método só // → é uma functional interface FreteStrategy freteGratis = peso -> 0.0; FreteStrategy fretePromocional = peso -> peso * 1.0; carrinho.setFrete(freteGratis);
Padrão não envelhece — a linguagem evolui pra expressá-lo melhor.
Estratégias pequenas, de escopo local, podem virar uma linha. O padrão continua lá — só mudou a sintaxe. Nasceu em 1994, Java 8 de 2014 expressa melhor.
new FreteJadlog().calcular(2.0), sem montar sistema.if/else.
Dica de ouro
"Se você se pegar escrevendo switch ou vários if/else baseados em tipos de comportamento — é hora de considerar Strategy."
if/else simples é mais honesto que interface + 2 classes.Use padrões pra problemas que doem. Código simples que nunca cresce é ok ser simples.
double calcular(String tipo, double peso) { if (tipo.equals("CORREIOS")) return peso*2.5+10; if (tipo.equals("JADLOG")) return peso*3.0+5; if (tipo.equals("RETIRADA")) return 0; }
void processar(Pedido p) { if (p.estado == PENDENTE) aguardar(p); if (p.estado == PAGO) separar(p); if (p.estado == ENVIADO) entregar(p); }
void salvar(Usuario u) { if (u.nome == null) throw new Erro(); if (u.email == null) throw new Erro(); if (u.idade < 18) throw new Erro(); repo.save(u); }
void onPedidoCriado(Pedido p) { email.enviar(p); estoque.reservar(p); analytics.track(p); auditLog.registrar(p); }
FreteStrategy. B é State (ciclo de vida), C é validação em cadeia (guard clauses bastam), D pede Observer.R$ 100,00 ou $ 100.00. Duas moedas, praticamente a mesma lógica — trocar vírgula por ponto.if/else basta), D é abstração vazia com variação trivial.Sobre Strategy, sobre a demo de frete, sobre o quiz — ou sobre qualquer coisa que ficou na cabeça durante a apresentação.
Três recursos que valem muito pra levar o padrão Strategy além do que coube nesses 20 minutos:
if/else pra uma solução com Strategy.