Strategy: trocando algoritmos sem reescrever quem os usa
Como tirar um `if/else` gigante do caminho sem perder a flexibilidade que ele tentava oferecer
Um sistema de pagamentos precisa calcular o valor de frete de formas diferentes dependendo da transportadora escolhida. A implementação mais natural costuma começar assim:
public BigDecimal calcularFrete(Pedido pedido, String transportadora) {
if (transportadora.equals("CORREIOS")) {
return pedido.getPeso().multiply(new BigDecimal("2.50"));
} else if (transportadora.equals("TRANSPORTADORA_X")) {
return pedido.getDistancia().multiply(new BigDecimal("0.80"));
} else if (transportadora.equals("RETIRADA")) {
return BigDecimal.ZERO;
}
throw new IllegalArgumentException("Transportadora desconhecida");
}
Esse método cresce a cada transportadora nova, e misturar a regra de negócio de cada uma dentro do mesmo bloco condicional torna difícil testar uma regra isoladamente das outras. Qualquer mudança na fórmula de uma transportadora arrisca quebrar o if/else inteiro por engano.
Separar o algoritmo de quem o usa
O Strategy propõe extrair cada variação do algoritmo para sua própria classe, todas implementando uma interface comum. Quem precisa calcular o frete não sabe qual fórmula está sendo usada, só invoca o método da interface.
public interface CalculadoraFrete {
BigDecimal calcular(Pedido pedido);
}
public class FreteCorreios implements CalculadoraFrete {
@Override
public BigDecimal calcular(Pedido pedido) {
return pedido.getPeso().multiply(new BigDecimal("2.50"));
}
}
public class FreteTransportadoraX implements CalculadoraFrete {
@Override
public BigDecimal calcular(Pedido pedido) {
return pedido.getDistancia().multiply(new BigDecimal("0.80"));
}
}
public class FreteRetirada implements CalculadoraFrete {
@Override
public BigDecimal calcular(Pedido pedido) {
return BigDecimal.ZERO;
}
}
A classe que precisa calcular o frete recebe a estratégia já escolhida, geralmente por injeção de dependência, e delega o cálculo:
public class PedidoService {
private final CalculadoraFrete calculadoraFrete;
public PedidoService(CalculadoraFrete calculadoraFrete) {
this.calculadoraFrete = calculadoraFrete;
}
public BigDecimal calcularFrete(Pedido pedido) {
return calculadoraFrete.calcular(pedido);
}
}
Testar a fórmula dos Correios isoladamente vira um teste unitário direto em FreteCorreios, sem precisar montar um cenário que passe pelo if/else inteiro. E adicionar uma transportadora nova significa criar uma classe nova, sem editar nenhuma das existentes.
Decidir qual estratégia usar ainda é um problema à parte
O Strategy resolve a execução do algoritmo, mas alguém ainda precisa decidir qual implementação instanciar. É comum combinar Strategy com uma fábrica (o Factory Method de um post anterior por aqui resolve exatamente isso), ou simplesmente registrar as estratégias num mapa:
Map<String, CalculadoraFrete> estrategias = Map.of(
"CORREIOS", new FreteCorreios(),
"TRANSPORTADORA_X", new FreteTransportadoraX(),
"RETIRADA", new FreteRetirada()
);
CalculadoraFrete estrategia = estrategias.get(transportadora);
Isso não elimina a decisão, só a move para um único ponto central, em vez de espalhá-la pela lógica de negócio.
Strategy e enum: uma combinação comum em Java
Quando as estratégias são poucas e fixas, uma alternativa mais enxuta em Java é usar um enum com um método abstrato, aproveitando que cada constante pode ter sua própria implementação:
public enum TipoFrete {
CORREIOS {
@Override
public BigDecimal calcular(Pedido pedido) {
return pedido.getPeso().multiply(new BigDecimal("2.50"));
}
},
RETIRADA {
@Override
public BigDecimal calcular(Pedido pedido) {
return BigDecimal.ZERO;
}
};
public abstract BigDecimal calcular(Pedido pedido);
}
Essa versão só compensa quando o conjunto de estratégias é conhecido e estável, já que adicionar uma nova constante exige recompilar o enum. Para cenários onde novas estratégias podem ser plugadas em tempo de execução (um sistema de plugins, por exemplo), interfaces e classes separadas continuam sendo a opção mais flexível.
Quando vale a pena
Se existe apenas uma forma de fazer algo, Strategy não tem o que resolver. O padrão se justifica quando o mesmo comportamento tem variações reais de regra de negócio, e principalmente quando essas variações mudam em momentos diferentes ou são mantidas por pessoas diferentes. Um if/else com duas opções que nunca mudam pode continuar sendo um if/else; o custo da abstração só compensa quando a lista de variações tende a crescer.
Fonte: Refactoring Guru.
Um grande abraço e até o próximo post!