El patrón Comando
es estructuralmente muy sencillo,
pero puede tener un impacto importante en el desacoplamiento (y, por
ende, en la limpieza) de su código.
En "Advanced C++: Programming Styles And Idioms (Addison Wesley,
1992)", Jim Coplien acuña el término functor
, que es un
objeto cuyo único propósito es encapsular una función (dado
que functor
tiene su significado en matemáticas, usaremos el
término "objeto función", que es más explícito). El quid está en
desacoplar la elección de la función que hay que llamar del sitio
donde se llama a dicha función.
Este término se menciona en el GoF, pero no se usa. Sin embargo, el concepto de "objeto función" se repite en numerosos patrones del libro.
Un Comando
es un objeto función en su estado más puro:
una función que tiene un objeto. Al envolver una función en un objeto,
puede pasarla a otras funciones u objetos como parámetro, para
decirles que realicen esta operación concreta mientras llevan a cabo
su petición. Se podría decir que un Comando
es un Mensajero
que lleva un comportamiento.
//: C10:CommandPattern.cpp #include <iostream> #include <vector> using namespace std; class Command { public: virtual void execute() = 0; }; class Hello : public Command { public: void execute() { cout << "Hello "; } }; class World : public Command { public: void execute() { cout << "World! "; } }; class IAm : public Command { public: void execute() { cout << "I'm the command pattern!"; } }; // An object that holds commands: class Macro { vector<Command*> commands; public: void add(Command* c) { commands.push_back(c); } void run() { vector<Command*>::iterator it = commands.begin(); while(it != commands.end()) (*it++)->execute(); } }; int main() { Macro macro; macro.add(new Hello); macro.add(new World); macro.add(new IAm); macro.run(); } ///:~
Listado 9.12. C10/CommandPattern.cpp
El punto principal del Comando
es permitirle dar una
acción deseada a una función u objeto. En el ejemplo anterior, esto
provee una manera de encolar un conjunto de acciones que se deben
ejecutar colectivamente. Aquí, puede crear dinámicamente nuevos
comportamientos, algo que puede hacer normalmente escribiendo nuevo
código, pero en el ejemplo anterior podría hacerse interpretando
un script
(vea el patrón Intérprete si lo que necesita hacer
se vuelve demasiado complicado).
Según el GoF, los Comandos
son un sustituto orientado a
objetos de las retrollamadas
(callbacks
). [141] Sin embargo, pensamos que la palabra
"retro" es una parte esencial del concepto de retrollamada -una
retrollamada retorna al creador de la misma. Por otro lado, un
objeto Comando
, simplemente se crea y se entrega a
alguna función u objeto, y no se permanece conectado de por vida
al objecto Comando
.
Un ejemplo habitual del patrón Comando
es la implementación
de la funcionalidad de "deshacer" en una aplicación. Cada vez que el
usuario realiza una operación, se coloca el correspondiente objeto
Comando de deshacer en una cola. Cada objeto Comando que se ejecuta
guarda el estado del programa en el paso anterior.
Como se verá en el siguiente capítulo, una de las razones para emplear técnicas de concurrencia es facilitar la gestión de la programación dirigida por eventos, donde los eventos pueden aparecer en el programa de forma impredecible. Por ejemplo, un usuario que pulsa un botón de "Salir" mientras se está realizando una operación espera que el programa responda rápidamente.
Un motivo para usar concurrencia es que previene el aclopamiento entre los bloques del código. Es decir, si está ejecutando un hilo aparte para vigilar el botónde salida, las operaciones normales de su programa no necesitan saber nada sobre el botón ni sobe ninguna de las demás operaciones que se están vigilando.
Sin embargo, una vez que comprenda que el quiz está en el acoplamiento, puede evitarlo usando el patrón Comando. Cada operación normal debe llamar periódicamente a una función para que compruebe el estado de los eventos, pero con el patrón Comando, estas operaciones normales no tienen porqué saber nada sobre lo que están comprobando, y por lo tanto, están desacopladas del código de manejo de eventos:
//: C10:MulticastCommand.cpp {RunByHand} // Decoupling event management with the Command pattern. #include <iostream> #include <vector> #include <string> #include <ctime> #include <cstdlib> using namespace std; // Framework for running tasks: class Task { public: virtual void operation() = 0; }; class TaskRunner { static vector<Task*> tasks; TaskRunner() {} // Make it a Singleton TaskRunner& operator=(TaskRunner&); // Disallowed TaskRunner(const TaskRunner&); // Disallowed static TaskRunner tr; public: static void add(Task& t) { tasks.push_back(&t); } static void run() { vector<Task*>::iterator it = tasks.begin(); while(it != tasks.end()) (*it++)->operation(); } }; TaskRunner TaskRunner::tr; vector<Task*> TaskRunner::tasks; class EventSimulator { clock_t creation; clock_t delay; public: EventSimulator() : creation(clock()) { delay = CLOCKS_PER_SEC/4 * (rand() % 20 + 1); cout << "delay = " << delay << endl; } bool fired() { return clock() > creation + delay; } }; // Something that can produce asynchronous events: class Button { bool pressed; string id; EventSimulator e; // For demonstration public: Button(string name) : pressed(false), id(name) {} void press() { pressed = true; } bool isPressed() { if(e.fired()) press(); // Simulate the event return pressed; } friend ostream& operator<<(ostream& os, const Button& b) { return os << b.id; } }; // The Command object class CheckButton : public Task { Button& button; bool handled; public: CheckButton(Button & b) : button(b), handled(false) {} void operation() { if(button.isPressed() && !handled) { cout << button << " pressed" << endl; handled = true; } } }; // The procedures that perform the main processing. These // need to be occasionally "interrupted" in order to // check the state of the buttons or other events: void procedure1() { // Perform procedure1 operations here. // ... TaskRunner::run(); // Check all events } void procedure2() { // Perform procedure2 operations here. // ... TaskRunner::run(); // Check all events } void procedure3() { // Perform procedure3 operations here. // ... TaskRunner::run(); // Check all events } int main() { srand(time(0)); // Randomize Button b1("Button 1"), b2("Button 2"), b3("Button 3"); CheckButton cb1(b1), cb2(b2), cb3(b3); TaskRunner::add(cb1); TaskRunner::add(cb2); TaskRunner::add(cb3); cout << "Control-C to exit" << endl; while(true) { procedure1(); procedure2(); procedure3(); } } ///:~
Listado 9.13. C10/MulticastCommand.cpp
Aquí, el objeto Comando está representado por Tarea
s
ejecutadas por el
Singleton TaskRunner
. EventSimulator
crea un
retraso aleatorio, de modo que si se llama periódicamente a la
función fired() el resultado cambiará de false
a true
en algún momento aleatorio. Los
objetos EventSimulator
se utilizan dentro de los
Botones para simular que ocurre un evento de usuario en un
momento impredecible. CheckButton
es la
implementación de la Tarea que es comprobada periódicamente
por todo el código "normal" del progama. Puede ver cómo ocurre
al final de procedure1(), procedure2() y procedure3().
Aunque esto requiere un poco más de razonamiento para establecerlo, verá en el Capítulo 11 que utilizar hilos requiere mucho pensamiento y cuidado para prevenir las muchas dificultades inhenerentes a la programación concurrente, por lo que la solución más simple puede ser preferible. También puede crear un esquema de hilos muy simple moviendo las llamadas a TaskRunner::run() a un objeto temporizador multi-hilo. Al hacer esto, se elimina todo el acoplamiento entre las operaciones "normales" (los "procedures" en el ejemplo anterior) y el código de eventos.