Apuntes DAM
Volver al inicio

Patrón Strategy

Patrones de diseñoDe comportamientoNivel básicoTambién: estrategia, policy

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.

estandar, urgente, tienda o internacional

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 switch o una cadena de if sobre 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 if es 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

  1. Estrategia. La interfaz común (EstrategiaEnvio) con el método que cambia según la variante (coste).
  2. Estrategias concretas. Una clase por variante (Estandar, Urgente, RecogidaEnTienda) que implementa la interfaz.
  3. 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.
  4. Cliente. Quien decide qué estrategia usar y se la pasa al contexto, a menudo a partir de una tabla (un Map de nombre a estrategia).

Diagrama de clases

1estrategia«interface»EstrategiaEnvio+coste(kg : double, km : double) : doubleEstandarUrgenteRecogidaEnTiendaPedido-estrategia : EstrategiaEnvio+setEstrategia(e : EstrategiaEnvio) : void+costeEnvio() : double
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
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@enduml

Puedes 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.

Java
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}

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

Java
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

En la práctica

  • Comparator en List.sort, Collections.sort o TreeMap; FileFilter en File.listFiles.
  • Los LayoutManager de 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); RejectedExecutionHandler en los pools de hilos.
  • Las reglas de validación de un formulario o las tarifas de una tienda guardadas en un Map de nombre a estrategia.

Errores típicos

  • Mover el switch del contexto al cliente sin más: sigue habiendo un switch que 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.
☕JavaGastos de envío con estrategiasFácil

Ejemplo

Entrada (lo que se escribe por teclado)
estandar 2 100
urgente 2 100
tienda 5 0
internacional 1 1000
Salida esperada
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 €
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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;edad añade un alumno. ordenar criterios escribe Por criterios: y la lista ordenada (sin cambiar la original).
  • Un criterio con un campo desconocido, un sentido que no es asc ni desc o palabras de más: Criterio no válido: criterios.
☕JavaOrdenar con estrategias encadenadasMedio

Ejemplo

Entrada (lo que se escribe por teclado)
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
Salida esperada
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
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 aciertos

Elige 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. 1.En el patrón Strategy, ¿qué hace el contexto?

  2. 2.¿Qué principio favorece el patrón Strategy?

  3. 3.¿Qué clase de la biblioteca de Java es una estrategia de ordenación?

  4. 4.La interfaz de tu estrategia tiene un solo método abstracto. ¿Qué puedes usar como estrategia sin crear una clase?

  5. 5.¿Qué olor del código suele pedir a gritos el patrón Strategy?

Relacionado