El patrón Strategy permite definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. El cliente elige la estrategia adecuada en tiempo de ejecución sin modificar el código que coordina la operación, lo que facilita extender o reemplazar comportamientos con bajo acoplamiento.
Strategy aplica polimorfismo para sustituir condicionales anidados y mantener abiertas las puertas a nuevas variantes de cálculo. El foco está en parametrizar el comportamiento; la clase que coordina delega el trabajo a un objeto estrategia que encapsula el algoritmo concreto.
En muchos sistemas existen operaciones que se calculan de distintos modos según reglas de negocio, configuraciones o condiciones de tiempo de ejecución. Sin Strategy se suele recurrir a estructuras condicionales complejas (if, switch) que crecen con cada variante agregada, dificultan las pruebas y provocan cambios recurrentes en las clases de alto nivel.
El patrón introduce un nivel de indirección: las clases que utilizan el algoritmo solo conocen la interfaz de la estrategia. Nuevas variantes se implementan como clases adicionales que cumplen el contrato sin alterar el código cliente.
La intención es definir una familia de algoritmos, encapsularlos y hacerlos intercambiables. Strategy es adecuado cuando:
La motivación está en separar el qué (la operación que el cliente necesita) del cómo (el algoritmo que la realiza), habilitando la evolución independiente de ambas partes.
Los elementos clásicos del patrón son:
El contexto puede exponer operaciones para cambiar la estrategia en tiempo de ejecución. Esto habilita modelos flexibles donde la lógica dominante se mantiene estable y solo se sustituyen las políticas de cálculo.
Una decisión de diseño importante es determinar si la estrategia mantiene estado interno:
Strategy no impone una opción; depende de los requisitos y de cómo se gestione la inmutabilidad o la compartición de instancias.
Imaginemos un marketplace que calcula el precio final de un producto combinando descuentos, impuestos y promociones según el canal de venta. Durante un evento masivo se lanza una campaña con estrategias dinámicas que deben ajustarse a distintos segmentos de clientes sin detener el servicio.
El motor de precios necesita experimentar con devaluaciones progresivas, descuentos escalonados y tarifas fijas para vendedores premium. Strategy permite encapsular cada algoritmo en una estrategia distinta, alternarlas en ejecución y agregar nuevas variantes sin modificar el código central del marketplace.
El siguiente ejemplo muestra una familia de estrategias para calcular precios finales y cómo el contexto decide cuál utilizar según la configuración del comercio:
package tutorial.strategy;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
public interface EstrategiaPrecio {
BigDecimal calcularPrecio(PeticionPrecio peticion);
}
class EstrategiaPrecioBase implements EstrategiaPrecio {
private final BigDecimal tasaImpositiva;
EstrategiaPrecioBase(BigDecimal tasaImpositiva) {
this.tasaImpositiva = tasaImpositiva;
}
@Override
public BigDecimal calcularPrecio(PeticionPrecio peticion) {
BigDecimal precio = peticion.precioBase();
BigDecimal impuestos = precio.multiply(tasaImpositiva);
return precio.add(impuestos).setScale(2, RoundingMode.HALF_UP);
}
}
class EstrategiaDescuentoTemporal implements EstrategiaPrecio {
private final EstrategiaPrecio delegada;
private final LocalDate inicio;
private final LocalDate fin;
private final BigDecimal porcentajeDescuento;
EstrategiaDescuentoTemporal(EstrategiaPrecio delegada,
LocalDate inicio,
LocalDate fin,
BigDecimal porcentajeDescuento) {
this.delegada = Objects.requireNonNull(delegada);
this.inicio = inicio;
this.fin = fin;
this.porcentajeDescuento = porcentajeDescuento;
}
@Override
public BigDecimal calcularPrecio(PeticionPrecio peticion) {
BigDecimal precio = delegada.calcularPrecio(peticion);
LocalDate fecha = peticion.fechaOperacion();
if (!fecha.isBefore(inicio) && !fecha.isAfter(fin)) {
BigDecimal descuento = precio.multiply(porcentajeDescuento);
precio = precio.subtract(descuento);
}
return precio.max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
}
}
class EstrategiaComisionVendedor implements EstrategiaPrecio {
private final EstrategiaPrecio delegada;
private final Map<TipoVendedor, BigDecimal> comisiones;
EstrategiaComisionVendedor(EstrategiaPrecio delegada,
Map<TipoVendedor, BigDecimal> comisiones) {
this.delegada = Objects.requireNonNull(delegada);
this.comisiones = new EnumMap<>(comisiones);
}
@Override
public BigDecimal calcularPrecio(PeticionPrecio peticion) {
BigDecimal precio = delegada.calcularPrecio(peticion);
BigDecimal comision = comisiones.getOrDefault(peticion.tipoVendedor(), BigDecimal.ZERO);
BigDecimal recargo = precio.multiply(comision);
return precio.add(recargo).setScale(2, RoundingMode.HALF_UP);
}
}
record PeticionPrecio(BigDecimal precioBase,
TipoVendedor tipoVendedor,
LocalDate fechaOperacion) {
}
enum TipoVendedor {
INDIVIDUAL, PREMIUM, CORPORATIVO
}
class MotorPrecios {
private EstrategiaPrecio estrategiaActual;
MotorPrecios(EstrategiaPrecio estrategiaInicial) {
this.estrategiaActual = Objects.requireNonNull(estrategiaInicial);
}
public BigDecimal calcular(PeticionPrecio peticion) {
return estrategiaActual.calcularPrecio(peticion);
}
public void actualizarEstrategia(EstrategiaPrecio nuevaEstrategia) {
this.estrategiaActual = Objects.requireNonNull(nuevaEstrategia);
}
}
class AplicacionStrategy {
public static void main(String[] args) {
EstrategiaPrecio base = new EstrategiaPrecioBase(new BigDecimal("0.21"));
Map<TipoVendedor, BigDecimal> comisiones = new EnumMap<>(TipoVendedor.class);
comisiones.put(TipoVendedor.INDIVIDUAL, new BigDecimal("0.03"));
comisiones.put(TipoVendedor.PREMIUM, new BigDecimal("0.01"));
EstrategiaPrecio estrategiaEvento = new EstrategiaDescuentoTemporal(
new EstrategiaComisionVendedor(base, comisiones),
LocalDate.now().minusDays(1),
LocalDate.now().plusDays(5),
new BigDecimal("0.15")
);
MotorPrecios motor = new MotorPrecios(estrategiaEvento);
PeticionPrecio pedido = new PeticionPrecio(new BigDecimal("5000"), TipoVendedor.PREMIUM, LocalDate.now());
System.out.println("Precio evento: " + motor.calcular(pedido));
motor.actualizarEstrategia(base);
System.out.println("Precio regular: " + motor.calcular(pedido));
}
}
El contexto MotorPrecios
recibe cualquier implementación de EstrategiaPrecio
. La estrategia evento encapsula descuentos y comisiones componiendo otras estrategias, lo que demuestra cómo se pueden encadenar. Cambiar la estrategia en caliente es tan simple como inyectar una nueva instancia, sin modificar el motor ni los clientes que lo consumen.
Las pruebas del motor se vuelven más simples porque es posible sustituir la estrategia real por dobles de prueba que implementen la misma interfaz.
La documentación oficial de Comparator ejemplifica el patrón: la interfaz define la estrategia de comparación y estructuras como Collections.sort
reciben el algorítmo por parámetro. Otros ejemplos incluyen Executor
para políticas de ejecución y la jerarquía de javax.crypto.Cipher
que encapsula algoritmos criptográficos.
Strategy puede combinarse con lambdas o referencias a métodos para simplificar implementaciones sin estado. En Java moderno, muchas bibliotecas admiten proveer estrategias mediante expresiones lambda, lo que reduce la necesidad de clases anónimas.
Otra extensión habitual es registrar estrategias en un mapa y seleccionarlas por clave, lo que facilita habilitar o deshabilitar algoritmos mediante configuraciones externas o bases de datos.
Un error frecuente es multiplicar el número de estrategias sin un criterio claro, generando una proliferación de clases pequeñas difíciles de mantener. También es riesgoso exponer demasiadas dependencias dentro de la estrategia, lo que incrementa el acoplamiento.
Si el cliente olvida configurar la estrategia adecuada, pueden ocurrir fallos en tiempo de ejecución. Es recomendable proveer valores por defecto seguros o validar la configuración durante la inicialización.
Strategy se relaciona con State, dado que ambos encapsulan comportamientos intercambiables; la diferencia principal es que en State el cambio de estrategia se vincula con transiciones de estado. Se puede combinar con Template Method cuando parte del algoritmo es fija y otra parte se delega a la estrategia. También colabora con Factory Method para crear estrategias según la configuración y con Decorator para agregar responsabilidades transversales a una estrategia existente.