Category: Behavioral
El problema
Un fragmento de comportamiento tiene varias variantes válidas, y cuál aplica depende de alguna
condición en tiempo de ejecución — el modo de transporte, el tipo de transacción, el orden de
clasificación. La primera implementación tentadora es un único método con un gran if/else o
switch sobre esa condición. Funciona hasta que aparece la tercera o cuarta variante, momento
en el que el método se vuelve largo, cada cambio arriesga romper una rama no relacionada, y
agregar una variante nueva significa editar código que ya funciona en vez de solo agregar código
nuevo al lado.
La solución
Extraer cada variante detrás de una interfaz común, y darle al código que la llama una forma de conectar la implementación que aplica — intercambiable en tiempo de ejecución, y cada variante es una clase autocontenida que se puede probar, leer y cambiar de forma aislada.
classDiagram
class Strategy {
<<interface>>
}
class ConcreteStrategyA
class ConcreteStrategyB
class Context {
-strategy
+setStrategy(s)
+execute()
}
Strategy <|.. ConcreteStrategyA
Strategy <|.. ConcreteStrategyB
Context --> Strategy
Ejemplo clásico
classic/RouteStrategy
calcula una ruta entre dos puntos; DrivingRouteStrategy,
WalkingRouteStrategy
y PublicTransportRouteStrategy
cada una aplica un factor de desvío, velocidad y (para el transporte público) un tiempo de
espera fijo distintos sobre el mismo cálculo de distancia en línea recta.
Navigator es
el contexto: mantiene una estrategia y le delega, y setStrategy(...) permite que quien llama
cambie el modo de viaje para el mismo trayecto sin tocar el propio Navigator.
NavigatorTest
verifica tanto las matemáticas de cada estrategia como que cambiar de estrategia realmente
cambia el resultado para el mismo par origen/destino.
Ejemplo aplicado: cálculo de tarifa por tipo de transacción
applied/FeeCalculator
busca una FeeCalculationStrategy
por TransactionType
en vez de ramificar sobre él: PixFeeStrategy
es gratuita (BACEN exige PIX gratuito entre personas físicas), TedFeeStrategy
cobra una tarifa fija sin importar el monto, y BoletoFeeStrategy
cobra un porcentaje con un piso mínimo. Este es precisamente el escenario para el que existe el
patrón: un gateway de pagos real que agrega un cuarto tipo de transacción más adelante
significa agregar una clase de estrategia nueva, no reabrir un método de cálculo de tarifa del
que ya depende cada tipo de transacción existente.
FeeCalculatorTest
cubre las tres estrategias más el caso de falla de "tipo no registrado".
Cuándo no usarlo
- Si hoy realmente solo existe una variante y ningún plan concreto para una segunda, una interfaz de estrategia es abstracción especulativa — un método simple es más claro hasta que la segunda variante realmente aparezca.
- Si las variantes comparten la mayor parte de su lógica y difieren solo en uno o dos pasos, Template Method (fijando el esqueleto, sobrescribiendo los pasos) suele ser un mejor encaje que Strategy (intercambiando el algoritmo completo).
- No deje que la clase de contexto acumule lógica de negocio que decida qué estrategia usar según reglas de dominio profundas — si esa lógica de selección se vuelve compleja, merece su propia fábrica (vea los módulos Factory Method / Abstract Factory de este repositorio cuando lleguen).
Cobertura de pruebas
100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúzcalo usted mismo:
./gradlew :behavioral:strategy:jacocoTestReport
Informe en behavioral/strategy/build/reports/jacoco/test/html/index.html.
Lecturas adicionales
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — el Capítulo 5 formaliza Strategy.
- Parnas, D. L. (1972). "On the Criteria to Be Used in Decomposing Systems into Modules." Communications of the ACM, 15(12), 1053–1058. — el argumento fundacional de ocultación de información de por qué una variante de algoritmo pertenece detrás de una interfaz estable (un límite de módulo) en vez de dentro de un condicional que cada llamador tiene que conocer.
- Liskov, B. (1987). "Data Abstraction and Hierarchy." OOPSLA '87 Addendum to the Proceedings,
ACM SIGPLAN Notices, 23(5). — la declaración original de lo que se convirtió en el
Principio de Sustitución de Liskov: toda estrategia concreta debe ser sustituible por
RouteStrategy/FeeCalculationStrategysin cambiar la corrección del código que la llama, que es exactamente el contrato del que dependenNavigatoryFeeCalculator.
Pruebas unitarias
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);
}
}