Category: Behavioral
O problema
Um trecho de comportamento tem várias variantes válidas, e qual delas se aplica depende de
alguma condição em tempo de execução — o modo de transporte, o tipo de transação, a ordem de
classificação. A primeira implementação tentadora é um único método com um grande if/else
ou switch sobre essa condição. Funciona até a terceira ou quarta variante aparecer, ponto em
que o método fica longo, toda mudança arrisca quebrar um branch não relacionado, e adicionar
uma variante nova significa editar código que já funciona em vez de só adicionar código novo do
lado.
A solução
Extrair cada variante atrás de uma interface comum, e dar ao código chamador uma forma de plugar qual implementação se aplica — trocável em tempo de execução, e cada variante é uma classe autocontida que pode ser testada, lida e alterada isoladamente.
classDiagram
class Strategy {
<<interface>>
}
class ConcreteStrategyA
class ConcreteStrategyB
class Context {
-strategy
+setStrategy(s)
+execute()
}
Strategy <|.. ConcreteStrategyA
Strategy <|.. ConcreteStrategyB
Context --> Strategy
Exemplo clássico
classic/RouteStrategy
calcula uma rota entre dois pontos; DrivingRouteStrategy,
WalkingRouteStrategy
e PublicTransportRouteStrategy
cada uma aplica um fator de desvio, velocidade e (pro transporte público) um tempo de espera
fixo diferentes em cima do mesmo cálculo de distância em linha reta.
Navigator é o
contexto: ele guarda uma estratégia e delega a ela, e setStrategy(...) deixa quem chama trocar
o modo de viagem pra mesma jornada sem tocar no próprio Navigator.
NavigatorTest
verifica tanto a matemática de cada estratégia quanto que trocar de estratégia de fato muda o
resultado pro mesmo par origem/destino.
Exemplo aplicado: cálculo de tarifa por tipo de transação
applied/FeeCalculator
busca uma FeeCalculationStrategy
por TransactionType
em vez de ramificar sobre ele: PixFeeStrategy
é gratuita (o BACEN exige PIX gratuito entre pessoas físicas), TedFeeStrategy
cobra uma tarifa fixa independente do valor, e BoletoFeeStrategy
cobra um percentual com um piso mínimo. Esse é precisamente o cenário pro qual o padrão existe:
um gateway de pagamento real adicionando um quarto tipo de transação depois significa
adicionar uma classe de estratégia nova, não reabrir um método de cálculo de tarifa do qual
todo tipo de transação já existente depende.
FeeCalculatorTest
cobre as três estratégias mais o caso de falha de "tipo não registrado".
Quando não usar
- Se hoje realmente só existe uma variante e nenhum plano concreto pra uma segunda, uma interface de estratégia é abstração especulativa — um método simples é mais claro até a segunda variante de fato aparecer.
- Se as variantes compartilham a maior parte da lógica e diferem só em um ou dois passos, Template Method (fixando o esqueleto, sobrescrevendo os passos) costuma ser um encaixe melhor do que Strategy (trocando o algoritmo inteiro).
- Não deixe a classe de contexto acumular lógica de negócio que decide qual estratégia usar com base em regras profundas de domínio — se essa lógica de seleção fica complexa, ela merece sua própria fábrica (ver os módulos Factory Method / Abstract Factory deste repositório quando chegarem).
Cobertura de testes
100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:
./gradlew :behavioral:strategy:jacocoTestReport
Relatório em behavioral/strategy/build/reports/jacoco/test/html/index.html.
Leitura complementar
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — o Capítulo 5 formaliza o Strategy.
- Parnas, D. L. (1972). "On the Criteria to Be Used in Decomposing Systems into Modules." Communications of the ACM, 15(12), 1053–1058. — o argumento fundacional de ocultação de informação de por que uma variante de algoritmo pertence atrás de uma interface estável (uma fronteira de módulo) em vez de dentro de um condicional que todo chamador precisa conhecer.
- Liskov, B. (1987). "Data Abstraction and Hierarchy." OOPSLA '87 Addendum to the Proceedings,
ACM SIGPLAN Notices, 23(5). — a declaração original do que se tornou o Princípio da
Substituição de Liskov: toda estratégia concreta precisa ser substituível por
RouteStrategy/FeeCalculationStrategysem mudar a correção do código que a chama, que é exatamente o contrato do qualNavigatoreFeeCalculatordependem.
Testes unitários
src/test/java/com/designpatterns/behavioral/strategy/classic/NavigatorTest.java
package com.designpatterns.behavioral.strategy.classic;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class NavigatorTest {
private final Location origin = new Location(0, 0);
private final Location destination = new Location(3, 4); // straight-line distance = 5km
@Test
void drivingRouteAppliesTheRoadDetourFactor() {
Navigator navigator = new Navigator(new DrivingRouteStrategy());
Route route = navigator.route(origin, destination);
assertThat(route.distanceKm()).isEqualTo(5 * 1.3);
assertThat(route.description()).isEqualTo("driving");
}
@Test
void swappingTheStrategyChangesTheRouteForTheSameTrip() {
Navigator navigator = new Navigator(new WalkingRouteStrategy());
Route walking = navigator.route(origin, destination);
navigator.setStrategy(new PublicTransportRouteStrategy());
Route transit = navigator.route(origin, destination);
assertThat(walking.distanceKm()).isNotEqualTo(transit.distanceKm());
assertThat(walking.estimatedMinutes()).isNotEqualTo(transit.estimatedMinutes());
}
}
src/test/java/com/designpatterns/behavioral/strategy/applied/FeeCalculatorTest.java
package com.designpatterns.behavioral.strategy.applied;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class FeeCalculatorTest {
private final FeeCalculator calculator = FeeCalculator.withDefaultStrategies();
@Test
void pixTransfersAreFree() {
assertThat(calculator.calculateFeeCents(TransactionType.PIX, 500_00L)).isZero();
}
@Test
void tedChargesAFlatFeeRegardlessOfAmount() {
assertThat(calculator.calculateFeeCents(TransactionType.TED, 100_00L)).isEqualTo(1000L);
assertThat(calculator.calculateFeeCents(TransactionType.TED, 50_000_00L)).isEqualTo(1000L);
}
@Test
void boletoChargesThePercentageFeeAboveTheMinimum() {
assertThat(calculator.calculateFeeCents(TransactionType.BOLETO, 100_000_00L)).isEqualTo(2_000_00L);
}
@Test
void boletoFallsBackToTheMinimumFeeForSmallAmounts() {
assertThat(calculator.calculateFeeCents(TransactionType.BOLETO, 1_00L)).isEqualTo(350L);
}
@Test
void rejectsAnUnregisteredTransactionType() {
FeeCalculator calculatorWithoutBoleto = new FeeCalculator(Map.of(TransactionType.PIX, new PixFeeStrategy()));
assertThatThrownBy(() -> calculatorWithoutBoleto.calculateFeeCents(TransactionType.BOLETO, 100L))
.isInstanceOf(IllegalArgumentException.class);
}
}