Patrón Command
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 96 de 104.
El patrón Command encapsula una solicitud como un objeto, lo que permite parametrizar clientes con diferentes solicitudes, encolar operaciones o soportar la funcionalidad de deshacer. Desacopla el objeto que invoca una operación de aquel que sabe cómo realizarla.
El patrón tiene cuatro componentes clave: una interfaz Command con un método execute(), Concrete Commands que implementan acciones específicas, un Receiver que realiza el trabajo real y un Invoker que activa los comandos:
#include <iostream>
#include <memory>
#include <vector>
// Receptor - sabe cómo realizar operaciones
class Light {
public:
void turnOn() { std::cout << "Light is ON\n"; }
void turnOff() { std::cout << "Light is OFF\n"; }
};
// Interfaz de comando
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// Comandos concretos
class LightOnCommand : public Command {
Light& light;
public:
LightOnCommand(Light& l) : light(l) {}
void execute() override { light.turnOn(); }
void undo() override { light.turnOff(); }
};
class LightOffCommand : public Command {
Light& light;
public:
LightOffCommand(Light& l) : light(l) {}
void execute() override { light.turnOff(); }
void undo() override { light.turnOn(); }
};
// Invocador
class RemoteControl {
std::vector<std::unique_ptr<Command>> history;
public:
void pressButton(std::unique_ptr<Command> cmd) {
cmd->execute();
history.push_back(std::move(cmd));
}
void pressUndo() {
if (!history.empty()) {
history.back()->undo();
history.pop_back();
}
}
};Cada comando almacena una referencia a su receptor y llama al método apropiado cuando se ejecuta. El invocador no sabe qué acción ocurrirá; simplemente llama a execute(). Al almacenar los comandos en un historial, puedes implementar fácilmente la función de deshacer llamando al método undo() de cada comando.
Usa Command cuando necesites encolar operaciones, implementar deshacer/rehacer o desacoplar el emisor de una solicitud de su manejador.
Desafío
FácilVamos a construir un Editor de Texto con funcionalidad de deshacer utilizando el patrón Command. Crearás un sistema donde las operaciones de edición (como insertar y eliminar texto) se encapsulan como objetos de comando, permitiendo a los usuarios ejecutar acciones y luego deshacerlas en orden inverso, tal como en un editor de texto real.
Organizarás tu código en cuatro archivos:
TextDocument.h: Crea la clase receptora que contiene y manipula el contenido de texto real.Tu clase
TextDocumentdebe almacenar el contenido del documento como un string. Implementa estos métodos:insertText(const std::string& text)— añade texto al final del contenidodeleteText(int count)— elimina los últimoscountcaracteres del contenido (no hace nada si count excede la longitud del contenido)getContent()— devuelve el contenido actual como una referencia constante
Command.h: Define la interfaz de comando y las clases de comando concretas.Crea una clase abstracta
Commandcon métodos virtuales purosexecute()yundo(), además de un destructor virtual.Luego implementa dos comandos concretos:
InsertCommand— toma una referencia a unTextDocumenty un string para insertar. Cuando se ejecuta, inserta el texto. Cuando se deshace, elimina el mismo número de caracteres que fueron insertados.DeleteCommand— toma una referencia a unTextDocumenty un recuento de caracteres a eliminar. Necesita almacenar el texto eliminado para poder restaurarlo al deshacer. Cuando se ejecuta, elimina caracteres del final. Cuando se deshace, inserta el texto almacenado de nuevo.
TextEditor.h: Crea el invocador que gestiona la ejecución de comandos y el historial.Tu clase
TextEditordebe mantener una referencia a unTextDocumenty mantener un historial de comandos ejecutados usando un vector deunique_ptr<Command>. Implementa:executeCommand(std::unique_ptr<Command> cmd)— ejecuta el comando y lo añade al historialundo()— deshace el comando más reciente y lo elimina del historial (no hace nada si el historial está vacío)showContent()— imprime el contenido actual del documento, o[empty]si el contenido está vacío
main.cpp: Demuestra el patrón Command con funcionalidad de deshacer.Lee tres entradas:
- Primer texto a insertar (string)
- Segundo texto a insertar (string)
- Número de caracteres a eliminar (entero)
Crea un
TextDocumenty unTextEditor. Luego realiza estas operaciones en orden, mostrando el contenido después de cada paso:- Inserta el primer texto, luego muestra el contenido
- Inserta el segundo texto, luego muestra el contenido
- Elimina el número especificado de caracteres, luego muestra el contenido
- Deshaz una vez, luego muestra el contenido
- Deshaz una vez más, luego muestra el contenido
Por ejemplo, con las entradas Hello, World, y 3:
Hello
Hello World
Hello Wo
Hello World
HelloCon las entradas Code, Editor, y 6:
Code
CodeEditor
Code
CodeEditor
CodeObserva cómo cada comando sabe cómo revertirse a sí mismo. El DeleteCommand recuerda lo que eliminó para poder restaurarlo, y el InsertCommand sabe exactamente cuántos caracteres eliminar al deshacer. Este es el poder del patrón Command: las operaciones se convierten en objetos de primera clase que pueden ser almacenados, ejecutados y revertidos.
Hoja de referencia
El patrón Command encapsula una solicitud como un objeto, lo que le permite parametrizar clientes con diferentes solicitudes, encolar operaciones o admitir la funcionalidad de deshacer. Desacopla el objeto que invoca una operación del que sabe cómo realizarla.
El patrón tiene cuatro componentes clave:
- Interfaz Command con un método
execute() - Commands concretos que implementan acciones específicas
- Receiver (Receptor) que realiza el trabajo real
- Invoker (Invocador) que activa los comandos
Estructura básica:
// Receiver - sabe cómo realizar las operaciones
class Light {
public:
void turnOn() { std::cout << "Light is ON\n"; }
void turnOff() { std::cout << "Light is OFF\n"; }
};
// Interfaz Command
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// Command concreto
class LightOnCommand : public Command {
Light& light;
public:
LightOnCommand(Light& l) : light(l) {}
void execute() override { light.turnOn(); }
void undo() override { light.turnOff(); }
};
// Invoker
class RemoteControl {
std::vector<std::unique_ptr<Command>> history;
public:
void pressButton(std::unique_ptr<Command> cmd) {
cmd->execute();
history.push_back(std::move(cmd));
}
void pressUndo() {
if (!history.empty()) {
history.back()->undo();
history.pop_back();
}
}
};Cada comando almacena una referencia a su receptor y llama al método apropiado cuando se ejecuta. El invocador no sabe qué acción ocurrirá; simplemente llama a execute(). Al almacenar los comandos en un historial, puede implementar la función de deshacer llamando al método undo() de cada comando.
Utilice Command cuando necesite encolar operaciones, implementar deshacer/rehacer o desacoplar al remitente de una solicitud de su manejador.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"
int main() {
// Leer entradas
std::string firstText;
std::string secondText;
int deleteCount;
std::getline(std::cin, firstText);
std::getline(std::cin, secondText);
std::cin >> deleteCount;
// TODO: Crear un TextDocument y un TextEditor
// TODO: Realizar las operaciones en orden, mostrando el contenido después de cada una:
// 1. Insertar el primer texto, luego mostrar el contenido
// 2. Insertar el segundo texto, luego mostrar el contenido
// 3. Eliminar el número especificado de caracteres, luego mostrar el contenido
// 4. Deshacer una vez, luego mostrar el contenido
// 5. Deshacer una vez más, luego mostrar el contenido
// Hint: Usar std::make_unique<InsertCommand>(...) y
// std::make_unique<DeleteCommand>(...) para crear comandos
return 0;
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico