Consejo 68 de Java: aprenda a implementar el patrón de comando en Java

Los patrones de diseño no solo aceleran la fase de diseño de un proyecto orientado a objetos (OO), sino que también aumentan la productividad del equipo de desarrollo y la calidad del software. Un patrón de comando es un patrón de comportamiento de objeto que nos permite lograr un desacoplamiento completo entre el emisor y el receptor. (Un remitente es un objeto que invoca una operación, y un receptor es un objeto que recibe la solicitud para ejecutar una determinada operación. Con el desacoplamiento, el remitente no tiene conocimiento de la Receiverinterfaz de.) El término solicitudaquí se refiere al comando que se va a ejecutar. El patrón Command también nos permite variar cuándo y cómo se cumple una solicitud. Por lo tanto, un patrón de comando nos proporciona flexibilidad y extensibilidad.

En lenguajes de programación como C, los punteros de función se utilizan para eliminar declaraciones de cambio gigantes. (Consulte el "Consejo 30 de Java: polimorfismo y Java" para obtener una descripción más detallada). Dado que Java no tiene punteros de función, podemos usar el patrón Command para implementar devoluciones de llamada. Verá esto en acción en el primer ejemplo de código a continuación, llamado TestCommand.java.

Los desarrolladores acostumbrados a utilizar punteros de función en otro idioma pueden verse tentados a utilizar los Methodobjetos de la API de Reflection de la misma forma. Por ejemplo, en su artículo "Reflexión de Java", Paul Tremblett le muestra cómo usar Reflection para implementar transacciones sin usar declaraciones de cambio. Me he resistido a esta tentación, ya que Sun desaconseja el uso de Reflection API cuando otras herramientas más naturales para el lenguaje de programación Java serán suficientes. (Consulte Recursos para obtener enlaces al artículo de Tremblett y la página del tutorial de Sun's Reflection). Su programa será más fácil de depurar y mantener si no usa Methodobjetos. En su lugar, debe definir una interfaz e implementarla en las clases que realizan la acción necesaria.

Por lo tanto, le sugiero que utilice el patrón Command combinado con el mecanismo de enlace y carga dinámica de Java para implementar punteros de función. (Para obtener detalles sobre el mecanismo de enlace y carga dinámica de Java, consulte "The Java Language Environment - A White Paper" de James Gosling y Henry McGilton, que se incluye en Recursos).

Siguiendo la sugerencia anterior, aprovechamos el polimorfismo proporcionado por la aplicación de un patrón de comando para eliminar declaraciones de cambio gigantes, lo que resulta en sistemas extensibles. También aprovechamos los mecanismos de enlace y carga dinámica únicos de Java para construir un sistema dinámico y extensible dinámicamente. Esto se ilustra en el segundo ejemplo de código a continuación, llamado TestTransactionCommand.java.

El patrón Command convierte la solicitud en sí misma en un objeto. Este objeto puede almacenarse y pasarse como otros objetos. La clave de este patrón es una Commandinterfaz, que declara una interfaz para ejecutar operaciones. En su forma más simple, esta interfaz incluye una executeoperación abstracta . Cada Commandclase concreta especifica un par receptor-acción almacenando Receivercomo variable de instancia. Proporciona diferentes implementaciones del execute()método para invocar la solicitud. El Receivertiene los conocimientos necesarios para llevar a cabo la solicitud.

La Figura 1 a continuación muestra la Switch- una agregación de Commandobjetos. Tiene flipUp()y flipDown()operaciones en su interfaz. Switchse llama invocador porque invoca la operación de ejecución en la interfaz de comandos.

El comando concreto LightOnCommand, implementa el executefuncionamiento de la interfaz de comandos. Tiene el conocimiento para llamar a la Receiveroperación del objeto apropiado . Actúa como un adaptador en este caso. Por el término adaptador, me refiero a que el Commandobjeto concreto es un conector simple, que conecta el Invokery el Receivercon diferentes interfaces.

El cliente crea una instancia del Invoker, el Receiver, y los objetos de comando concreto.

La Figura 2, el diagrama de secuencia, muestra las interacciones entre los objetos. Ilustra cómo se Commanddesacopla el Invokerde Receiver(y la solicitud que realiza). El cliente crea un comando concreto parametrizando su constructor con el apropiado Receiver. Luego almacena el Commanden formato Invoker. El Invokervuelve a llamar el comando concreto, que tiene el conocimiento para realizar la Action()operación deseada .

El cliente (programa principal en el listado) crea un Commandobjeto concreto y establece su Receiver. Como Invokerobjeto, Switchalmacena el Commandobjeto concreto . El Invokeremite una petición llamando executeal Commandobjeto. El Commandobjeto concreto invoca operaciones en su Receiverpara realizar la solicitud.

La idea clave aquí es que el comando concreto se registre con el Invokery el Invokerlo devuelva, ejecutando el comando en el Receiver.

Código de ejemplo de patrón de comando

Echemos un vistazo a un ejemplo simple que ilustra el mecanismo de devolución de llamada logrado a través del patrón Command.

El ejemplo muestra ay Fana Light. Nuestro objetivo es desarrollar un objeto Switchque pueda encender o apagar cualquier objeto. Vemos que el Fany el Lighttienen diferentes interfaces, lo que significa que Switchtiene que ser independiente de la Receiverinterfaz o no tiene conocimiento del código> Interfaz del receptor. Para solucionar este problema, necesitamos parametrizar cada uno de los Switchs con el comando apropiado. Obviamente, el Switchconectado al Lighttendrá un comando diferente al Switchconectado al Fan. La Commandclase tiene que ser abstracta o una interfaz para que esto funcione.

Cuando Switchse invoca el constructor de a , se parametriza con el conjunto apropiado de comandos. Los comandos se almacenarán como variables privadas del Switch.

Cuando se llaman las operaciones flipUp()y flipDown(), simplemente harán el comando apropiado para execute( ). No Switchtendrán idea de lo que sucede como resultado de execute( )ser llamado.

TestCommand.java class Fan {public void startRotate () {System.out.println ("El ventilador está girando"); } public void stopRotate () {System.out.println ("El ventilador no gira"); }} class Light {public void turnOn () {System.out.println ("La luz está encendida"); } public void turnOff () {System.out.println ("La luz está apagada"); }} class Switch {comando privado UpCommand, DownCommand; Interruptor público (Comando arriba, Comando abajo) {Comando arriba = Arriba; // comando concreto se registra con el invocador DownCommand = Down; } void flipUp () {// el invocador devuelve el comando concreto, que ejecuta el comando en el UpCommand del receptor. ejecutar (); } void flipDown () {DownCommand. ejecutar (); }} clase LightOnCommand implementa Command {Private Light myLight; Public LightOnCommand (Light L) {myLight = L;} public void execute () {myLight. encender( ); }} clase LightOffCommand implementa Command {Private Light myLight; Public LightOffCommand (Light L) {myLight = L; } public void execute () {myLight. apagar( ); }} la clase FanOnCommand implementa Command {private Fan myFan; FanOnCommand público (Fan F) {myFan = F; } public void execute () {myFan. startRotate (); }} la clase FanOffCommand implementa Command {private Fan myFan; FanOffCommand público (Fan F) {myFan = F; } public void execute () {myFan. stopRotate (); }} Public class TestCommand {public static void main (String [] args) {Light testLight = new Light (); LightOnCommand testLOC = nuevo LightOnCommand (testLight); LightOffCommand testLFC = nuevo LightOffCommand (testLight); Switch testSwitch = nuevo Switch (testLOC, testLFC); testSwitch.flipUp (); testSwitch.flipDown ();Fan testFan = new Fan (); FanOnCommand foc = new FanOnCommand (testFan); FanOffCommand ffc = nuevo FanOffCommand (testFan); Switch ts = new Switch (foc, ffc); ts.flipUp (); ts.flipDown (); }} Command.java public interface Command {public abstract void execute (); }

Observe en el ejemplo de código anterior que el patrón Command desacopla completamente el objeto que invoca la operación - (Switch )- de los que tienen el conocimiento para realizarla - Lighty Fan. Esto nos da mucha flexibilidad: el objeto que emite una solicitud debe saber solo cómo emitirla; no necesita saber cómo se llevará a cabo la solicitud.

Patrón de comando para implementar transacciones

Un patrón de comando también se conoce como patrón de acción o transacción. Consideremos un servidor que acepta y procesa transacciones entregadas por clientes a través de una conexión de socket TCP / IP. Estas transacciones constan de un comando, seguido de cero o más argumentos.

Los desarrolladores pueden usar una declaración de cambio con un caso para cada comando. El uso de Switchdeclaraciones durante la codificación es una señal de mal diseño durante la fase de diseño de un proyecto orientado a objetos. Los comandos representan una forma orientada a objetos para admitir transacciones y se pueden utilizar para resolver este problema de diseño.

En el código de cliente del programa TestTransactionCommand.java, todas las solicitudes se encapsulan en el TransactionCommandobjeto genérico . El TransactionCommandconstructor es creado por el cliente y está registrado con CommandManager. Las solicitudes en cola se pueden ejecutar en diferentes momentos llamando al runCommands(), lo que nos da mucha flexibilidad. También nos da la capacidad de ensamblar comandos en un comando compuesto. También tengo CommandArgument, CommandReceivery CommandManagerlas clases y subclases de TransactionCommand- a saber AddCommandy SubtractCommand. A continuación se muestra una descripción de cada una de estas clases:

  • CommandArgumentes una clase auxiliar, que almacena los argumentos del comando. Puede reescribirse para simplificar la tarea de pasar un número grande o variable de argumentos de cualquier tipo.

  • CommandReceiver implementa todos los métodos de procesamiento de comandos y se implementa como un patrón Singleton.

  • CommandManageres el invocador y es el Switchequivalente del ejemplo anterior. Almacena el TransactionCommandobjeto genérico en su myCommandvariable privada . Cuando runCommands( )se invoca, llama al execute( )del TransactionCommandobjeto apropiado .

En Java, es posible buscar la definición de una clase dada una cadena que contenga su nombre. En el execute ( )funcionamiento de la TransactionCommandclase, calculo el nombre de la clase y lo vinculo dinámicamente con el sistema en ejecución, es decir, las clases se cargan sobre la marcha según sea necesario. Utilizo la convención de nomenclatura, el nombre del comando concatenado por la cadena "Comando" como el nombre de la subclase del comando de transacción, de modo que se pueda cargar dinámicamente.

Tenga en cuenta que el Classobjeto devuelto por newInstance( )debe convertirse al tipo apropiado. Esto significa que la nueva clase tiene que implementar una interfaz o subclase de una clase existente que es conocida por el programa en tiempo de compilación. En este caso, dado que implementamos la Commandinterfaz, esto no es un problema.