Apuntes DAM

Patrón Template Method

Un método de la clase base fija los pasos de un algoritmo y su orden, y las subclases rellenan solo los que cambian. Así funcionan los servlets, los tests de JUnit o el ciclo de vida de Android.

nivel básicoTambién: método plantilla, plantilla, hook, gancho

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.

Template Method

Elige el tipo de viaje y el destino. Los pasos fijos están en la clase base; los que cambian, en la subclase.

Paso 1

new AltaVelocidad().hacer("Sevilla"): el método plantilla está en Viaje y es final. Columna izquierda: pasos de la clase base; derecha: lo que pone AltaVelocidad.

1final void hacer(String destino) {  // viaje = AltaVelocidad, paso = 0
2    comprarBillete();
3    if (necesitaControl())
4        System.out.println("  Pasar el control");
5    System.out.println("  Subir al tren");
6    trayecto(destino);
7    System.out.println("  Bajar en " + destino);
8}
9
10protected boolean necesitaControl() { return false; }

Variables

viaje
AltaVelocidad
paso
0

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Dos viajes en tren siguen los mismos pasos: comprar el billete, subir, hacer el trayecto y bajar. Lo que cambia es cómo se compra el billete o cuánto dura el trayecto. Si cada clase escribe el viaje entero, el orden de los pasos se copia y, cuando hay que añadir uno (pasar un control), hay que tocar todas.

El patrón Template Method escribe el algoritmo una sola vez en la clase base, en un método (el método plantilla, hacer) que llama a los pasos en su orden. Los pasos que cambian son métodos abstractos que escribe cada subclase; los opcionales, «ganchos» con un comportamiento por defecto que la subclase puede redefinir (necesitaControl() devuelve false salvo en el AVE).

El método plantilla suele ser final: las subclases deciden el contenido de los pasos, nunca su orden. Es el principio de Hollywood, «no nos llames, nosotros te llamamos»: el framework llama a tu doGet, a tu onCreate o a tu método @Test cuando le toca.

Cuándo usarlo

  • Varias clases hacen lo mismo con pequeñas variaciones en algunos pasos: el esqueleto se escribe una vez.
  • Quieres controlar el orden de los pasos y que las subclases solo puedan cambiar algunos (por ejemplo, para garantizar que siempre se cierra un recurso).
  • Estás escribiendo un framework o una clase base para que otros la extiendan (servlets, tests, actividades, juegos).

Cuándo no

  • Si los pasos que cambian se pueden pasar como objetos o lambdas: con composición (Strategy) no hace falta heredar.
  • Si las subclases necesitan cambiar el orden de los pasos: el esqueleto no es común de verdad.

Participantes

  1. Clase abstracta. Tiene el método plantilla (final), que llama a los pasos en orden, y declara los pasos abstractos y los ganchos.
  2. Pasos abstractos. Los que cada subclase tiene que escribir (comprarBillete, trayecto).
  3. Ganchos. Pasos opcionales con un comportamiento por defecto (necesitaControl() devuelve false).
  4. Clases concretas. Cada subclase (Cercanias, AltaVelocidad) rellena los pasos, sin tocar el orden.

Diagrama de clases

«abstract»Viaje+hacer(destino : String) : void#comprarBillete() : void#trayecto(destino : String) : void#necesitaControl() : booleanCercanias#comprarBillete() : void#trayecto(destino : String) : voidAltaVelocidad#comprarBillete() : void#trayecto(destino : String) : void#necesitaControl() : boolean
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2abstract class Viaje {
3  +hacer(destino : String) : void
4  #{abstract} comprarBillete() : void
5  #{abstract} trayecto(destino : String) : void
6  #necesitaControl() : boolean
7}
8class Cercanias {
9  #comprarBillete() : void
10  #trayecto(destino : String) : void
11}
12class AltaVelocidad {
13  #comprarBillete() : void
14  #trayecto(destino : String) : void
15  #necesitaControl() : boolean
16}
17Viaje <|-- Cercanias
18Viaje <|-- AltaVelocidad
19@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Dos formas de viajar en tren

hacer está escrito una vez y es final. Cada viaje rellena dos pasos, y el AVE además cambia un gancho para pasar el control de equipajes.

Java
1/** La clase base fija el esqueleto del viaje; las subclases rellenan los pasos que cambian. */
2abstract class Viaje {
3    /** El método plantilla: el orden de los pasos es siempre el mismo y no se puede cambiar (final). */
4    final void hacer(String destino) {
5        System.out.println("Viaje a " + destino + ":");
6        comprarBillete();
7        if (necesitaControl()) System.out.println("  Pasar el control de equipajes");   // un gancho
8        System.out.println("  Subir al tren");
9        trayecto(destino);
10        System.out.println("  Bajar en " + destino);
11    }
12
13    protected abstract void comprarBillete();                 // pasos que escribe cada subclase
14    protected abstract void trayecto(String destino);
15    protected boolean necesitaControl() { return false; }     // gancho: tiene un valor por defecto
16}
17
18class Cercanias extends Viaje {
19    protected void comprarBillete() { System.out.println("  Pasar el abono por el torno"); }
20    protected void trayecto(String destino) { System.out.println("  12 paradas de pie hasta " + destino); }
21}
22
23class AltaVelocidad extends Viaje {
24    protected void comprarBillete() { System.out.println("  Comprar un billete con asiento: coche 5, plaza 8A"); }
25    protected boolean necesitaControl() { return true; }
26    protected void trayecto(String destino) { System.out.println("  2 h 30 min sin paradas hasta " + destino); }
27}
28
29public class Main {
30    public static void main(String[] args) {
31        new Cercanias().hacer("Alcalá");
32        new AltaVelocidad().hacer("Sevilla");
33    }
34}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Viaje a Alcalá:
  Pasar el abono por el torno
  Subir al tren
  12 paradas de pie hasta Alcalá
  Bajar en Alcalá
Viaje a Sevilla:
  Comprar un billete con asiento: coche 5, plaza 8A
  Pasar el control de equipajes
  Subir al tren
  2 h 30 min sin paradas hasta Sevilla
  Bajar en Sevilla

Servlets, AbstractList y frameworks

Métodos plantilla que ya usas sin saberlo.

Java
1// HttpServlet: service() es el método plantilla; tú solo escribes doGet o doPost
2@WebServlet("/alumnos")
3public class AlumnosServlet extends HttpServlet {
4    @Override
5    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
6        resp.getWriter().println("Listado de alumnos");
7    }
8}
9
10// AbstractList: escribes get y size, y te da iterator, contains, indexOf, equals, toString…
11class Pares extends AbstractList<Integer> {
12    public Integer get(int i) { return 2 * i; }
13    public int size() { return 10; }
14}
15
16// JUnit y Android siguen la misma idea: el framework fija el orden (@BeforeEach, @Test,
17// @AfterEach; onCreate, onStart, onResume…) y tú rellenas los pasos que necesitas.

En la práctica

  • HttpServlet.service() llama a doGet, doPost…; AbstractList, AbstractMap e InputStream piden unos pocos métodos y dan el resto hecho.
  • Los ciclos de vida de Android (onCreate, onStart, onResume…) y de los componentes de los frameworks web.
  • JUnit: el framework ejecuta @BeforeEach, el test y @AfterEach en su orden; en PHPUnit, setUp y tearDown.
  • Los juegos por turnos, las importaciones de datos (leer, validar, guardar, informar) y los informes con cabecera, filas y pie.

Errores típicos

  • No marcar como final el método plantilla: una subclase puede redefinirlo y saltarse pasos o cambiar su orden.
  • Hacer abstractos todos los pasos, incluso los que casi todas las subclases hacen igual: mejor un gancho con un valor por defecto.
  • Que las subclases llamen ellas mismas a los pasos en vez de dejar que los llame el método plantilla.
  • Usar herencia solo para reutilizar dos líneas: si lo que cambia es un algoritmo entero, Strategy suele encajar mejor.

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. Informes con el mismo esqueleto

Informe.generar escribe siempre una cabecera, una fila por cada alumno incluido y un pie. Ya está el informe de texto. Escribe InformeAprobados (que usa el gancho incluir para quedarse solo con las notas de 5 o más) e InformeCsv, y regístralos en el main con las claves aprobados y csv.

  • Órdenes: alumno nombre nota e informe texto|aprobados|csv. Las notas se escriben con coma y sin ceros de más.
  • Aprobados: APROBADOS, una línea + nombre (nota) por aprobado y N aprobados, media M (1 aprobado…), o Nadie ha aprobado.
  • CSV: nombre;nota y una línea nombre;nota por alumno, sin pie.
JavaInformes con el mismo esqueletoFácil

Ejemplo

Entrada (lo que se escribe por teclado)
alumno Ana 8.5
alumno Luis 4
alumno Marta 6.25
informe texto
informe aprobados
informe csv
Salida esperada
NOTAS
  Ana        8,5
  Luis       4
  Marta      6,25
Media de 3 alumnos: 6,25
APROBADOS
  + Ana (8,5)
  + Marta (6,25)
2 aprobados, media 7,38
nombre;nota
Ana;8,5
Luis;4
Marta;6,25
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.math.*;
2import java.util.*;
3
4record Alumno(String nombre, double nota) {}
5
6/** El esqueleto de cualquier informe: cabecera, una fila por alumno incluido y pie. */
7abstract class Informe {
8    final String generar(List<Alumno> alumnos) {
9        StringBuilder sb = new StringBuilder(cabecera());
10        int n = 0;
11        double suma = 0;
12        for (Alumno a : alumnos) {
13            if (!incluir(a)) continue;
14            sb.append(fila(a));
15            n++;
16            suma += a.nota();
17        }
18        sb.append(pie(n, n == 0 ? 0 : suma / n));
19        return sb.toString();
20    }
21
22    protected abstract String cabecera();
23    protected abstract String fila(Alumno a);
24    protected abstract String pie(int n, double media);
25    protected boolean incluir(Alumno a) { return true; }          // gancho: por defecto, todos
26}
27
28class InformeTexto extends Informe {
29    protected String cabecera() { return "NOTAS\n"; }
30    protected String fila(Alumno a) { return String.format("  %-10s %s", a.nombre(), Main.nota(a.nota())) + "\n"; }
31    protected String pie(int n, double media) { return "Media de " + n + " alumnos: " + Main.nota(media) + "\n"; }
32}
33
34class InformeAprobados extends Informe {
35    protected boolean incluir(Alumno a) { return a.nota() >= 5; }
36    protected String cabecera() { return "APROBADOS\n"; }
37    protected String fila(Alumno a) { return "  + " + a.nombre() + " (" + Main.nota(a.nota()) + ")\n"; }
38    protected String pie(int n, double media) {
39        return n == 0 ? "Nadie ha aprobado\n" : n + (n == 1 ? " aprobado" : " aprobados") + ", media " + Main.nota(media) + "\n";
40    }
41}
42
43class InformeCsv extends Informe {
44    protected String cabecera() { return "nombre;nota\n"; }
45    protected String fila(Alumno a) { return a.nombre() + ";" + Main.nota(a.nota()) + "\n"; }
46    protected String pie(int n, double media) { return ""; }
47}
48
49public class Main {
50    static String nota(double x) {
51        return BigDecimal.valueOf(x).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString().replace('.', ',');
52    }
53
54    public static void main(String[] args) {
55        Map<String, Informe> informes = new HashMap<>();
56        informes.put("texto", new InformeTexto());
57        informes.put("aprobados", new InformeAprobados());
58        informes.put("csv", new InformeCsv());
59        List<Alumno> alumnos = new ArrayList<>();
60        Scanner sc = new Scanner(System.in);
61        while (sc.hasNextLine()) {
62            String[] p = sc.nextLine().trim().split("\\s+");
63            if (p[0].isEmpty()) continue;
64            try {
65                if (p[0].equals("alumno") && p.length == 3) {
66                    double n = Double.parseDouble(p[2]);
67                    if (n < 0 || n > 10) throw new NumberFormatException();
68                    alumnos.add(new Alumno(p[1], n));
69                } else if (p[0].equals("informe") && p.length == 2) {
70                    Informe i = informes.get(p[1]);
71                    System.out.print(i == null ? "Informe desconocido: " + p[1] + "\n" : i.generar(alumnos));
72                } else {
73                    System.out.println("Orden no válida: " + String.join(" ", p));
74                }
75            } catch (NumberFormatException e) {
76                System.out.println("Nota no válida: " + String.join(" ", p));
77            }
78        }
79    }
80}

El recorrido, el filtrado y la media están escritos una sola vez; cada informe solo dice cómo se escribe.

El gancho permite cambiar un paso opcional sin obligar a las demás subclases a escribirlo.

2. Juegos por turnos

Juego.jugar es el bucle de cualquier juego por turnos: pide jugadas, las valida, cambia de turno y anuncia al ganador. Ya está el Nim. Escribe Carrera (la carrera a 21) rellenando los cinco pasos abstractos, y regístrala en el main con juego carrera.

  • La primera línea es juego nim o juego carrera; después, una jugada por línea. Juegan Ana y Luis, empieza Ana.
  • Carrera a 21: se empieza en 0 y cada jugador suma 1, 2 o 3, sin repetir lo que acaba de sumar el rival y sin pasarse de 21; gana quien llega justo a 21. nombre() es Carrera a 21, estado() es van N y cada jugada válida se cuenta como Jugador suma n: van N.
  • Una jugada no válida devuelve null en mover, y el bucle hace repetir al mismo jugador.
JavaJuegos por turnosMedio

Ejemplo

Entrada (lo que se escribe por teclado)
juego carrera
3
2
3
1
3
2
3
1
3
Salida esperada
Empieza Carrera a 21: van 0
  Ana suma 3: van 3
  Luis suma 2: van 5
  Ana suma 3: van 8
  Luis suma 1: van 9
  Ana suma 3: van 12
  Luis suma 2: van 14
  Ana suma 3: van 17
  Luis suma 1: van 18
  Ana suma 3: van 21
Gana Ana
Test oculto #3
Test oculto #4
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3/** El esqueleto de cualquier juego por turnos entre dos jugadores. */
4abstract class Juego {
5    protected final String[] jugadores = {"Ana", "Luis"};
6    protected int turno = 0;
7
8    /** El método plantilla: el bucle de turnos es el mismo para todos los juegos. */
9    final void jugar(Iterator<String> jugadas) {
10        System.out.println("Empieza " + nombre() + ": " + estado());
11        while (!terminado()) {
12            if (!jugadas.hasNext()) {
13                System.out.println("Partida sin terminar");
14                return;
15            }
16            String jugador = jugadores[turno];
17            String jugada = jugadas.next();
18            String r = mover(jugador, jugada);
19            if (r == null) {
20                System.out.println("  Jugada no válida de " + jugador + ": " + jugada);
21                continue;                                  // vuelve a tirar el mismo jugador
22            }
23            System.out.println("  " + r);
24            if (!terminado()) turno = 1 - turno;
25        }
26        System.out.println("Gana " + ganador());
27    }
28
29    protected abstract String nombre();
30    protected abstract String estado();
31    /** Aplica la jugada y cuenta lo que ha pasado, o devuelve null si no es válida. */
32    protected abstract String mover(String jugador, String jugada);
33    protected abstract boolean terminado();
34    protected abstract String ganador();
35
36    /** La jugada como número, o -1 si no lo es. */
37    static int numero(String jugada) {
38        try { return Integer.parseInt(jugada); } catch (NumberFormatException e) { return -1; }
39    }
40}
41
42/** Nim: hay 15 palillos y cada jugador quita 1, 2 o 3; quien quita el último pierde. */
43class Nim extends Juego {
44    private int quedan = 15;
45    protected String nombre() { return "Nim"; }
46    protected String estado() { return quedan + " palillos"; }
47    protected String mover(String jugador, String jugada) {
48        int n = numero(jugada);
49        if (n < 1 || n > 3 || n > quedan) return null;
50        quedan -= n;
51        return jugador + " quita " + n + ": quedan " + quedan;
52    }
53    protected boolean terminado() { return quedan == 0; }
54    protected String ganador() { return jugadores[1 - turno]; }    // quien quitó el último pierde
55}
56
57/** Carrera a 21: cada jugador suma 1, 2 o 3 (sin repetir lo que acaba de sumar el rival); gana quien llega a 21. */
58class Carrera extends Juego {
59    private int total = 0;
60    private int anterior = 0;
61    protected String nombre() { return "Carrera a 21"; }
62    protected String estado() { return "van " + total; }
63    protected String mover(String jugador, String jugada) {
64        int n = numero(jugada);
65        if (n < 1 || n > 3 || n == anterior || total + n > 21) return null;
66        total += n;
67        anterior = n;
68        return jugador + " suma " + n + ": van " + total;
69    }
70    protected boolean terminado() { return total == 21; }
71    protected String ganador() { return jugadores[turno]; }        // quien llega a 21 gana
72}
73
74public class Main {
75    public static void main(String[] args) {
76        Scanner sc = new Scanner(System.in);
77        if (!sc.hasNextLine()) return;
78        String cual = sc.nextLine().trim();
79        List<String> jugadas = new ArrayList<>();
80        while (sc.hasNextLine()) {
81            String l = sc.nextLine().trim();
82            if (!l.isEmpty()) jugadas.add(l);
83        }
84        Juego juego = switch (cual) {
85            case "juego nim" -> new Nim();
86            case "juego carrera" -> new Carrera();
87            default -> null;
88        };
89        if (juego == null) System.out.println("Juego desconocido: " + cual);
90        else juego.jugar(jugadas.iterator());
91    }
92}

Los turnos, las jugadas no válidas y el final de partida están resueltos una vez para todos los juegos: cada uno solo pone sus reglas.

Así funcionan los motores de juegos y los frameworks: llaman a tus métodos cuando toca.

Test

Test: Template Method

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.¿Qué hace el método plantilla?

  2. 2.¿Por qué el método plantilla suele ser final?

  3. 3.¿Qué es un «gancho» (hook)?

  4. 4.¿Cuál de estos usa el patrón Template Method?

  5. 5.¿En qué se diferencia de Strategy?

Relacionado