Patrón Strategy
Encapsula cada variante de un algoritmo en su propia clase con una interfaz común, para elegirla o cambiarla en tiempo de ejecución sin llenar el código de if: envíos, descuentos, pagos, ordenaciones.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Strategy
Elige el peso, la distancia y las estrategias que se van a usar, una detrás de otra.
Paso 1
El pedido guarda una estrategia por su interfaz (EstrategiaEnvio): no sabe cuál es ni cómo calcula.
1class Pedido {
2 private final double kg, km;
3 private EstrategiaEnvio estrategia; // estrategia = null, coste = —
4
5 void setEstrategia(EstrategiaEnvio e) {
6 this.estrategia = e;
7 }
8
9 double costeEnvio() {
10 return estrategia.coste(kg, km);
11 }
12}Variables
- estrategia
- null
- coste
- —
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Muchas veces una misma tarea se puede hacer de varias formas: calcular el envío estándar o urgente, cobrar con tarjeta o con PayPal, ordenar por nombre o por nota. La solución ingenua es un método con un if o un switch por cada forma, que crece cada vez que aparece una nueva y que obliga a tocar código que ya funcionaba.
El patrón Strategy saca cada forma a su propia clase. Todas implementan la misma interfaz (la estrategia), y la clase que las usa (el contexto) guarda una referencia a esa interfaz y le delega el trabajo, sin saber cuál es. Añadir una forma nueva es añadir una clase; el contexto no cambia (principio abierto/cerrado).
En Java moderno, si la estrategia tiene un solo método, es una interfaz funcional y cada estrategia puede ser una lambda o una referencia a método. Comparator es el ejemplo de todos los días: sort es siempre el mismo algoritmo y el comparador es la estrategia que decide el orden.
Cuándo usarlo
- Hay varias variantes de un algoritmo o de una regla de negocio y se elige una según la configuración, el usuario o los datos.
- Un método tiene un
switcho una cadena deifsobre un «tipo» que crece con el tiempo. - Se quiere poder añadir variantes sin tocar (ni volver a probar) el código que las usa.
Cuándo no
- Si solo hay dos variantes que nunca van a cambiar: un
ifes más sencillo. - Si las variantes necesitan datos muy distintos del contexto: la interfaz común acaba recibiendo parámetros que casi nadie usa.
Participantes
- Estrategia. La interfaz común (
EstrategiaEnvio) con el método que cambia según la variante (coste). - Estrategias concretas. Una clase por variante (
Estandar,Urgente,RecogidaEnTienda) que implementa la interfaz. - Contexto. La clase que usa una estrategia (
Pedido): la guarda en un atributo del tipo de la interfaz, permite cambiarla y le delega el cálculo. - Cliente. Quien decide qué estrategia usar y se la pasa al contexto, a menudo a partir de una tabla (un
Mapde nombre a estrategia).
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2interface EstrategiaEnvio {
3 +coste(kg : double, km : double) : double
4}
5class Estandar
6class Urgente
7class RecogidaEnTienda
8class Pedido {
9 -estrategia : EstrategiaEnvio
10 +setEstrategia(e : EstrategiaEnvio) : void
11 +costeEnvio() : double
12}
13EstrategiaEnvio <|.. Estandar
14EstrategiaEnvio <|.. Urgente
15EstrategiaEnvio <|.. RecogidaEnTienda
16Pedido o-- "1" EstrategiaEnvio : estrategia
17@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Estrategias de envío
El pedido no sabe qué tipo de envío tiene: llama a coste y la estrategia hace el resto. La última se escribe con una lambda.
1import java.util.Locale;
2
3/** La estrategia: una forma de calcular el envío. */
4interface EstrategiaEnvio {
5 double coste(double kg, double km);
6}
7
8class Estandar implements EstrategiaEnvio {
9 public double coste(double kg, double km) { return 3 + 0.5 * kg; }
10}
11
12class Urgente implements EstrategiaEnvio {
13 public double coste(double kg, double km) { return 8 + kg + 0.02 * km; }
14}
15
16/** El contexto: usa una estrategia sin saber cuál es. */
17class Pedido {
18 private final double kg, km;
19 private EstrategiaEnvio estrategia = new Estandar();
20
21 Pedido(double kg, double km) { this.kg = kg; this.km = km; }
22
23 void setEstrategia(EstrategiaEnvio e) { this.estrategia = e; } // se cambia en tiempo de ejecución
24
25 double costeEnvio() { return estrategia.coste(kg, km); } // ni un if sobre el tipo de envío
26}
27
28public class Main {
29 public static void main(String[] args) {
30 Pedido p = new Pedido(2, 300);
31 Locale es = Locale.forLanguageTag("es-ES");
32 System.out.printf(es, "Estándar: %.2f €%n", p.costeEnvio());
33 p.setEstrategia(new Urgente());
34 System.out.printf(es, "Urgente: %.2f €%n", p.costeEnvio());
35 p.setEstrategia((kg, km) -> 0); // una interfaz funcional admite una lambda
36 System.out.printf(es, "Recogida en tienda: %.2f €%n", p.costeEnvio());
37 }
38}from abc import ABC, abstractmethod
class EstrategiaEnvio(ABC):
"""La estrategia: una forma de calcular el envío."""
@abstractmethod
def coste(self, kg, km): ...
class Estandar(EstrategiaEnvio):
def coste(self, kg, km):
return 3 + 0.5 * kg
class Urgente(EstrategiaEnvio):
def coste(self, kg, km):
return 8 + kg + 0.02 * km
class Pedido:
"""El contexto: usa una estrategia sin saber cuál es."""
def __init__(self, kg, km):
self.kg, self.km = kg, km
self.estrategia = Estandar()
def set_estrategia(self, e): # se cambia en tiempo de ejecución
self.estrategia = e
def coste_envio(self): # ni un if sobre el tipo de envío
return self.estrategia.coste(self.kg, self.km)
class RecogidaEnTienda(EstrategiaEnvio):
def coste(self, kg, km):
return 0
def euros(x):
return f"{x:.2f}".replace(".", ",") + " €"
p = Pedido(2, 300)
print("Estándar:", euros(p.coste_envio()))
p.set_estrategia(Urgente())
print("Urgente:", euros(p.coste_envio()))
p.set_estrategia(RecogidaEnTienda())
print("Recogida en tienda:", euros(p.coste_envio()))/** Cada estrategia es una clase con el mismo método: coste(kg, km). */
class Estandar {
coste(kg, km) { return 3 + 0.5 * kg; }
}
class Urgente {
coste(kg, km) { return 8 + kg + 0.02 * km; }
}
/** El contexto: usa una estrategia sin saber cuál es. */
class Pedido {
estrategia = new Estandar();
constructor(kg, km) { this.kg = kg; this.km = km; }
setEstrategia(e) { this.estrategia = e; } // se cambia en tiempo de ejecución
costeEnvio() { return this.estrategia.coste(this.kg, this.km); } // ni un if sobre el tipo de envío
}
const euros = (x) => x.toFixed(2).replace(".", ",") + " €";
const p = new Pedido(2, 300);
console.log("Estándar: " + euros(p.costeEnvio()));
p.setEstrategia(new Urgente());
console.log("Urgente: " + euros(p.costeEnvio()));
p.setEstrategia({ coste: () => 0 }); // vale cualquier objeto con un método coste
console.log("Recogida en tienda: " + euros(p.costeEnvio()));using System;
using System.Globalization;
// La estrategia: una forma de calcular el envío.
interface IEstrategiaEnvio {
double Coste(double kg, double km);
}
class Estandar : IEstrategiaEnvio {
public double Coste(double kg, double km) => 3 + 0.5 * kg;
}
class Urgente : IEstrategiaEnvio {
public double Coste(double kg, double km) => 8 + kg + 0.02 * km;
}
class RecogidaEnTienda : IEstrategiaEnvio {
public double Coste(double kg, double km) => 0;
}
// El contexto: usa una estrategia sin saber cuál es.
class Pedido {
private readonly double kg, km;
private IEstrategiaEnvio estrategia = new Estandar();
public Pedido(double kg, double km) { this.kg = kg; this.km = km; }
public void SetEstrategia(IEstrategiaEnvio e) => estrategia = e; // se cambia en tiempo de ejecución
public double CosteEnvio() => estrategia.Coste(kg, km); // ni un if sobre el tipo de envío
}
class Program {
static void Main() {
var es = CultureInfo.GetCultureInfo("es-ES");
var p = new Pedido(2, 300);
Console.WriteLine("Estándar: " + p.CosteEnvio().ToString("F2", es) + " €");
p.SetEstrategia(new Urgente());
Console.WriteLine("Urgente: " + p.CosteEnvio().ToString("F2", es) + " €");
p.SetEstrategia(new RecogidaEnTienda());
Console.WriteLine("Recogida en tienda: " + p.CosteEnvio().ToString("F2", es) + " €");
}
}<?php
/** La estrategia: una forma de calcular el envío. */
interface EstrategiaEnvio {
public function coste(float $kg, float $km): float;
}
class Estandar implements EstrategiaEnvio {
public function coste(float $kg, float $km): float { return 3 + 0.5 * $kg; }
}
class Urgente implements EstrategiaEnvio {
public function coste(float $kg, float $km): float { return 8 + $kg + 0.02 * $km; }
}
/** El contexto: usa una estrategia sin saber cuál es. */
class Pedido {
private EstrategiaEnvio $estrategia;
public function __construct(private float $kg, private float $km) {
$this->estrategia = new Estandar();
}
public function setEstrategia(EstrategiaEnvio $e): void { $this->estrategia = $e; } // se cambia en tiempo de ejecución
public function costeEnvio(): float { return $this->estrategia->coste($this->kg, $this->km); } // ni un if
}
function euros(float $x): string { return number_format($x, 2, ",", ".") . " €"; }
$p = new Pedido(2, 300);
echo "Estándar: " . euros($p->costeEnvio()) . "\n";
$p->setEstrategia(new Urgente());
echo "Urgente: " . euros($p->costeEnvio()) . "\n";
$p->setEstrategia(new class implements EstrategiaEnvio { // una clase anónima también vale
public function coste(float $kg, float $km): float { return 0; }
});
echo "Recogida en tienda: " . euros($p->costeEnvio()) . "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Estándar: 4,00 € Urgente: 16,00 € Recogida en tienda: 0,00 €
Comparator: la estrategia de la biblioteca
1// Comparator es una estrategia de ordenación: el algoritmo (sort) no cambia, la forma de comparar sí
2List<Alumno> lista = cargarAlumnos();
3lista.sort(Comparator.comparing(Alumno::nota).reversed()); // por nota, de mayor a menor
4lista.sort(Comparator.comparing(Alumno::curso).thenComparing(Alumno::nombre)); // por curso y, dentro, por nombre# key= es la estrategia de ordenación: sort no cambia, la forma de comparar sí alumnos = cargar_alumnos() alumnos.sort(key=lambda a: a.nota, reverse=True) # por nota, de mayor a menor alumnos.sort(key=lambda a: (a.curso, a.nombre)) # por curso y, dentro, por nombre
// La función de comparación es la estrategia: sort no cambia, la forma de comparar sí const alumnos = cargarAlumnos(); alumnos.sort((a, b) => b.nota - a.nota); // por nota, de mayor a menor alumnos.sort((a, b) => a.curso - b.curso || a.nombre.localeCompare(b.nombre)); // por curso y, dentro, por nombre
// El selector de clave es la estrategia: OrderBy no cambia, la forma de comparar sí List<Alumno> alumnos = CargarAlumnos(); var porNota = alumnos.OrderByDescending(a => a.Nota).ToList(); // por nota, de mayor a menor var porCurso = alumnos.OrderBy(a => a.Curso).ThenBy(a => a.Nombre).ToList(); // por curso y, dentro, por nombre
// La función de comparación es la estrategia: usort no cambia, la forma de comparar sí $alumnos = cargarAlumnos(); usort($alumnos, fn($a, $b) => $b->nota <=> $a->nota); // por nota, de mayor a menor usort($alumnos, fn($a, $b) => [$a->curso, $a->nombre] <=> [$b->curso, $b->nombre]); // por curso y, dentro, por nombre
En la práctica
ComparatorenList.sort,Collections.sortoTreeMap;FileFilterenFile.listFiles.- Los
LayoutManagerde Swing: el contenedor delega en su estrategia de colocación (FlowLayout,BorderLayout…). - En Spring, se inyecta la implementación de una interfaz que toca según la configuración (por ejemplo, el servicio de pago);
RejectedExecutionHandleren los pools de hilos. - Las reglas de validación de un formulario o las tarifas de una tienda guardadas en un
Mapde nombre a estrategia.
Errores típicos
- Mover el
switchdel contexto al cliente sin más: sigue habiendo unswitchque crece. Una tabla (Map) de nombre a estrategia lo evita. - Que el contexto pregunte el tipo de su estrategia (
if (estrategia instanceof Urgente)): se pierde todo lo ganado. - Crear una clase para cada estrategia de una línea cuando una lambda basta.
- Interfaces con métodos que solo algunas estrategias usan: es señal de que mezclan dos responsabilidades.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Gastos de envío con estrategias
La tienda calcula el envío con la estrategia Estandar (ya escrita). Añade las estrategias Urgente, RecogidaEnTienda e Internacional según la tabla y regístralas en el mapa ESTRATEGIAS del main con las claves urgente, tienda e internacional. El pedido no debe saber qué tipo de envío tiene.
- Cada línea:
tipo kg km. Salida:nombre: kg kg, km km → coste(con dos decimales y coma) y, al final,Total de N pedidos: X €. - Tarifas: estándar 3 € + 0,50 €/kg + 0,01 €/km; urgente 8 € + 1 €/kg + 0,02 €/km; recogida en tienda, gratis; internacional, el mayor de 15 € + 2 €/kg y 0,05 €/km. Nombres:
estándar,urgente,recogida en tienda,internacional. - Un tipo que no está en el mapa:
Envío desconocido: tipo; números mal escritos:Línea no válida: línea.
Ejemplo
estandar 2 100 urgente 2 100 tienda 5 0 internacional 1 1000
estándar: 2 kg, 100 km → 5,00 € urgente: 2 kg, 100 km → 12,00 € recogida en tienda: 5 kg, 0 km → 0,00 € internacional: 1 kg, 1000 km → 50,00 € Total de 4 pedidos: 67,00 €
Ver la solución explicada
1import java.util.*;
2
3interface EstrategiaEnvio {
4 String nombre();
5 double coste(double kg, double km);
6}
7
8class Estandar implements EstrategiaEnvio {
9 public String nombre() { return "estándar"; }
10 public double coste(double kg, double km) { return 3 + 0.5 * kg + 0.01 * km; }
11}
12
13class Urgente implements EstrategiaEnvio {
14 public String nombre() { return "urgente"; }
15 public double coste(double kg, double km) { return 8 + 1.0 * kg + 0.02 * km; }
16}
17
18class RecogidaEnTienda implements EstrategiaEnvio {
19 public String nombre() { return "recogida en tienda"; }
20 public double coste(double kg, double km) { return 0; }
21}
22
23class Internacional implements EstrategiaEnvio {
24 public String nombre() { return "internacional"; }
25 public double coste(double kg, double km) { return Math.max(15 + 2 * kg, 0.05 * km); }
26}
27
28class Pedido {
29 private final double kg, km;
30 private EstrategiaEnvio estrategia;
31
32 Pedido(double kg, double km, EstrategiaEnvio estrategia) {
33 this.kg = kg;
34 this.km = km;
35 this.estrategia = estrategia;
36 }
37
38 double costeEnvio() { return estrategia.coste(kg, km); }
39 String descripcion() { return estrategia.nombre(); }
40}
41
42public class Main {
43 static final Map<String, EstrategiaEnvio> ESTRATEGIAS = Map.of(
44 "estandar", new Estandar(),
45 "urgente", new Urgente(),
46 "tienda", new RecogidaEnTienda(),
47 "internacional", new Internacional());
48
49 static String euros(double x) {
50 return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
51 }
52
53 public static void main(String[] args) {
54 Scanner sc = new Scanner(System.in);
55 double total = 0;
56 int pedidos = 0;
57 while (sc.hasNextLine()) {
58 String linea = sc.nextLine().trim();
59 if (linea.isEmpty()) continue;
60 String[] p = linea.split("\\s+");
61 EstrategiaEnvio e = ESTRATEGIAS.get(p[0]);
62 if (e == null) {
63 System.out.println("Envío desconocido: " + p[0]);
64 continue;
65 }
66 double kg, km;
67 try {
68 kg = Double.parseDouble(p[1]);
69 km = Double.parseDouble(p[2]);
70 } catch (RuntimeException ex) {
71 System.out.println("Línea no válida: " + linea);
72 continue;
73 }
74 Pedido pedido = new Pedido(kg, km, e);
75 double c = pedido.costeEnvio();
76 total += c;
77 pedidos++;
78 System.out.println(pedido.descripcion() + ": " + p[1] + " kg, " + p[2] + " km → " + euros(c));
79 }
80 System.out.println("Total de " + pedidos + (pedidos == 1 ? " pedido: " : " pedidos: ") + euros(total));
81 }
82}Añadir una forma de envío es añadir una clase y una entrada en el mapa: Pedido y el bucle del main no cambian. Es el principio abierto/cerrado: abierto a extensiones, cerrado a modificaciones.
El mapa de nombre a estrategia sustituye al switch que habría decidido qué cálculo usar.
2. Ordenar con estrategias encadenadas
Los Comparator son estrategias de ordenación. Construye el comparador que pide cada orden ordenar a partir de sus criterios: cada uno es un campo (nombre, curso, nota o edad) con asc (por defecto) o desc, y van separados por comas; el segundo criterio solo desempata el primero, y así sucesivamente. Los nombres se comparan sin distinguir mayúsculas. La lectura y la salida ya están: completa criterio y comparador.
alumno nombre;curso;nota;edadañade un alumno.ordenar criteriosescribePor criterios:y la lista ordenada (sin cambiar la original).- Un criterio con un campo desconocido, un sentido que no es
ascnidesco palabras de más:Criterio no válido: criterios.
Ejemplo
alumno Lucia;DAM1;8.5;19 alumno marcos;DAW1;6;21 alumno Ana;DAM1;8.5;18 alumno Bruno;DAW1;9.25;19 ordenar nota desc, nombre ordenar curso, edad desc
Por nota desc, nombre: Bruno DAW1 9,25 19 Ana DAM1 8,50 18 Lucia DAM1 8,50 19 marcos DAW1 6,00 21 Por curso, edad desc: Lucia DAM1 8,50 19 Ana DAM1 8,50 18 marcos DAW1 6,00 21 Bruno DAW1 9,25 19
Ver la solución explicada
1import java.util.*;
2
3record Alumno(String nombre, String curso, double nota, int edad) { }
4
5public class Main {
6 /** El comparador de un criterio como «nota desc »; null si el campo no existe. */
7 static Comparator<Alumno> criterio(String texto) {
8 String[] p = texto.trim().split("\\s+");
9 Comparator<Alumno> c = switch (p[0]) {
10 case "nombre" -> Comparator.comparing(Alumno::nombre, String.CASE_INSENSITIVE_ORDER);
11 case "curso" -> Comparator.comparing(Alumno::curso);
12 case "nota" -> Comparator.comparingDouble(Alumno::nota);
13 case "edad" -> Comparator.comparingInt(Alumno::edad);
14 default -> null;
15 };
16 if (c == null || p.length > 2 || (p.length == 2 && !p[1].matches("asc|desc"))) return null;
17 return p.length == 2 && p[1].equals("desc") ? c.reversed() : c;
18 }
19
20 /** Encadena los criterios separados por comas: el segundo solo desempata el primero, y así. */
21 static Comparator<Alumno> comparador(String criterios) {
22 Comparator<Alumno> total = null;
23 for (String parte : criterios.split(",")) {
24 Comparator<Alumno> c = criterio(parte);
25 if (c == null) return null;
26 total = total == null ? c : total.thenComparing(c);
27 }
28 return total;
29 }
30
31 public static void main(String[] args) {
32 Scanner sc = new Scanner(System.in);
33 List<Alumno> alumnos = new ArrayList<>();
34 while (sc.hasNextLine()) {
35 String linea = sc.nextLine().trim();
36 if (linea.isEmpty()) continue;
37 if (linea.startsWith("alumno ")) {
38 String[] d = linea.substring(7).split(";");
39 try {
40 alumnos.add(new Alumno(d[0].trim(), d[1].trim(), Double.parseDouble(d[2].trim()), Integer.parseInt(d[3].trim())));
41 } catch (RuntimeException e) {
42 System.out.println("Alumno no válido: " + linea);
43 }
44 } else if (linea.startsWith("ordenar ")) {
45 Comparator<Alumno> c = comparador(linea.substring(8));
46 if (c == null) {
47 System.out.println("Criterio no válido: " + linea.substring(8));
48 continue;
49 }
50 List<Alumno> copia = new ArrayList<>(alumnos);
51 copia.sort(c);
52 System.out.println("Por " + linea.substring(8) + ":");
53 for (Alumno a : copia) System.out.println(String.format(Locale.ROOT, " %-14s %-5s %5.2f %3d", a.nombre(), a.curso(), a.nota(), a.edad()).replace('.', ','));
54 } else {
55 System.out.println("Orden no válida: " + linea);
56 }
57 }
58 }
59}Cada criterio es una estrategia pequeña y thenComparing las combina en una más grande sin escribir ni una comparación a mano: el algoritmo de ordenación es siempre el mismo.
Construir la estrategia a partir del texto del usuario es lo que hace un listado con columnas ordenables: la vista elige el comparador y la lista se ordena igual.
Test
Test: Strategy
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.En el patrón Strategy, ¿qué hace el contexto?
2.¿Qué principio favorece el patrón Strategy?
3.¿Qué clase de la biblioteca de Java es una estrategia de ordenación?
4.La interfaz de tu estrategia tiene un solo método abstracto. ¿Qué puedes usar como estrategia sin crear una clase?
5.¿Qué olor del código suele pedir a gritos el patrón Strategy?