Programación Avanzada

Patrones de diseño

El patrón de diseño Acción (Command)

Introducción

Existen ocasiones donde nos interesa realizar acciones sin tener conocimiento de quien es el sujeto que las ejecuta finalmente.

Piensa, por ejemplo, en un sistema de aire acondicionado. No necesito conocer donde está la máquina de aire acondicionado, lo único que necesito conocer es qué botón pulsar para que el aire acondicionado se ponga en marcha o se detenga.

El patrón de diseño Command nos permite ocultar quien llevará a cabo una determinada acción, de lo único de lo que nos preocupamos es de indicar la acción que queremos que se realice, sin saber quien la realizará.

Bibliografía

Contenidos

  1. El telemando universal.
  2. Encapsular llamadas a métodos.
  3. Definición del patrón Command.
  4. Undo y Redo con el patrón Command.
  5. Otros usos del patrón Command.

El telemando universal

Cada vez que llego a casa y quiero ver la televisión, utilizo el mando para controlar la televisión.

Si lo que quiero es ver una película en el DVD, tengo otro mando para controlar el DVD.

Si lo que quiero es escuchar un CD, tengo otro mando para controlar el equipo HiFi.

El aire acondicionado también tiene su propio mando.

Y así con otros dispositivos.

El telemando universal

Me tengo que decidir y construir un mando universal que me sirva para controlar todos los dispositivos.

Me gustaría poder programar el mando universal de tal modo que con él pueda controlar cualquier dispositivo, sea del tipo que sea.

Y si compro nuevos dispositivos que tienen su propio mando a distancia, también quiero poder prescindir de sus mandos a distancia, y poder controlarlos desde el telemando universal.

Un amigo me ha dicho que en MarketMedia venden uno de estos dispositivos, y que además tiene una máquina virtual de Java embebida. Justo lo que necesito.

El telemando universal

¿Qués es lo que quiero?

Obtener respuestas sin saber qué dispositivo las realiza.

Problemas con los que me encuentro:

  • El interfaz de los distintos dispositivos no es uniforme.
  • El usuario, no usará un dispositivo en particular, sino que utilizará el telemando universal.

Veamos cómo es la implementación que da el fabricante para los distintos dispositivos.

El telemando universal

Este es el aspecto de la clase que implementa el mando de la TV:

public class TV {
    public TV() {
        super();
    }
    
    public void enciende() {
        System.out.println("Encendiendo la televisión");
    }

    public void apaga() {
        System.out.println("Apagando la televisión");
    }
}

El telemando universal

Y este es el aspecto de la clase que implementa el mando del DVD:

public class DVD {
    public DVD() {
    }

    public void play() {
        System.out.println("Reproduciendo la película");
    }

    public void stop() {
        System.out.println("Deteniendo la reproducción de la película");
    }
}

El telemando universal

La interfaz pública del mando que controla la TV es:

public void enciende() {...}
public void apaga() {...}

Y para el DVD la interfaz pública es:

public void play() {...}
public void stop() {...}

Y no podemos cambiarlas, cada fabricante ha decidido que son esas.

Además, TV es una clase y DVD otra. Si quiero utilizar sus método, primero necesito obtener una instancia de esas clases.

El telemando universal

Intentemos una primera solución con las siguientes hipótesis:

  • Nuestro mando conoce todos los dispositivos que controla.
  • En todos los casos podemos encenderlos o apagarlos.
public class PrimerTelemando {
    private TV tv = new TV();
    private DVD dvd = new DVD();
    public void enciende(TipoDispositivo tipoDispositivo) {
        switch(tipoDispositivo) {
            case TV:
                tv.enciende();
                break;
            case DVD:
                dvd.play();
                break;
        }
    }

El telemando universal

Continuación:

public void apaga(TipoDispositivo tipoDispositivo) {
    switch(tipoDispositivo) {
        case TV:
            tv.apaga();
            break;
        case DVD:
            dvd.stop();
            break;
    }
}

Detalle de implementación: he utilizado una enumeración TipoDispositivo para seleccionar el tipo de dispositivo que quiero usar.

El telemando universal

Probemos nuestra primera solución:

PrimerTelemando primerTelemando = new PrimerTelemando();
primerTelemando.apaga(TipoDispositivo.TV);
primerTelemando.enciende(TipoDispositivo.DVD);

El resultado que obtenemos es:

Apagando la televisión
Reproduciendo la película

El telemando universal

La solución que acabamos de encontrar funciona, pero tiene algunos problemas si necesitamos hacer cambios:

  • Si queremos controlar un nuevo dispositivo, tenemos que modificar la implementación del telemando.
  • Si los nuevos dispotivos tienen más acciones disponibles, también tenemos que modificar la implementación del telemando.

El telemando universal

Para resolver el primer problema, se nos puede ocurrir crear una interface con las acciones que podemos llevar a cabo sobre cada uno de los dispositivos, y una nueva clase que recubra cada uno de los dispositivos, así podremos verlos a todos con una única interfaz pública y los podremos añadir todos a una colección única, por ejemplo.

No parece mala solución, el problema es si existen dispositivos sobre los que se pueden hacer acciones que no se pueden llevar a cabo sobre otros, por ejemplo, subir el volumen es perfecto para el equipo HiFi, pero no tiene sentido para el aire acondicionado.

Analicemos con algo más de detalle la solución.

Encapsular llamadas a métodos

Si observamos con detalle este fragmento de código:

public void enciende(TipoDispositivo tipoDispositivo) {
    switch(tipoDispositivo) {
        case TV:
            tv.enciende();
            break;
        case DVD:
            dvd.play();
            break;
    }
}

Nos daremos cuenta de que lo que varía de un dispositivo a otro es que estamos usando en cada caso un método que dependen del dispositivo.

Lo que varía es la llamada a un método.

Encapsular llamadas a métodos

¿Qué hemos estado haciendo en los patrones anteriores?

Encapsular lo que varía dentro de una clase.

Ahora tenemos que «encapsular la llamada a un método».

Hipótesis:

  1. Todas las llamadas a un método se tienen que ver del mismo modo, como la misma cosa.
  2. Cada llamada actua sobre un dispositivo distinto.
  3. Puede ocurrir que un dispositivo ofrezca una interfaz pública con más métodos que otro dispositivo, o símplemente distintos métodos.

Encapsular llamadas a métodos

Programemos las hipótesis:

  1. Una interface (la herencia en general) me permite ver como el mismo tipo de datos, tipos de datos concretos con implementaciones distintas. Puedo, por ejemplo, unificar todas las acciones bajo una misma interface.
  2. La llamada a un método la puedo encapsular en una clase que tenga una referencia al dispositivo sobre el que actúa.
  3. Tendré una clase distinta por cada acción que puedo llevar a cabo sobre un dispositivo.

Encapsular llamadas a métodos

Vamos por pasos. Implementemos primero la interface que me permite ver cualquier acción sobre cualquier dispositivo como la misma cosa.

public interface Accion {
    void ejecutaAccion();
}

Vaya, de momento es muy sencilla, demos el siguiente paso.

Encapsular llamadas a métodos

Las acciones concretas se llevan a cabo sobre dispositivos concretos, por ejemplo encender la televisión. Dicho de otro modo, cada acción, implementada como una clase concreta, conoce el dispositivo final que realiza la acción.

public class AccionEnciendeTV implements Accion {
    private TV tv;
    
    public AccionEnciendeTV(TV tv) {
        super();
        this.tv = tv;
    }
    @Override
    public void ejecutaAccion() {
        tv.enciende();
    }
}

Encapsular llamadas a métodos

Y si lo que quiero hacer es llamar al método que enciende el DVD... lo encapsulo en una nueva clase:

public class AccionEnciendeDVD implements Accion {
    private DVD dvd;
    
    public AccionEnciendeDVD(DVD dvd) {
        super();
        this.dvd = dvd;
    }
    @Override
    public void ejecutaAccion() {
        dvd.play();
    }
}

Encapsular llamadas a métodos

Con todo esto, ¿cómo queda la implementación del telemando?

public class Telemando {
    private Map< TipoAccion, Accion > acciones = new HashMap< TipoAccion, Accion >();

    public void ejecutaAccion(TipoAccion tipoAccion) {
        acciones.get(tipoAccion).ejecutaAccion();
    }
    
    public void setAccion(TipoAccion tipoAccion, Accion accion) {
        acciones.put(tipoAccion, accion);
    }
}

El telemando ni conoce los dispositivos que controla, ni mucho menos sabe qué se puede hacer con ellos. Simplemente ejecuta acciones.

Encapsular llamadas a métodos

Detalle de implementación, la clase Telemando tiene parametrizadas las acciones que puede realizar.

Para realizar una acción, no se le pasa la acción concreta al Telemando, sino sólo el tipo de la acción como un elemento de una enumeración (TipoAccion).

public enum TipoAccion {
    ENCENDER_TV, ENCENDER_DVD, ENCENDER_HIFI, ENCENDER_AA,
    APAGAR_TV, APAGAR_DVD, APAGAR_HIFI, APAGAR_AA;
}

¿Podría, en algún caso, tener sentido enviar al telemando la acción concreta, en vez de un parámetro (enumeración) que indica la acción?

Piénsalo, y lo contestamos al final de la presentación.

Encapsular llamadas a métodos

Vale, pero de algún modo tendremos que conectar las acciones con los dispositivos que las realizan. Sí, aquí lo tienes.

public class CargadorTelemando {
    public void cargaTelemando(Telemando telemando) {
        TV tv = new TV();
        telemando.setAccion(TipoAccion.ENCENDER_TV, new AccionEnciendeTV(tv));
        telemando.setAccion(TipoAccion.APAGAR_TV, new AccionApagaTV(tv));
        DVD dvd = new DVD();
        telemando.setAccion(TipoAccion.ENCENDER_DVD, new AccionEnciendeDVD(dvd));
        telemando.setAccion(TipoAccion.APAGAR_DVD, new AccionApagaDVD(dvd));
    }
}

Encapsular llamadas a métodos

Vamos a probarlo.

CargadorTelemando cargadorTelemando = new CargadorTelemando();
Telemando telemando = new Telemando();
cargadorTelemando.cargaTelemando(telemando);
telemando.ejecutaAccion(TipoAccion.ENCENDER_TV);
telemando.ejecutaAccion(TipoAccion.ENCENDER_DVD);
telemando.ejecutaAccion(TipoAccion.APAGAR_TV);
telemando.ejecutaAccion(TipoAccion.APAGAR_DVD);
Encendiendo la televisión
Reproduciendo la película
Apagando la televisión
Deteniendo la reproducción de la película

Encapsular llamadas a métodos

Recapitulemos:

  1. Tenemos varios dispositivos.
  2. Sobre cada dispositivo podemos realizar diferentes acciones.
  3. Queremos realizar las acciones sobre cualquier dispositivo desde el mismo telemando.

Solución:

  1. Hemos definido una interface para poder ver las distintas acciones como la misma cosa.
  2. Cada acción concreta se realiza sobre un dispositivo concreto.
  3. El telemando sólo ejecuta acciones, no sabe cual es el dispositivo concreto que responde a cada acción.

Hemos encapsulado llamadas a métodos como objetos.

Definición del patrón Command

Encapsula una petición en un objeto, permitiendo así parametrizara los clientes con diferentes peticiones, hacer cola, o llevar un registro de las peticiones y poder deshacer las operaciones.
Patrones de Diseño
Erich Gamma et al.

Definición del patrón Command

El diagrama UML de Command:

Definición del patrón Command

El caso particular del telemando:

Definición del patrón Command

Ventajas:

  1. La clase que lleva a cabo la acción está desacoplada de la clase que solicita realizar la acción.
  2. Las acciones se pueden extender, para definir nuevas acciones.
  3. Se pueden empaquetar varias acciones simples en una macro-acción.
  4. Es fácil añadir nuevas acciones al sistema.

Undo y Redo con el patrón Command

Vamos a ver una aplicación muy interesante del patrón Command.

¿Alguna vez te has preguntado cómo recuerdan los programas las acciones que han hecho para poder deshacerlas si lo necesitamos?

Hay programas que incluso pueden volver a hacer lo que han deshecho. El editor de Eclipse en un ejemplo, puedo borrar y escribir, y cuando deshago lo último que he hecho, se borra lo que he escrito y se escribe lo que había borrado. Y puedo rehacer lo que había deshecho, e ir para adelante o para atrás...

Veamos cómo hacerlo con el patrón Command.

Undo y Redo con el patrón Command

Empecemos, de nuevo, con la definición del interface Accion. Vamos a añadir un nuevo método en la interfaz, para que cada acción se pueda deshacer.

public interface Accion {
    void ejecutaAccion();
    void undo();
}

Así, además de realizar acciones, las podré deshacer.

Undo y Redo con el patrón Command

Cada acción concreta sabe deshacerse. Lo contrario de encender la TV es apagar la TV:

public class AccionEnciendeTV implements Accion {
    private TV tv;
    public AccionEnciendeTV(TV tv) {
        super();
        this.tv = tv;
    }
    @Override
    public void ejecutaAccion() {
        tv.enciende();
    }
    @Override
    public void undo() {
        tv.apaga();
    }
}

Undo y Redo con el patrón Command

Y lo mismo para el DVD:

public class AccionEnciendeDVD implements Accion {
    private DVD dvd;
    public AccionEnciendeDVD(DVD dvd) {
        super();
        this.dvd = dvd;
    }
    @Override
    public void ejecutaAccion() {
        dvd.play();
    }
    @Override
    public void undo() {
        dvd.stop();
    }
}

Undo y Redo con el patrón Command

Finalmente, debemos implementar el telemando con memoria. El telemando tiene dos pilas de acciones, una para añadir las que se pueden deshacer (undo), y otra para rehacer lo deshecho (redo).

public class TelemandoUndoRedo extends Telemando {
    private Deque< TipoAccion > undo = new ArrayDeque< TipoAccion >();
    private Deque< TipoAccion > redo = new ArrayDeque< TipoAccion >();
    
    public TelemandoUndoRedo() {
        super();
    }

    @Override
    public void ejecutaAccion(TipoAccion tipoAccion) {
        super.ejecutaAccion(tipoAccion);
        undo.push(tipoAccion);
    } ....

Undo y Redo con el patrón Command

    public void undoAccion() {
        if(undo.isEmpty() == false) {
            TipoAccion tipoAccion = undo.pop();
            acciones.get(tipoAccion).undo();
            redo.push(tipoAccion);
        } else System.out.println("La pila undo está vacía.");
    }
    public void redoAccion() {
        if(redo.isEmpty() == false) {
            TipoAccion tipoAccion = redo.pop();
            ejecutaAccion(tipoAccion);
        } else System.out.println("La pila redo está vacía.");
    }
}

Nota: he modificado el mapa acciones como protected.

Undo y Redo con el patrón Command

Probemos nuestro nuevo telemando:

CargadorTelemando cargadorTelemando = new CargadorTelemando();
TelemandoUndoRedo telemando = new TelemandoUndoRedo();
cargadorTelemando.cargaTelemando(telemando);
telemando.ejecutaAccion(TipoAccion.ENCENDER_TV);
telemando.ejecutaAccion(TipoAccion.ENCENDER_DVD);
telemando.undoAccion();
telemando.undoAccion();
telemando.redoAccion();
telemando.redoAccion();
Encendiendo la televisión
Reproduciendo la película
Deteniendo la reproducción de la película
Apagando la televisión
Encendiendo la televisión
Reproduciendo la película

Undo y Redo con el patrón Command

Detalles de implementación:

  • Las acciones no tienen estado, son binarias, encender/apagar.
  • El nuevo telemando es hijo del anterior.

Posibles mejoras:

  • Hay acciones que pueden ser superfluas, por ejemplo, encender la televisión no debería hacer nada si ya está encendida. Lo mismo para apagarla.
  • Hay acciones que inherentemente tienen estado, por ejemplo, subir el volumen, si deshacemos esta acción, el volumen debería quedar con la misma intensidad que la última vez.

¿Te animas ha intentarlo como un ejercicio?

Otros usos del patrón Command

Hemos visto cómo podemos implementar undo/redo con la ayuda del patrón Command.

Command ofrece otras posibilidades:

  • Macros: se pueden construir acciones «macro» como la unión de varias acciones simples, por ejemplo una misma acción podría encender el HiFi y el aire acondicionado, al llegar a casa en verano me vendría muy bien.
  • Ejecución tardía: nuestra solución ejecuta las acciones en el mismo momento de solicitarlas, pero las acciones se podrían ir añadiendo a una cola para ejecutarse en otro momento.
  • Concurrencia: el acceso al recurso puede estar compartido por varios clientes, quienes encolarían sus peticiones y serían atendidas en orden de llegada, por ejemplo.
  • Otros usos que a tí se te ocurran.

Resumen

El patrón Command hace de las llamadas a métodos ciudadanos de primera clase en POO.

Una acción encapsula la llamada a un método.

La ejecución al método se realiza a través de un invocador, quien no conoce ni el objeto concreto que atenderá la llamada, ni el método concreto que se llamará.

Este patrón desacopla las llamadas a métodos de los clientes de esos métodos.

Hemos visto un caso práctico, muy interesante, de cómo implementar undo/redo con el patrón Command.

Recursos en Internet