9.5. Comando: elegir la operación

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.

9.5.1. Desacoplar la gestión de eventos con Comando

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 Tareas 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.