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
- Comando. La interfaz con
ejecutar()y, si hace falta,deshacer(). - Comandos concretos. Cada acción (
Escribir,Borrar): guarda su receptor, sus datos y lo necesario para deshacerse. - Receptor. El objeto que hace el trabajo de verdad (
Editor). - 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
Ver el diagrama en PlantUML
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@endumlPuedes 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.
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}class Editor:
"""El receptor: sabe hacer el trabajo, pero no guarda historia."""
def __init__(self):
self.texto = ""
class Escribir:
"""Un comando: una acción convertida en objeto, que sabe ejecutarse y deshacerse."""
def __init__(self, editor, texto):
self.editor, self.texto = editor, texto
def ejecutar(self):
self.editor.texto += self.texto
def deshacer(self):
self.editor.texto = self.editor.texto[:len(self.editor.texto) - len(self.texto)]
class Borrar:
def __init__(self, editor, n):
self.editor, self.n = editor, n
self.borrado = "" # lo que quitó, para poder devolverlo
def ejecutar(self):
desde = max(0, len(self.editor.texto) - self.n)
self.borrado = self.editor.texto[desde:]
self.editor.texto = self.editor.texto[:desde]
def deshacer(self):
self.editor.texto += self.borrado
class Historial:
"""El invocador: ejecuta los comandos y los guarda para deshacerlos en orden inverso."""
def __init__(self):
self._hechos = []
def ejecutar(self, c):
c.ejecutar()
self._hechos.append(c)
def deshacer(self):
if self._hechos:
self._hechos.pop().deshacer()
editor = Editor()
historial = Historial()
historial.ejecutar(Escribir(editor, "Hola"))
historial.ejecutar(Escribir(editor, ", mundo"))
print(f"«{editor.texto}»")
historial.ejecutar(Borrar(editor, 5))
print(f"«{editor.texto}»")
historial.deshacer()
print(f"«{editor.texto}» (deshecho el borrado)")
historial.deshacer()
print(f"«{editor.texto}» (deshecho el segundo texto)")/** El receptor: sabe hacer el trabajo, pero no guarda historia. */
class Editor {
texto = "";
}
/** Un comando: una acción convertida en objeto, que sabe ejecutarse y deshacerse. */
class Escribir {
constructor(editor, texto) { this.editor = editor; this.texto = texto; }
ejecutar() { this.editor.texto += this.texto; }
deshacer() { this.editor.texto = this.editor.texto.slice(0, this.editor.texto.length - this.texto.length); }
}
class Borrar {
borrado = ""; // lo que quitó, para poder devolverlo
constructor(editor, n) { this.editor = editor; this.n = n; }
ejecutar() {
const desde = Math.max(0, this.editor.texto.length - this.n);
this.borrado = this.editor.texto.slice(desde);
this.editor.texto = this.editor.texto.slice(0, desde);
}
deshacer() { this.editor.texto += this.borrado; }
}
/** El invocador: ejecuta los comandos y los guarda para deshacerlos en orden inverso. */
class Historial {
#hechos = [];
ejecutar(c) { c.ejecutar(); this.#hechos.push(c); }
deshacer() { this.#hechos.pop()?.deshacer(); }
}
const editor = new Editor();
const historial = new Historial();
historial.ejecutar(new Escribir(editor, "Hola"));
historial.ejecutar(new Escribir(editor, ", mundo"));
console.log(`«${editor.texto}»`);
historial.ejecutar(new Borrar(editor, 5));
console.log(`«${editor.texto}»`);
historial.deshacer();
console.log(`«${editor.texto}» (deshecho el borrado)`);
historial.deshacer();
console.log(`«${editor.texto}» (deshecho el segundo texto)`);using System;
using System.Collections.Generic;
using System.Text;
// El receptor: sabe hacer el trabajo, pero no guarda historia.
class Editor {
public readonly StringBuilder Texto = new();
}
// El comando: una acción convertida en objeto, que sabe ejecutarse y deshacerse.
interface IComando {
void Ejecutar();
void Deshacer();
}
class Escribir : IComando {
private readonly Editor editor;
private readonly string texto;
public Escribir(Editor editor, string texto) { this.editor = editor; this.texto = texto; }
public void Ejecutar() => editor.Texto.Append(texto);
public void Deshacer() => editor.Texto.Length -= texto.Length;
}
class Borrar : IComando {
private readonly Editor editor;
private readonly int n;
private string borrado = ""; // lo que quitó, para poder devolverlo
public Borrar(Editor editor, int n) { this.editor = editor; this.n = n; }
public void Ejecutar() {
int desde = Math.Max(0, editor.Texto.Length - n);
borrado = editor.Texto.ToString(desde, editor.Texto.Length - desde);
editor.Texto.Length = desde;
}
public void Deshacer() => editor.Texto.Append(borrado);
}
// El invocador: ejecuta los comandos y los guarda para deshacerlos en orden inverso.
class Historial {
private readonly Stack<IComando> hechos = new();
public void Ejecutar(IComando c) { c.Ejecutar(); hechos.Push(c); }
public void Deshacer() { if (hechos.Count > 0) hechos.Pop().Deshacer(); }
}
class Program {
static void Main() {
var editor = new Editor();
var historial = new Historial();
historial.Ejecutar(new Escribir(editor, "Hola"));
historial.Ejecutar(new Escribir(editor, ", mundo"));
Console.WriteLine(quot;«{editor.Texto}»");
historial.Ejecutar(new Borrar(editor, 5));
Console.WriteLine(quot;«{editor.Texto}»");
historial.Deshacer();
Console.WriteLine(quot;«{editor.Texto}» (deshecho el borrado)");
historial.Deshacer();
Console.WriteLine(quot;«{editor.Texto}» (deshecho el segundo texto)");
}
}<?php
/** El receptor: sabe hacer el trabajo, pero no guarda historia. */
class Editor {
public string $texto = "";
}
/** El comando: una acción convertida en objeto, que sabe ejecutarse y deshacerse. */
interface Comando {
public function ejecutar(): void;
public function deshacer(): void;
}
class Escribir implements Comando {
public function __construct(private Editor $editor, private string $texto) {}
public function ejecutar(): void { $this->editor->texto .= $this->texto; }
public function deshacer(): void {
$this->editor->texto = mb_substr($this->editor->texto, 0, mb_strlen($this->editor->texto) - mb_strlen($this->texto));
}
}
class Borrar implements Comando {
private string $borrado = ""; // lo que quitó, para poder devolverlo
public function __construct(private Editor $editor, private int $n) {}
public function ejecutar(): void {
$desde = max(0, mb_strlen($this->editor->texto) - $this->n);
$this->borrado = mb_substr($this->editor->texto, $desde);
$this->editor->texto = mb_substr($this->editor->texto, 0, $desde);
}
public function deshacer(): void { $this->editor->texto .= $this->borrado; }
}
/** El invocador: ejecuta los comandos y los guarda para deshacerlos en orden inverso. */
class Historial {
private array $hechos = [];
public function ejecutar(Comando $c): void { $c->ejecutar(); $this->hechos[] = $c; }
public function deshacer(): void { if ($this->hechos) array_pop($this->hechos)->deshacer(); }
}
$editor = new Editor();
$historial = new Historial();
$historial->ejecutar(new Escribir($editor, "Hola"));
$historial->ejecutar(new Escribir($editor, ", mundo"));
echo "«{$editor->texto}»\n";
$historial->ejecutar(new Borrar($editor, 5));
echo "«{$editor->texto}»\n";
$historial->deshacer();
echo "«{$editor->texto}» (deshecho el borrado)\n";
$historial->deshacer();
echo "«{$editor->texto}» (deshecho el segundo texto)\n";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.
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());from concurrent.futures import ThreadPoolExecutor
from functools import partial
# En Python cualquier función es un objeto: guardarla es guardar un comando
guardar = partial(repositorio.guardar, alumno)
# Un pool de hilos es una cola de comandos
with ThreadPoolExecutor(max_workers=4) as pool:
for a in alumnos:
pool.submit(enviar_correo, a)
# En tkinter, cada botón guarda el comando que ejecutará al pulsarlo
tk.Button(ventana, text="Guardar", command=guardar)// En JavaScript las funciones son objetos: guardarlas es guardar comandos
const guardar = () => repositorio.guardar(alumno);
// Una cola de comandos que se ejecutan uno detrás de otro
const cola = [];
cola.push(() => enviarCorreo("ana@correo.es"));
cola.push(guardar);
for (const comando of cola) await comando();
// Cada botón guarda el comando que ejecutará, y un atajo puede lanzar el mismo
botonGuardar.addEventListener("click", guardar);
document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.key === "s") guardar(); });// WPF y .NET MAUI tienen la interfaz ICommand: los botones se enlazan a comandos
public ICommand GuardarCommand => new RelayCommand(() => repositorio.Guardar(alumno));
// <Button Content="Guardar" Command="{Binding GuardarCommand}" />
// Una tarea es un comando que se ejecuta en otro hilo cuando hay uno libre
foreach (var a in alumnos) _ = Task.Run(() => EnviarCorreo(a));<?php
// Laravel: un «job» es un comando que se guarda en una cola y lo ejecuta un trabajador después
class EnviarCorreoBienvenida implements ShouldQueue {
public function __construct(private User $usuario) {}
public function handle(Mailer $correo): void { $correo->to($this->usuario)->send(new Bienvenida()); }
}
EnviarCorreoBienvenida::dispatch($usuario); // se encola ahora, se ejecuta cuando toque
// Las migraciones son comandos que saben deshacerse: up() y down()En la práctica
- Deshacer y rehacer en cualquier editor (texto, imagen, diagramas): una pila de comandos.
RunnableyCallableconExecutorService; las colas de trabajos de Laravel (dispatch(new EnviarCorreo(...))) y de Spring.- Las acciones de las interfaces gráficas:
Actionen Swing,ICommanden WPF y .NET MAUI, losonClickde Android. - Las migraciones de bases de datos (cada una sabe aplicarse con
upy deshacerse condown) 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(escribenop x = valor),deshacer(Deshecho: op x (vuelve a valor)oNada que deshacer) ehistorial. Al final,Resultado: valor. - Los números se escriben con coma y como mucho dos decimales, sin ceros de más.
dividir 0no se ejecuta:No se puede dividir entre 0.
Ejemplo
sumar 5 multiplicar 3 restar 2.5 historial deshacer deshacer historial
+ 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
Ver la solución explicada
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) omacro llegar(salón al 80 % y 21 °C),deshacer,rehaceryestado. - Cada orden hecha escribe
Hecho: descripción;deshaceryrehacer,Deshecho: …/Rehecho: …oNada que deshacer/Nada que rehacer. Una orden nueva vacía la pila de rehacer.
Ejemplo
luz salon on brillo salon 60 termo 22 estado deshacer deshacer estado rehacer estado
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
Ver la solución explicada
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 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.¿Qué convierte en objeto el patrón Command?
2.¿Qué necesita guardar un comando para poder deshacerse?
3.¿Qué estructura de datos se usa para deshacer?
4.¿Qué interfaz de Java es un comando sin deshacer?
5.Después de deshacer dos órdenes, el usuario hace una nueva. ¿Qué pasa con rehacer?