Los gastos de envío de PideYa se calculan de tres maneras: por distancia en los mercados maduros, tarifa plana donde estamos entrando, y gratis cuando aplica una promoción. Y la CentralReparto dejó una deuda parecida: el criterio de asignación de repartidor (¿el más cercano?, ¿el menos cargado?, ¿por turnos?) estaba cableado dentro del mediador. Dos síntomas del mismo mal: algoritmos alternativos enterrados en condicionales dentro de quien los usa. Strategy es la respuesta canónica — y seguramente el patrón de comportamiento que más veces aplicarás en tu carrera: una familia de algoritmos, cada uno en su clase, intercambiables tras una interfaz común, elegidos desde fuera. Si el curso entero tuviera que resumirse en un patrón, sería este: es "programa contra interfaces" y OCP en estado puro.
Contenido
- El problema en PideYa: algoritmos enterrados en condicionales
- Intención y estructura del patrón
- Implementación Java completa: gastos de envío
- Segundo caso: asignación de repartidor
- Seleccionar la estrategia: config, contexto y registro
- Strategy con lambdas y
Function - Strategy vs. State vs. Bridge: la tabla
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- Ejercicios y conclusión
El problema en PideYa: algoritmos enterrados en condicionales
El cálculo de envío, versión actual, dentro de la FachadaCheckout:
private BigDecimal calcularEnvio(Pedido pedido) {
if (promocionEnvioGratisActiva && pedido.getTotal().compareTo(MINIMO_PROMO) >= 0) {
return BigDecimal.ZERO;
} else if (mercado.equals("MX")) { // mercado nuevo: plana
return TARIFA_PLANA_MX;
} else {
double km = geo.distancia(pedido.getRestaurante(), pedido.getDireccionEntrega());
return BASE.add(POR_KM.multiply(BigDecimal.valueOf(km)));
}
}Los dolores, ya familiares pero con matiz propio:
- Cada política nueva modifica el checkout (adiós OCP): la de "tarifa reducida en hora valle" que pide negocio significa otro
else ifen una clase que no va de envíos. - Las políticas no se pueden probar aisladas: testear la fórmula por distancia arrastra la fachada entera con sus cinco dependencias.
- No se pueden combinar ni configurar: España quiere distancia, México plana, y marketing quiere activar "gratis" por campaña y por ciudad — con
ifs, cada combinación es más ramas. - El algoritmo no es un concepto del código: "política de envío" existe en las conversaciones de negocio pero no hay ninguna clase que se llame así. Señal clásica de abstracción ausente.
La jugada, la de todo el módulo: reificar. Que cada forma de calcular sea un objeto con interfaz común, y que el checkout dependa solo de la interfaz.
Intención y estructura del patrón
Intención (GoF): definir una familia de algoritmos, encapsular cada uno, y hacerlos intercambiables. Strategy permite que el algoritmo varíe independientemente de los clientes que lo usan.
classDiagram
class CalculoEnvio {
<<interface>>
+calcular(pedido: Pedido) BigDecimal
}
class EnvioPorDistancia {
-geo: ServicioGeo
+calcular(pedido) BigDecimal
}
class EnvioTarifaPlana {
-tarifa: BigDecimal
+calcular(pedido) BigDecimal
}
class EnvioGratisPromocion {
+calcular(pedido) BigDecimal
}
class FachadaCheckout {
-calculoEnvio: CalculoEnvio
+confirmarPedido(...)
}
CalculoEnvio <|.. EnvioPorDistancia
CalculoEnvio <|.. EnvioTarifaPlana
CalculoEnvio <|.. EnvioGratisPromocion
FachadaCheckout o-- CalculoEnvio : usa la inyectada
| Rol GoF | En PideYa |
|---|---|
| Strategy (interfaz del algoritmo) | CalculoEnvio |
| ConcreteStrategy | EnvioPorDistancia, EnvioTarifaPlana, EnvioGratisPromocion |
| Context (usa una estrategia a través de la interfaz) | FachadaCheckout |
Estructuralmente es el patrón más simple del módulo — interfaz, implementaciones, campo. Su profundidad está en las decisiones de contorno: qué firma dar a la interfaz, quién elige la estrategia y cuándo. A eso vamos.
Implementación Java completa: gastos de envío
La interfaz. La decisión clave es la firma: debe dar a todas las estrategias lo que necesitan, sin acoplarlas a lo que no. Pedido como parámetro es buen término medio: la de distancia leerá direcciones, la plana lo ignorará casi todo:
Las estrategias. Cada una con sus dependencias propias, inyectadas — la de distancia necesita el servicio geográfico; las demás ni saben que existe:
public class EnvioPorDistancia implements CalculoEnvio {
private final ServicioGeo geo;
private final BigDecimal base;
private final BigDecimal porKm;
public EnvioPorDistancia(ServicioGeo geo, BigDecimal base, BigDecimal porKm) {
this.geo = geo;
this.base = base;
this.porKm = porKm;
}
@Override
public BigDecimal calcular(Pedido pedido) {
double km = geo.distancia(pedido.getRestaurante().getDireccion(),
pedido.getDireccionEntrega());
return base.add(porKm.multiply(BigDecimal.valueOf(km)))
.setScale(2, RoundingMode.HALF_UP);
}
}
public class EnvioTarifaPlana implements CalculoEnvio {
private final BigDecimal tarifa;
public EnvioTarifaPlana(BigDecimal tarifa) { this.tarifa = tarifa; }
@Override
public BigDecimal calcular(Pedido pedido) { return tarifa; }
}
public class EnvioGratisPromocion implements CalculoEnvio {
@Override
public BigDecimal calcular(Pedido pedido) { return BigDecimal.ZERO; }
}El contexto, que ya solo conoce la interfaz:
public class FachadaCheckout {
private final CalculoEnvio calculoEnvio; // inyectada (DIP)
// ... resto de colaboradores del módulo 3 ...
public ConfirmacionPedido confirmarPedido(Carrito carrito, ...) {
// validar (la cadena de 04-02) → impuestos → ...
BigDecimal envio = calculoEnvio.calcular(pedidoEnCurso);
// ... cobrar, construir con Pedido.Builder, notificar ...
}
}Cada estrategia se testea sola en tres líneas; la de "hora valle" será una clase nueva sin tocar nada; y "política de envío" ya es un concepto con nombre en el código.
Segundo caso: asignación de repartidor
La deuda del Mediator, saldada. Nótese que aquí la firma natural recibe candidatos y pedido — la estrategia decide, la central ejecuta:
public interface EstrategiaAsignacion {
Optional<Repartidor> elegir(List<Repartidor> disponibles, Pedido pedido);
}
public class RepartidorMasCercano implements EstrategiaAsignacion {
private final ServicioGeo geo;
public RepartidorMasCercano(ServicioGeo geo) { this.geo = geo; }
@Override
public Optional<Repartidor> elegir(List<Repartidor> disponibles, Pedido pedido) {
return disponibles.stream()
.min(Comparator.comparingDouble(
r -> geo.distancia(r.getPosicion(), pedido.getRestaurante().getDireccion())));
}
}
public class RepartidorMenosCargado implements EstrategiaAsignacion {
@Override
public Optional<Repartidor> elegir(List<Repartidor> disponibles, Pedido pedido) {
return disponibles.stream()
.min(Comparator.comparingInt(Repartidor::getEntregasHoy));
}
}
public class AsignacionRoundRobin implements EstrategiaAsignacion {
private final AtomicInteger turno = new AtomicInteger(); // ¡estrategia CON estado!
@Override
public Optional<Repartidor> elegir(List<Repartidor> disponibles, Pedido pedido) {
if (disponibles.isEmpty()) return Optional.empty();
return Optional.of(disponibles.get(turno.getAndIncrement() % disponibles.size()));
}
}Y la CentralReparto sustituye su buscarDisponible por estrategia.elegir(candidatos, pedido) — el mediador orquesta, la estrategia decide, tal como prometimos en su lección. El round-robin ilustra un matiz importante: las estrategias pueden tener estado propio (el turno); cuando lo tienen, ojo con compartir la instancia entre contextos que no deban compartir ese estado.
Seleccionar la estrategia: config, contexto y registro
¿Quién elige la estrategia? Es la pregunta del patrón, con tres respuestas escalonadas:
1. Por configuración (composición root). La más común: al arrancar, según mercado — recuerda la Abstract Factory: la familia por mercado puede fabricar también la estrategia de envío, junto a la pasarela y los impuestos:
CalculoEnvio envio = switch (ConfiguracionPideYa.getInstancia().getMercado()) {
case "ES" -> new EnvioPorDistancia(geo, new BigDecimal("1.50"), new BigDecimal("0.80"));
case "MX" -> new EnvioTarifaPlana(new BigDecimal("35.00"));
default -> throw new IllegalStateException();
};(Sí, hay un switch — pero uno solo, en el borde de la aplicación, no repetido en cada punto de uso. Ese es el trato.)
2. Por contexto de la petición, en ejecución. Si con promoción activa el envío es gratis, alguien decide por pedido. Ese "alguien" puede ser un pequeño selector — que es, a su vez, una estrategia de orden superior:
public class SelectorEnvio implements CalculoEnvio {
private final ServicioPromociones promos;
private final CalculoEnvio normal;
@Override
public BigDecimal calcular(Pedido pedido) {
return promos.tieneEnvioGratis(pedido)
? BigDecimal.ZERO
: normal.calcular(pedido);
}
}3. Por registro, estilo RegistroNotificadores. El mismo movimiento del Factory Method con lambdas: un mapa nombre → estrategia, y la elección se vuelve dato (configurable en caliente, ampliable sin tocar el selector):
public class RegistroEstrategiasEnvio {
private final Map<String, CalculoEnvio> estrategias = new HashMap<>();
public void registrar(String nombre, CalculoEnvio estrategia) {
estrategias.put(nombre, estrategia);
}
public CalculoEnvio obtener(String nombre) {
CalculoEnvio e = estrategias.get(nombre);
if (e == null) throw new IllegalArgumentException("Política desconocida: " + nombre);
return e;
}
}
// arranque:
registro.registrar("distancia", new EnvioPorDistancia(geo, base, porKm));
registro.registrar("plana", new EnvioTarifaPlana(tarifa));
registro.registrar("gratis", new EnvioGratisPromocion());
// uso: la política del restaurante es un String en su ficha
CalculoEnvio envio = registro.obtener(restaurante.getPoliticaEnvio());Strategy con lambdas y Function
Con un solo método en la interfaz, Strategy es interfaz funcional — y las estrategias sin dependencias ni estado caben en lambdas:
@FunctionalInterface
public interface CalculoEnvio {
BigDecimal calcular(Pedido pedido);
}
CalculoEnvio plana = pedido -> new BigDecimal("35.00");
CalculoEnvio gratis = pedido -> BigDecimal.ZERO;De hecho, llevas años usando Strategy con lambdas: Comparator es una estrategia de ordenación (lista.sort(comparing(Plato::getPrecio))), los Predicate de filter(...) son estrategias de selección, y Function/BiFunction sirven de interfaz Strategy genérica sin declarar nada (Function<Pedido, BigDecimal>). El criterio, idéntico al de Command: lambda para algoritmos pequeños sin dependencias; clase cuando la estrategia tiene colaboradores inyectados, estado o merece tests propios. EnvioPorDistancia (con ServicioGeo y redondeos) es clase; la tarifa plana vive feliz en una lambda. Un matiz a favor de la interfaz propia frente a Function pelada: CalculoEnvio dice qué es en las firmas y permite evolucionar (un método descripcion() para el desglose del ticket) sin romper nada.
Strategy vs. State vs. Bridge: la tabla
Los dos careos que la lección debía saldar, juntos — porque el diagrama de los tres es casi idéntico (contexto/abstracción que delega en una interfaz) y la diferencia es de intención:
| Criterio | Strategy | State | Bridge |
|---|---|---|---|
| Qué encapsula | Un algoritmo completo | El comportamiento de una etapa del ciclo de vida | La implementación de una abstracción (un "cómo" técnico) |
| Quién cambia el objeto | El cliente/configuración, desde fuera | Los propios estados, al transitar | Nadie normalmente: se fija al construir |
| ¿Las implementaciones se conocen entre sí? | No: cada estrategia ignora a sus hermanas | Sí: cada estado sabe a cuáles se transita | No |
| Cardinalidad típica | Un contexto, un algoritmo a la vez | Un contexto, un estado a la vez, cambiando en secuencia | Dos jerarquías completas evolucionando aparte |
| Pregunta que responde | ¿Cómo hago este cálculo? | ¿Qué puedo hacer ahora mismo? | ¿Sobre qué mecanismo se apoya esta familia? |
| En PideYa | CalculoEnvio, EstrategiaAsignacion |
EstadoPedido |
Notificacion × CanalEnvio |
La prueba rápida cuando dudes entre Strategy y State: pregúntate quién y por qué cambia el objeto delegado. ¿Lo elige el exterior según preferencia/configuración y las variantes no se conocen? Strategy. ¿Cambia solo, como consecuencia de las operaciones, siguiendo un grafo de transiciones? State.
Cuándo usarlo y cuándo no
Úsalo cuando:
- Existen (o se prevén de forma concreta) varias formas de hacer lo mismo y la elección varía por configuración, mercado, cliente o contexto.
- Un condicional sobre "el modo de cálculo" se repite o crece dentro de clases que no deberían conocer esos detalles.
- Quieres testear algoritmos aislados de sus contextos, o esconder al contexto datos/dependencias que solo el algoritmo necesita.
Evítalo cuando:
- Solo hay un algoritmo y las "variantes futuras" son especulación: interfaz + una implementación + inyección para nada es sobreingeniería de manual (lección 01-06). Extrae la estrategia cuando llegue la segunda variante real.
- Las variantes difieren en un paso pequeño de un flujo común, no en el algoritmo completo: ahí encaja mejor el patrón de la próxima lección.
- La variación cabe en un parámetro (la tarifa plana de México vs. Colombia es un
BigDecimal, no una clase nueva).
Relación con otros patrones
- Template Method (siguiente lección): el mismo problema —variar partes de un algoritmo— resuelto con herencia en vez de composición; careo completo allí.
- State y Bridge: la tabla de arriba.
- Command: Command reifica que se pidió algo (con undo, cola, auditoría); Strategy, cómo se hace algo. Careo en la comparativa.
- Abstract Factory: fabrica la estrategia coherente con cada familia/mercado.
- Decorator: el
SelectorEnvioque envuelve otra estrategia añade una decisión sin cambiar la interfaz — mecánica de decorador sobre estrategias. - Flyweight: estrategias sin estado, compartidas como instancias únicas.
Errores comunes
- El contexto que pregunta
instanceofa su estrategia ("si es la de distancia, entonces también..."): rompe el contrato entero; si el contexto necesita saber cuál es, la interfaz está mal diseñada (falta un método que exprese esa variación). - Firmas anémicas que van creciendo:
calcular(BigDecimal total)se queda corta cuando llega la estrategia por distancia, y cada ampliación rompe todas las implementaciones. Pasa el objeto de dominio (Pedido) o un parámetro-objeto desde el principio. - Estrategias que mutan el contexto o el pedido: una estrategia de cálculo con efectos colaterales (marcar el pedido, escribir en BD) es una mina; mantenlas puras siempre que el dominio lo permita.
- Compartir estrategias con estado entre hilos o contextos sin pensarlo: el round-robin con su contador es compartido a propósito; hazlo thread-safe (nuestro
AtomicInteger) o no lo compartas. - Un enum-switch disfrazado:
switch (tipoEstrategia)dentro del contexto para llamar a métodos distintos no es Strategy, es lo que Strategy vino a eliminar. El contexto no elige entre ramas: recibe el objeto ya elegido.
Ejercicios
Ejercicio 1: estrategia de hora valle
Implementa EnvioHoraValle: fuera de las franjas punta (13–15 h y 20–22 h), aplica el 50% del cálculo de otra estrategia (la normal del mercado); en hora punta delega sin descuento. Pista: se construye envolviendo otra CalculoEnvio. ¿Qué patrón estructural estás reutilizando?
Ejercicio 2: estrategia como lambda... ¿o no?
Para cada política, decide lambda o clase, y por qué: (a) envío gratis; (b) asignación por menor distancia (usa ServicioGeo); (c) "el repartidor con mejor valoración media, desempatando por menos entregas hoy"; (d) ordenar los platos del ticket alfabéticamente.
Ejercicio 3: careo State/Strategy en código ajeno
Un compañero modela los métodos de pago así: pedido.setComportamientoPago(new PagoTarjeta()), y dentro de PagoTarjeta.procesar() hay un pedido.setComportamientoPago(new PagoCompletado()). ¿Es Strategy o State? ¿Qué delata la respuesta, y qué le recomendarías?
Soluciones
Solución 1:
public class EnvioHoraValle implements CalculoEnvio {
private static final List<LocalTime[]> PUNTA = List.of(
new LocalTime[]{LocalTime.of(13, 0), LocalTime.of(15, 0)},
new LocalTime[]{LocalTime.of(20, 0), LocalTime.of(22, 0)});
private final CalculoEnvio base;
private final Supplier<LocalTime> reloj; // inyectado para poder testear horas
public EnvioHoraValle(CalculoEnvio base, Supplier<LocalTime> reloj) {
this.base = base;
this.reloj = reloj;
}
@Override
public BigDecimal calcular(Pedido pedido) {
BigDecimal normal = base.calcular(pedido);
return esHoraPunta(reloj.get())
? normal
: normal.multiply(new BigDecimal("0.5")).setScale(2, RoundingMode.HALF_UP);
}
private boolean esHoraPunta(LocalTime ahora) {
return PUNTA.stream().anyMatch(f -> !ahora.isBefore(f[0]) && ahora.isBefore(f[1]));
}
}El patrón reutilizado es Decorator: misma interfaz, envuelve una CalculoEnvio y modifica su resultado — los patrones se componen entre familias con total naturalidad. (Y fíjate en el reloj inyectado: sin él, los tests dependerían de la hora a la que los ejecutes.)
Solución 2: (a) lambda (pedido -> BigDecimal.ZERO): sin estado ni dependencias; (b) clase: dependencia inyectada (ServicioGeo) y merece tests propios; (c) clase (o al menos un Comparator con nombre construido con comparing(...).thenComparing(...)): la regla de negocio compuesta merece nombre y tests; (d) lambda/método referencia (comparing(LineaPedido::getDescripcion)): es un Comparator trivial — que, recordemos, ya es Strategy de serie en el JDK.
Solución 3: es State disfrazado de Strategy: el objeto delegado se sustituye a sí mismo desde dentro (PagoTarjeta transita a PagoCompletado), que es exactamente la firma de State — las "estrategias" se conocen entre sí y forman un grafo de transiciones. Lo delata la pregunta de la tabla: no lo elige el exterior; cambia como consecuencia de las operaciones. Recomendación: renombrarlo y diseñarlo como máquina de estados explícita (interfaz EstadoPago, transiciones con una puerta única tipo transitarA, diagrama de estados) — con nombres de Strategy, el lector buscará intercambiabilidad externa donde hay ciclo de vida, y viceversa.
Conclusión
Strategy reifica el algoritmo: CalculoEnvio y EstrategiaAsignacion convierten políticas enterradas en condicionales en familias de clases intercambiables — inyectadas por configuración, elegidas por contexto o servidas desde un registro estilo RegistroNotificadores, con lambdas para las variantes ligeras y clases para las que cargan dependencias. La FachadaCheckout y la CentralReparto quedan como deben: orquestando, sin saber cómo se calcula nada. Y te llevas la tabla que desarma las dos confusiones clásicas: frente a State (¿quién cambia el objeto y por qué?) y frente a Bridge (¿algoritmo completo o cimiento técnico de una abstracción?).
Queda una tercera frontera por trazar, la anunciada en "cuándo no": ¿y si las variantes no son algoritmos completos, sino pasos sueltos de un flujo que es siempre el mismo? Los informes de cierre diario de PideYa cargan datos, agregan, formatean y distribuyen — idéntico esqueleto, pasos distintos según el formato. Para eso el GoF tiene su patrón de herencia por excelencia, con su famoso principio de Hollywood: "no nos llames; nosotros te llamaremos". Nos vemos en Template Method.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
