En el desarrollo de software, es común enfrentarse a situaciones donde múltiples algoritmos resuelven el mismo problema, como diferentes métodos de ordenamiento (burbuja, rápido, inserción) o diversas pasarelas de pago (PayPal, tarjeta de crédito, transferencia bancaria). Mezclar estas implementaciones directamente en el código genera un sistema rígido y difícil de mantener. El patrón de estrategia (Strategy Pattern) ofrece una solución elegante al encapsular cada algoritmo en clases separadas e intercambiables, mejorando la claridad, mantenibilidad y escalabilidad del código. Este artículo explora en profundidad el patrón de estrategia en Java, desde su estructura hasta ejemplos prácticos.
1. Concepto del Patrón de Estrategia
El patrón de estrategia es un patrón de diseño de comportamiento que define una familia de algoritmos, encapsula cada uno de ellos en una clase independiente y los hace intercambiables. Esto permite que el contexto (el cliente que utiliza los algoritmos) pueda seleccionar dinámicamente la estrategia adecuada en tiempo de ejecución sin modificar su propio código. La esencia del patrón es separar la lógica del algoritmo de su uso, aumentando la flexibilidad y facilitando la incorporación de nuevas variantes.
2. Estructura del Patrón de Estrategia
La estructura típica del patrón de estrategia incluye tres componentes principales:
- Contexto (Context): Mantiene una referencia a un objeto de la interfaz de estrategia. Delega la ejecución del algoritmo al objeto de estrategia actual.
- Interfaz de Estrategia (Strategy): Declara un método común que todas las estrategias concretas deben implementar.
- Estrategias Concretas (ConcreteStrategy): Implementan la interfaz de estrategia con lógica algorítmica específica.
3. Ejemplo en Código Java
Consideremos un sistema de descuentos promocionales para una tienda en línea, donde pueden aplicarse diferentes tipos de descuento: monto fijo, porcentaje y reducción por umbral de compra.
3.1. Definición de la Interfaz de Estrategia
public interface DescuentoStrategy {
double calcularDescuento(double precioOriginal);
}
3.2. Implementación de Estrategias Concretas
DescuentoMontoFijo
public class DescuentoMontoFijo implements DescuentoStrategy {
private double montoDescuento;
public DescuentoMontoFijo(double montoDescuento) {
this.montoDescuento = montoDescuento;
}
@Override
public double calcularDescuento(double precioOriginal) {
return Math.max(0, precioOriginal - montoDescuento);
}
}
DescuentoPorcentual
public class DescuentoPorcentual implements DescuentoStrategy {
private double porcentajeDescuento;
public DescuentoPorcentual(double porcentajeDescuento) {
this.porcentajeDescuento = porcentajeDescuento;
}
@Override
public double calcularDescuento(double precioOriginal) {
return precioOriginal * (1 - porcentajeDescuento);
}
}
DescuentoUmbral
public class DescuentoUmbral implements DescuentoStrategy {
private double umbral; // monto mínimo para aplicar descuento
private double reduccion; // monto a reducir si se supera el umbral
public DescuentoUmbral(double umbral, double reduccion) {
this.umbral = umbral;
this.reduccion = reduccion;
}
@Override
public double calcularDescuento(double precioOriginal) {
if (precioOriginal >= umbral) {
return precioOriginal - reduccion;
}
return precioOriginal;
}
}
3.3. Contexto: CarritoDeCompras
public class CarritoDeCompras {
private DescuentoStrategy estrategiaDescuento;
public CarritoDeCompras(DescuentoStrategy estrategiaDescuento) {
this.estrategiaDescuento = estrategiaDescuento;
}
public void setEstrategiaDescuento(DescuentoStrategy estrategiaDescuento) {
this.estrategiaDescuento = estrategiaDescuento;
}
public double calcularPrecioFinal(double precioOriginal) {
return estrategiaDescuento.calcularDescuento(precioOriginal);
}
}
3.4. Código de Prueba
public class PruebaPatronEstrategia {
public static void main(String[] args) {
double precioOriginal = 200;
CarritoDeCompras carrito1 = new CarritoDeCompras(new DescuentoMontoFijo(50));
System.out.println("Precio con descuento fijo de $50: " + carrito1.calcularPrecioFinal(precioOriginal));
CarritoDeCompras carrito2 = new CarritoDeCompras(new DescuentoPorcentual(0.2));
System.out.println("Precio con 20% de descuento: " + carrito2.calcularPrecioFinal(precioOriginal));
CarritoDeCompras carrito3 = new CarritoDeCompras(new DescuentoUmbral(150, 30));
System.out.println("Precio con descuento de $30 si supera $150: " + carrito3.calcularPrecioFinal(precioOriginal));
}
}
En este ejemplo, la interfaz DescuentoStrategy define el contrato. Cada clase concreta implementa la lógica específica de descuento. El contexto CarritoDeCompras delega el cálculo a la estrategia activa, permitiendo cambiar dinámicamente la política de descuento sin alterar el carrito.
4. Ventajas y Desventajas
4.1. Ventajas
- Intercambiabilidad dinámica: Las estrategias pueden reemplazarse en tiempo de ejecución sin afectar al contexto.
- Alta extensibilidad: Agregar una nueva estrategia solo requiere implementar la interfaz, sin modificar código existente (principio abierto/cerrado).
- Reutilización de código: Las estrategias son clases independientes que pueden compartirse entre distintos contextos.
- Separación de responsabilidades: Cada estrategia se enfoca en un único algoritmo, mejorando la legibilidad y el mantenimiento.
4.2. Desventajas
- Incremento de clases: Cada nueva estratgeia añade una clase, lo que puede aumentar la complejidad del proyecto.
- Conocimiento del cliente: El contexto o el usuario final debe conocer las estrategias disponibles para seleccionar la adecuada.
5. Escenarios de Aplicación
- Algoritmos de ordenamiento y búsqueda: Permitir elegir entre burbuja, quicksort, mergesort, etc., según el tamaño o naturaleza de los datos.
- Sistemas de pago: Integrar múltiples pasarelas (PayPal, tarjeta, criptomonedas) como estrategias intercambiables.
- Comportamiento de personajes en juegos: Definir estrategias de ataque, defensa o movimiento que puedan cambiarse durante el juego.
- Compresión de datos: Seleccionar entre ZIP, GZIP o RAR según la necesidad de velocidad o tasa de compresión.