Apuntes DAM

Patrón Command

Convierte una acción en un objeto que sabe ejecutarse y deshacerse, para guardarla, encolarla, combinarla en macros o deshacerla: lo que hay detrás del Ctrl+Z, los botones y las colas de tareas.

nivel intermedioTambién: comando, orden, acción, deshacer, undo

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.

Command

Escribe las órdenes separadas por punto y coma: escribir texto, borrar n o deshacer.

Paso 1

La orden se convierte en un objeto, Escribir("Hola"), y el historial le pide que se ejecute.

1void ejecutar(Comando c) {                     // Historial
2    c.ejecutar();  // texto = "", en la pila = 0
3    hechos.push(c);
4}
5
6void deshacer() {
7    if (hechos.isEmpty()) return;
8    hechos.pop().deshacer();
9}
10
11public void ejecutar() {                       // Borrar
12    borrado = editor.texto.substring(desde);
13    editor.texto.setLength(desde);
14}

Variables

texto
""
en la pila
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

Pulsar un botón, elegir una opción del menú o un atajo de teclado deberían hacer lo mismo. Además, el usuario quiere deshacer, y a veces hay que guardar las acciones para ejecutarlas más tarde o en otro hilo. Si cada botón llama directamente a los métodos que hacen el trabajo, nada de eso es posible: la acción desaparece en cuanto se ejecuta.

El patrón Command convierte cada acción en un objeto con un método ejecutar() (y, si se puede deshacer, deshacer()). El comando guarda a quién afecta (el receptor) y los datos que necesita, incluido lo necesario para volver atrás. Quien lanza los comandos (el invocador: un botón, un historial, una cola) no sabe qué hacen: solo los ejecuta y los guarda.

Con los comandos guardados en una pila, deshacer es sacar el último y llamar a su deshacer(); rehacer es otra pila. Un comando hecho de comandos es una macro. En Java, Runnable es un comando sin deshacer, y un ExecutorService es una cola de comandos.

Cuándo usarlo

  • Hay que deshacer (y rehacer) acciones: editores, formularios, juegos, herramientas de dibujo.
  • La misma acción se lanza desde varios sitios (botón, menú, atajo) o se quiere configurar qué hace cada botón.
  • Las acciones se guardan para ejecutarlas después: colas de tareas, trabajos en segundo plano, reintentos.
  • Se quiere registrar lo que se ha hecho o agrupar varias acciones en una macro.

Cuándo no

  • Si la acción es simple, se ejecuta al momento y nunca se deshace: una lambda o una llamada directa bastan.
  • Si deshacer exige guardar demasiado estado: a veces es mejor guardar instantáneas del objeto (patrón Memento).

Participantes

  1. Comando. La interfaz con ejecutar() y, si hace falta, deshacer().
  2. Comandos concretos. Cada acción (Escribir, Borrar): guarda su receptor, sus datos y lo necesario para deshacerse.
  3. Receptor. El objeto que hace el trabajo de verdad (Editor).
  4. Invocador. Quien ejecuta los comandos sin saber qué hacen (Historial, un botón, una cola) y los guarda si hay que deshacer.

Diagrama de clases

1editor1editor0..*hechos«interface»Comando+ejecutar() : void+deshacer() : voidEscribir-texto : StringBorrar-n : int-borrado : StringEditor+texto : StringBuilderHistorial-hechos : Deque<Comando>+ejecutar(c : Comando) : void+deshacer() : void
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2interface Comando {
3  +ejecutar() : void
4  +deshacer() : void
5}
6class Escribir {
7  -texto : String
8}
9class Borrar {
10  -n : int
11  -borrado : String
12}
13class Editor {
14  +texto : StringBuilder
15}
16class Historial {
17  -hechos : Deque<Comando>
18  +ejecutar(c : Comando) : void
19  +deshacer() : void
20}
21Comando <|.. Escribir
22Comando <|.. Borrar
23Escribir --> "1" Editor : editor
24Borrar --> "1" Editor : editor
25Historial o-- "0..*" Comando : hechos
26@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Un editor con deshacer

Cada acción es un objeto en la pila del historial. Borrar guarda lo que quitó, porque sin eso no podría devolverlo.

Java
1import java.util.*;
2
3/** El receptor: sabe hacer el trabajo, pero no guarda historia. */
4class Editor {
5    final StringBuilder texto = new StringBuilder();
6}
7
8/** El comando: una acción convertida en objeto, que sabe ejecutarse y deshacerse. */
9interface Comando {
10    void ejecutar();
11    void deshacer();
12}
13
14class Escribir implements Comando {
15    private final Editor editor;
16    private final String texto;
17    Escribir(Editor editor, String texto) { this.editor = editor; this.texto = texto; }
18    public void ejecutar() { editor.texto.append(texto); }
19    public void deshacer() { editor.texto.setLength(editor.texto.length() - texto.length()); }
20}
21
22class Borrar implements Comando {
23    private final Editor editor;
24    private final int n;
25    private String borrado = "";                       // lo que quitó, para poder devolverlo
26    Borrar(Editor editor, int n) { this.editor = editor; this.n = n; }
27    public void ejecutar() {
28        int desde = Math.max(0, editor.texto.length() - n);
29        borrado = editor.texto.substring(desde);
30        editor.texto.setLength(desde);
31    }
32    public void deshacer() { editor.texto.append(borrado); }
33}
34
35/** El invocador: ejecuta los comandos y los guarda para deshacerlos en orden inverso. */
36class Historial {
37    private final Deque<Comando> hechos = new ArrayDeque<>();
38    void ejecutar(Comando c) { c.ejecutar(); hechos.push(c); }
39    void deshacer() { if (!hechos.isEmpty()) hechos.pop().deshacer(); }
40}
41
42public class Main {
43    public static void main(String[] args) {
44        Editor editor = new Editor();
45        Historial historial = new Historial();
46        historial.ejecutar(new Escribir(editor, "Hola"));
47        historial.ejecutar(new Escribir(editor, ", mundo"));
48        System.out.println("«" + editor.texto + "»");
49        historial.ejecutar(new Borrar(editor, 5));
50        System.out.println("«" + editor.texto + "»");
51        historial.deshacer();
52        System.out.println("«" + editor.texto + "» (deshecho el borrado)");
53        historial.deshacer();
54        System.out.println("«" + editor.texto + "» (deshecho el segundo texto)");
55    }
56}

Salida al ejecutarlo (la misma en los 5 lenguajes)

«Hola, mundo»
«Hola, »
«Hola, mundo» (deshecho el borrado)
«Hola» (deshecho el segundo texto)

Runnable, colas de tareas y botones

Comandos que ya usas en Java.

Java
1// Runnable es un comando sin deshacer: una acción guardada en un objeto para ejecutarla después
2Runnable guardar = () -> repositorio.guardar(alumno);
3
4// Un pool de hilos es una cola de comandos: se encolan ahora y se ejecutan cuando haya un hilo libre
5ExecutorService pool = Executors.newFixedThreadPool(4);
6for (Alumno a : alumnos) pool.submit(() -> enviarCorreo(a));
7pool.shutdown();
8
9// En una interfaz gráfica, cada botón guarda el comando que ejecutará al pulsarlo,
10// y el mismo comando puede lanzarse también desde un menú o un atajo de teclado
11botonGuardar.addActionListener(e -> guardar.run());
12menuGuardar.addActionListener(e -> guardar.run());

En la práctica

  • Deshacer y rehacer en cualquier editor (texto, imagen, diagramas): una pila de comandos.
  • Runnable y Callable con ExecutorService; las colas de trabajos de Laravel (dispatch(new EnviarCorreo(...))) y de Spring.
  • Las acciones de las interfaces gráficas: Action en Swing, ICommand en WPF y .NET MAUI, los onClick de Android.
  • Las migraciones de bases de datos (cada una sabe aplicarse con up y deshacerse con down) y las transacciones con compensación.

Errores típicos

  • Deshacer aplicando la operación inversa cuando no existe: multiplicar por 0 no se deshace dividiendo. Hay que guardar el estado de antes.
  • Guardar el estado anterior en el constructor en vez de al ejecutar: si entre medias cambia algo, se deshace a un valor equivocado.
  • No vaciar la pila de rehacer al ejecutar una orden nueva: «rehacer» aplicaría algo que ya no tiene sentido.
  • Deshacer una macro en el mismo orden en que se ejecutó: hay que hacerlo al revés.

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. Una calculadora con deshacer

Cada operación de la calculadora es un comando que se apila en el historial. Ya se ejecutan, pero no se pueden deshacer. Completa Operacion para que recuerde el valor de antes y deshacer() lo recupere, y Historial.deshacer() para que deshaga la última operación.

  • Órdenes: sumar x, restar x, multiplicar x, dividir x (escriben op x = valor), deshacer (Deshecho: op x (vuelve a valor) o Nada que deshacer) e historial. Al final, Resultado: valor.
  • Los números se escriben con coma y como mucho dos decimales, sin ceros de más. dividir 0 no se ejecuta: No se puede dividir entre 0.
JavaUna calculadora con deshacerFácil

Ejemplo

Entrada (lo que se escribe por teclado)
sumar 5
multiplicar 3
restar 2.5
historial
deshacer
deshacer
historial
Salida esperada
+ 5 = 5
× 3 = 15
- 2,5 = 12,5
Historial: + 5, × 3, - 2,5
Deshecho: - 2,5 (vuelve a 15)
Deshecho: × 3 (vuelve a 5)
Historial: + 5
Resultado: 5
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
4/** El receptor: la calculadora solo sabe su valor. */
5class Calculadora {
6    double valor = 0;
7}
8
9interface Comando {
10    void ejecutar();
11    void deshacer();
12    String descripcion();
13}
14
15/** Base de las operaciones: recuerda el valor de antes para poder deshacer. */
16abstract class Operacion implements Comando {
17    protected final Calculadora calc;
18    protected final double x;
19    private final String simbolo;
20    private double antes;
21
22    Operacion(Calculadora calc, double x, String simbolo) {
23        this.calc = calc;
24        this.x = x;
25        this.simbolo = simbolo;
26    }
27
28    protected abstract double aplicar(double v);
29
30    public void ejecutar() {
31        antes = calc.valor;                     // lo que hace falta para deshacer, antes de cambiar nada
32        calc.valor = aplicar(calc.valor);
33    }
34
35    public void deshacer() {
36        calc.valor = antes;
37    }
38
39    public String descripcion() { return simbolo + " " + Main.num(x); }
40}
41
42class Sumar extends Operacion {
43    Sumar(Calculadora c, double x) { super(c, x, "+"); }
44    protected double aplicar(double v) { return v + x; }
45}
46
47class Restar extends Operacion {
48    Restar(Calculadora c, double x) { super(c, x, "-"); }
49    protected double aplicar(double v) { return v - x; }
50}
51
52class Multiplicar extends Operacion {
53    Multiplicar(Calculadora c, double x) { super(c, x, "×"); }
54    protected double aplicar(double v) { return v * x; }
55}
56
57class Dividir extends Operacion {
58    Dividir(Calculadora c, double x) {
59        super(c, x, "÷");
60        if (x == 0) throw new ArithmeticException("No se puede dividir entre 0");
61    }
62    protected double aplicar(double v) { return v / x; }
63}
64
65/** El invocador: ejecuta las operaciones y las apila para deshacerlas en orden inverso. */
66class Historial {
67    private final Deque<Comando> hechos = new ArrayDeque<>();
68
69    void ejecutar(Comando c) {
70        c.ejecutar();
71        hechos.push(c);
72    }
73
74    /** Deshace la última operación y devuelve su descripción, o null si no hay nada. */
75    String deshacer() {
76        if (hechos.isEmpty()) return null;
77        Comando c = hechos.pop();
78        c.deshacer();
79        return c.descripcion();
80    }
81
82    List<String> descripciones() {
83        List<String> l = new ArrayList<>();
84        for (Comando c : hechos) l.add(c.descripcion());
85        Collections.reverse(l);                   // la pila tiene la última arriba
86        return l;
87    }
88}
89
90public class Main {
91    static String num(double x) {
92        return BigDecimal.valueOf(x).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString().replace('.', ',');
93    }
94
95    public static void main(String[] args) {
96        Calculadora calc = new Calculadora();
97        Historial historial = new Historial();
98        Scanner sc = new Scanner(System.in);
99        while (sc.hasNextLine()) {
100            String linea = sc.nextLine().trim();
101            if (linea.isEmpty()) continue;
102            String[] p = linea.split("\\s+");
103            try {
104                switch (p[0]) {
105                    case "sumar", "restar", "multiplicar", "dividir" -> {
106                        if (p.length != 2) throw new NumberFormatException();
107                        double x = Double.parseDouble(p[1]);
108                        Comando c = switch (p[0]) {
109                            case "sumar" -> new Sumar(calc, x);
110                            case "restar" -> new Restar(calc, x);
111                            case "multiplicar" -> new Multiplicar(calc, x);
112                            default -> new Dividir(calc, x);
113                        };
114                        historial.ejecutar(c);
115                        System.out.println(c.descripcion() + " = " + num(calc.valor));
116                    }
117                    case "deshacer" -> {
118                        String d = historial.deshacer();
119                        System.out.println(d == null ? "Nada que deshacer" : "Deshecho: " + d + " (vuelve a " + num(calc.valor) + ")");
120                    }
121                    case "historial" -> {
122                        List<String> l = historial.descripciones();
123                        System.out.println(l.isEmpty() ? "Historial vacío" : "Historial: " + String.join(", ", l));
124                    }
125                    default -> System.out.println("Orden no válida: " + linea);
126                }
127            } catch (NumberFormatException e) {
128                System.out.println("Orden no válida: " + linea);
129            } catch (ArithmeticException e) {
130                System.out.println(e.getMessage());
131            }
132        }
133        System.out.println("Resultado: " + num(calc.valor));
134    }
135}

Cada comando lleva consigo lo que necesita para volver atrás; el historial no sabe qué operación es cada uno.

Guardar el estado anterior funciona con cualquier operación, también con las que no tienen inversa.

2. El mando de la casa: macros, deshacer y rehacer

El mando de domótica ejecuta comandos sobre las luces y el termostato. Los comandos sencillos ya saben deshacerse. Completa Historial con dos pilas para deshacer y rehacer, y Macro.deshacer(), para que una macro se deshaga entera como una sola orden.

  • Órdenes: luz nombre on|off, brillo nombre n (1-100, enciende la luz), termo grados, macro noche (apaga todas las luces y pone 18 °C) o macro llegar (salón al 80 % y 21 °C), deshacer, rehacer y estado.
  • Cada orden hecha escribe Hecho: descripción; deshacer y rehacer, Deshecho: … / Rehecho: … o Nada que deshacer / Nada que rehacer. Una orden nueva vacía la pila de rehacer.
JavaEl mando de la casa: macros, deshacer y rehacerMedio

Ejemplo

Entrada (lo que se escribe por teclado)
luz salon on
brillo salon 60
termo 22
estado
deshacer
deshacer
estado
rehacer
estado
Salida esperada
Hecho: encender salon
Hecho: brillo salon 60 %
Hecho: termostato a 22 °C
salon: encendida (60 %) · cocina: apagada · dormitorio: apagada · termostato: 22 °C
Deshecho: termostato a 22 °C
Deshecho: brillo salon 60 %
salon: encendida (100 %) · cocina: apagada · dormitorio: apagada · termostato: 20 °C
Rehecho: brillo salon 60 %
salon: encendida (60 %) · cocina: apagada · dormitorio: apagada · termostato: 20 °C
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
3// ───── Los receptores ─────
4class Luz {
5    final String nombre;
6    boolean encendida = false;
7    int brillo = 100;
8    Luz(String nombre) { this.nombre = nombre; }
9    public String toString() { return nombre + ": " + (encendida ? "encendida (" + brillo + " %)" : "apagada"); }
10}
11
12class Termostato {
13    int grados = 20;
14    public String toString() { return "termostato: " + grados + " °C"; }
15}
16
17interface Comando {
18    void ejecutar();
19    void deshacer();
20    String descripcion();
21}
22
23// ───── Los comandos: cada uno guarda lo que necesita para deshacerse ─────
24class Encender implements Comando {
25    private final Luz luz;
26    private final boolean encender;
27    private boolean antes;
28    Encender(Luz luz, boolean encender) { this.luz = luz; this.encender = encender; }
29    public void ejecutar() { antes = luz.encendida; luz.encendida = encender; }
30    public void deshacer() { luz.encendida = antes; }
31    public String descripcion() { return (encender ? "encender " : "apagar ") + luz.nombre; }
32}
33
34class Brillo implements Comando {
35    private final Luz luz;
36    private final int brillo;
37    private int brilloAntes;
38    private boolean encendidaAntes;
39    Brillo(Luz luz, int brillo) { this.luz = luz; this.brillo = brillo; }
40    public void ejecutar() {
41        brilloAntes = luz.brillo;
42        encendidaAntes = luz.encendida;
43        luz.brillo = brillo;
44        luz.encendida = true;
45    }
46    public void deshacer() { luz.brillo = brilloAntes; luz.encendida = encendidaAntes; }
47    public String descripcion() { return "brillo " + luz.nombre + " " + brillo + " %"; }
48}
49
50class Temperatura implements Comando {
51    private final Termostato t;
52    private final int grados;
53    private int antes;
54    Temperatura(Termostato t, int grados) { this.t = t; this.grados = grados; }
55    public void ejecutar() { antes = t.grados; t.grados = grados; }
56    public void deshacer() { t.grados = antes; }
57    public String descripcion() { return "termostato a " + grados + " °C"; }
58}
59
60/** Un comando hecho de comandos: se ejecuta y se deshace como una sola cosa. */
61class Macro implements Comando {
62    private final String nombre;
63    private final List<Comando> pasos;
64    Macro(String nombre, List<Comando> pasos) { this.nombre = nombre; this.pasos = pasos; }
65
66    public void ejecutar() {
67        for (Comando c : pasos) c.ejecutar();
68    }
69
70    public void deshacer() {
71        for (int i = pasos.size() - 1; i >= 0; i--) pasos.get(i).deshacer();   // al revés
72    }
73
74    public String descripcion() { return "macro " + nombre + " (" + pasos.size() + " órdenes)"; }
75}
76
77/** Deshacer y rehacer con dos pilas. */
78class Historial {
79    private final Deque<Comando> hechos = new ArrayDeque<>();
80    private final Deque<Comando> deshechos = new ArrayDeque<>();
81
82    void ejecutar(Comando c) {
83        c.ejecutar();
84        hechos.push(c);
85        deshechos.clear();                        // una orden nueva hace imposible rehacer lo anterior
86    }
87
88    String deshacer() {
89        if (hechos.isEmpty()) return null;
90        Comando c = hechos.pop();
91        c.deshacer();
92        deshechos.push(c);
93        return c.descripcion();
94    }
95
96    String rehacer() {
97        if (deshechos.isEmpty()) return null;
98        Comando c = deshechos.pop();
99        c.ejecutar();
100        hechos.push(c);
101        return c.descripcion();
102    }
103}
104
105public class Main {
106    public static void main(String[] args) {
107        Map<String, Luz> luces = new LinkedHashMap<>();
108        for (String n : List.of("salon", "cocina", "dormitorio")) luces.put(n, new Luz(n));
109        Termostato termo = new Termostato();
110        Historial historial = new Historial();
111        Scanner sc = new Scanner(System.in);
112        while (sc.hasNextLine()) {
113            String linea = sc.nextLine().trim();
114            if (linea.isEmpty()) continue;
115            String[] p = linea.split("\\s+");
116            try {
117                Comando c = null;
118                switch (p[0] + "/" + p.length) {
119                    case "luz/3" -> {
120                        Luz l = luces.get(p[1]);
121                        if (l == null) { System.out.println("No existe la luz: " + p[1]); continue; }
122                        if (!p[2].equals("on") && !p[2].equals("off")) throw new NumberFormatException();
123                        c = new Encender(l, p[2].equals("on"));
124                    }
125                    case "brillo/3" -> {
126                        Luz l = luces.get(p[1]);
127                        if (l == null) { System.out.println("No existe la luz: " + p[1]); continue; }
128                        int b = Integer.parseInt(p[2]);
129                        if (b < 1 || b > 100) { System.out.println("Brillo no válido: " + b); continue; }
130                        c = new Brillo(l, b);
131                    }
132                    case "termo/2" -> c = new Temperatura(termo, Integer.parseInt(p[1]));
133                    case "macro/2" -> {
134                        List<Comando> pasos = new ArrayList<>();
135                        switch (p[1]) {
136                            case "noche" -> {
137                                for (Luz l : luces.values()) pasos.add(new Encender(l, false));
138                                pasos.add(new Temperatura(termo, 18));
139                            }
140                            case "llegar" -> {
141                                pasos.add(new Brillo(luces.get("salon"), 80));
142                                pasos.add(new Temperatura(termo, 21));
143                            }
144                            default -> { System.out.println("No existe la macro: " + p[1]); continue; }
145                        }
146                        c = new Macro(p[1], pasos);
147                    }
148                    case "deshacer/1" -> {
149                        String d = historial.deshacer();
150                        System.out.println(d == null ? "Nada que deshacer" : "Deshecho: " + d);
151                    }
152                    case "rehacer/1" -> {
153                        String d = historial.rehacer();
154                        System.out.println(d == null ? "Nada que rehacer" : "Rehecho: " + d);
155                    }
156                    case "estado/1" -> {
157                        List<String> partes = new ArrayList<>();
158                        for (Luz l : luces.values()) partes.add(l.toString());
159                        partes.add(termo.toString());
160                        System.out.println(String.join(" · ", partes));
161                    }
162                    default -> throw new NumberFormatException();
163                }
164                if (c != null) {
165                    historial.ejecutar(c);
166                    System.out.println("Hecho: " + c.descripcion());
167                }
168            } catch (NumberFormatException e) {
169                System.out.println("Orden no válida: " + linea);
170            }
171        }
172    }
173}

La macro es un comando hecho de comandos (también es un Composite): el historial la apila, la deshace y la rehace sin saber que dentro hay varias órdenes.

Vaciar la pila de rehacer al ejecutar algo nuevo evita rehacer una orden que se pensó para un estado que ya no existe.

Test

Test: Command

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é convierte en objeto el patrón Command?

  2. 2.¿Qué necesita guardar un comando para poder deshacerse?

  3. 3.¿Qué estructura de datos se usa para deshacer?

  4. 4.¿Qué interfaz de Java es un comando sin deshacer?

  5. 5.Después de deshacer dos órdenes, el usuario hace una nueva. ¿Qué pasa con rehacer?

Relacionado