Este documento es el resumen del estudio que he realizado sobre el patrón Command. Es un patrón que me ha costado bastante entenderlo. Cuando al final se me ha hecho la luz, he pensado en escribir este documento, en el que explico (a mi manera) como funciona realmente. A mi me servirá para no olvidarme, y a otros que tengan el mismo problema que yo, para que puedan tener un punto de apoyo y poder entenderlo en menos tiempo del que yo he tardado
Lo primero que quiero decir es que me he comprado un libro fantástico sobre Patrones de diseño, [Ver Referencia Bibliográfica] el único problema que tiene es que la parte de código es bastante escueta, esquemática, y ademas esta escrita en C++, pero a pesar de este pequeño detalle, es un libro que merece la pena comparar
Y lo segundo, es que he observado, que todos los textos que se encuentran en Internet, son copia del mismo, porque todos son iguales (mas o menos), y supongo que todos son una traducción del libro original de GOF, incluida la Wikipedia que tampoco dice nada diferente, así que lo que voy a intentar es contar algo diferente sobre este Patrón.
Para empezar, vamos a trabajar con Visual Basic .NET (versión 2008). Vamos a diseñar un formulario que tenga un TextBox y un MenuEdicion con las opciones clásicas de cortar, copiar, pegar, etc. que corresponden a ese menú de Windows. Y el problema que queremos resolver es diseñar el código que resuelva este problema utilizando el Patrón Command
Un punto a considerar es que la descripción de los patrones tiene su propio lenguaje [Ver LENGUAJE_PATRONES]. En este documento, y con el objeto de mantener la norma, he seguido esa nomenclatura
Este Patrón permite solicitar una operación a un objeto sin conocer realmente el contenido de esta operación, ni el receptor real de la misma. Para ello se encapsula la petición como un objeto, con lo que además se facilita la parametrización de los métodos. El Patrón permite:
El problema a resolver es implementar el código de un menú edición
Ejemplo de un menú edición
La forma, (digamos normal), de hacerlo es utilizar un formulario de [Windows.Form], arrastrar un control [MenuStrip], escribir en ese control los elementos del menú, y a continuación hacer doble clic sobre cada elemento y escribir el código correspondiente en los correspondientes de manejadores de eventos del menú
Algo así como:
Public Class FormCommand Private Sub CortarToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles CortarToolStripMenuItem.Click ' Proceso Cortar End Sub Private Sub CopiarToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles CopiarToolStripMenuItem.Click ' Proceso de Copiar End Sub Private Sub PegarToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles PegarToolStripMenuItem.Click ' Proceso de pegar End Sub End Class
Este enfoque tiene un problema: El código esta fuertemente acoplado, y por lo tanto, es difícil de ampliar y mantener.
Para complicar más las cosas, queremos que nuestro código soporte operaciones de deshacer y repetir [Es decir, volver a realizar una operación a la que se dio marcha atrás (se anularon sus efectos)] para la mayoría de sus funciones, pero no para todas. En concreto, queremos ser capaces de deshacer operaciones que modifican el documento, (como Cortar), con las que el usuario puede perder muchos datos sin darse cuenta. Sin embargo, no deberíamos intentar deshacer operaciones como "Seleccionar todo". Estas operaciones no deberían tener efecto en el proceso de deshacer. Tampoco queremos un límite arbitrario en el número de niveles de deshacer y repetir. Es decir, el objetivo es conseguir un mecanismo simple y extensible que satisfaga todas estas necesidades.
Desde nuestra perspectiva de diseñadores, un menú desplegable no es más que un objeto, que tiene hijos, y, estos hijos, hacen alguna operación en respuesta a un clic del ratón es decir, en respuesta a una petición de un cliente. [Conceptualmente, el cliente es cualquier usuario (cualquier persona), pero en realidad es otro objeto (como un despachador de eventos) que gestiona las entradas del usuario]
El proceso que ejecuta la petición (por ejemplo Cortar, Pegar) puede implicar una operación en un objeto, o muchas operaciones sobre muchos objetos, o algo entre medias. En el Ejemplo que estamos describiendo, se realiza una operación sobre dos objetos, un control TextBox, y el Portapapeles
A la hora de escribir el código podemos optar por varias soluciones
1) Escribir el código directamente en el manejador de eventos. Este enfoque tiene el problema de que el código esta totalmente acoplado al manejador del evento
Private Sub CortarToolStripMenuItem_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles CortarToolStripMenuItem.Click ' Asegurarme de que en el textbox hay algo seleccionado If unTextBox.SelectedText.Length > 0 Then ' Cortar el texto seleccionado y levarlo al portapapeles unTextBox.Cut() End If End Sub
2) Escribir una clase que contenga una función que realice el proceso, es decir, parametrizar cada ElementoDeMenu con la función a llamar.
Friend NotInheritable Class FuncionesMenuEdicion ''' ------------------------------------------------- ''' <summary> ''' Constructor privado ''' </summary> Private Sub New() ' ------------------------------------------------- ' Regla [FXCOP] ' http://msdn.microsoft.com/es-es/ms182169.aspx ' ------------------------------------------------- End Sub ''' ------------------------------------------------- ''' <summary> ''' Cortar el texto seleccionado en el TextBox al portapapeles ''' </summary> ''' <param name = 'unTextBox'> ''' El textBox cuyo texto seleccinado se mueve al protapapeles ''' <see cref = "System.Windows.Forms.TextBox"> ''' (System.Windows.Forms.TextBox)</see> ''' </param> ''' ------------------------------------------------- Public Shared Sub Cortar(ByRef unTextBox As System.Windows.Forms.TextBox) '----------------------------------- ' Control del parametro If unTextBox Is Nothing Then Throw New ArgumentNullException("pUnTextBox", _ "El TextBox tiene un valor nothing") End If '----------------------------------- ' proceso Try ' Asegurarme de que en el textbox hay algo seleccionado If unTextBox.SelectedText.Length > 0 Then ' Cortar el texto seleccionado y levarlo al portapapeles unTextBox.Cut() End If Catch ex As Exception Throw End Try End Sub Public Shared Sub Copiar(ByRef unTextBox As System.Windows.Forms.TextBox) End Sub Public Shared Sub Pegar(ByRef unTextBox As System.Windows.Forms.TextBox) End Sub End Class
Este enfoque tiene la ventaja de que un proceso (por ejemplo, Cortar) se puede llamar desde el menú del formulario, desde un menú contextual y/o desde un botón, pero , a pesar de lo dicho, no es una buena solución, por al menos tres razones:
Estas razones sugieren que deberíamos parametrizar los elementos de menú con un objeto, y no con una función. De ese modo podríamos usar la herencia para ampliar y reutilizar la implementación de la petición, y también tendríamos un lugar donde almacenar el estado e implementar la funcionalidad de deshacer/repetir. Encapsularemos cada petición en un objeto ORDEN. [Command en el original en inglés]
Las órdenes de un menú, se pueden implementar de forma fácil usando el Patrón Command, que describe cómo encapsular una petición. El Patrón también trata los mecanismos de deshacer y repetir incorporados en la interfaz básica de Orden.
En primer lugar definiremos una clase abstracta Orden que proporciona una interfaz para emitir una petición. La interfaz básica consiste en una única operación abstracta denominada "Ejecutar".
''' ---------------------------------------------- ''' <summary> ''' [Command] - Clase Orden (Command) ''' Declara una interfaz para ejecutar una operación ''' </summary> ''' <remarks> - Patrón Command - </remarks> Friend MustInherit Class Orden Public MustOverride Sub Ejecutar() ' execute End Class
Las subclases de Orden (las ordenes concretas) implementan "Ejecutar" de distintas formas para satisfacer diferentes peticiones. Por ejemplo la subclase de Orden a la que llamaremos OrdenConcretaCortar implementa en su método Ejecutar [Public Overrides Sub Ejecutar()] todas las instrucciones necesarias para quitar del TextBox el texto seleccionado y copiarlo en el portapapeles
Algunas subclases pueden delegar parte del trabajo, o todo, en otros objetos. Por ejemplo, la OrdenConcretaCopiar, y la OrdenConcretaCortar realizan las dos la misma operación, Copiar un texto en el portapapeles. Lo lógico es colocar el código que realiza esa operación (copiar en el portapapeles) en una clase que realice ese trabajo con objeto de no duplicar código innecesariamente, es decir, delega parte del trabajo en otro objeto
Otras subclases pueden ser capaces de responder a la petición por sí solas, etc. No obstante, para el solicitante un objeto Orden no es más que eso, un objeto Orden -todos son tratados por igual-.
El término original en inglés es undoability. Tanto éste como el verbo correspondiente undo, hacen alusión, en este contexto, a la acción de dar marcha atrás a las operaciones, esto es, a la posibilidad de revertir los efectos de una operación anterior. Si bien "deshacer" es una traducción demasiado literal (el DRAE no sanciona esta acepción) y es un apalabra que se usa en la mayoría de las aplicaciones existentes.
Es importante que las aplicaciones interactivas cuenten con la capacidad de deshacer/repetir.
Para deshacer y repetir órdenes, añadiremos una operación Deshacer a la interfaz de Orden, que anula los efectos de la operación Ejecutar precedente, usando la información que ésta haya guardado. Por ejemplo, en el caso de una OrdenFuente, la operación Ejecutar debería guardar la parte del texto a la que afecta el cambio de fuente junto con la fuente o fuentes originales. La operación Deshacer de OrdenFuente devolvería el texto a su fuente original.
A veces la capacidad de deshacer tiene que ser establecida en tiempo de ejecución. Una petición para cambiar la fuente de una selección no hace nada si el texto ya aparece en esa fuente. Supongamos que el usuario selecciona parte del texto y realiza un cambio de fuente que no tiene ningún efecto. ¿Cuál debería ser el resultado de la siguiente petición deshacer? ¿Un cambio sin sentido debería causar que la petición deshacer hiciera algo igualmente sin sentido? Probablemente no. Si el usuario repite ese cambio de fuente varias veces, no debería tener que realizar exactamente el mismo número de operaciones deshacer para regresar a la última operación con sentido. Si el resultado final de ejecutar una orden fue nulo, entonces no hay necesidad de la correspondiente operación deshacer.
Así que para determinar si una orden se puede deshacer, añadiremos una operación abstracta "Reversible" a la interfaz de Orden. Reversible devuelve un valor lógico, y las subclases de Orden pueden redefinir esta operación para devolver verdadero o falso basándose en criterios de tiempo de ejecución.
En resumen, la clase abstracta Orden quedara de la siguiente manera
''' ---------------------------------------------- ''' <summary> ''' [Command] - Clase Orden (Command) ''' Declara una interfaz para ejecutar una operación ''' </summary> ''' <remarks> - Patrón Command - </remarks> Friend MustInherit Class Orden ''' ---------------------------------------------- ''' <summary> ''' Constructor ''' </summary> Protected Sub New() ' Regla [FXCOP - http://msdn.microsoft.com/es-es/ms182126.aspx ' Los tipos derivados pueden llamar sólo a los constructores de tipos abstractos. ' Puesto que los constructores públicos crean instancias de un tipo y no se ' pueden crear instancias de un tipo abstracto, no es correcto diseñar un tipo ' abstracto con un constructor público. End Sub ''' ---------------------------------------------- ''' <summary> ''' Ejecuta la orden ''' </summary> Public MustOverride Sub Ejecutar() ' execute ''' ---------------------------------------------- ''' <summary> ''' Deshace la ejecucion de la orden ''' </summary> Public MustOverride Sub Deshacer() ' UnExecute ''' ---------------------------------------------- ''' <summary> ''' Obtiene un valor que indica si el invoker tiene que ''' almacenar la orden o no en el almacén de ordenes ejecutadas. ''' </summary> ''' <returns> ''' <para>Un valor lógico:</para> ''' <para>TRUE --> SI, si hay que almacenar la orden</para> ''' <para>FALSE -> NO, no se almacena</para> ''' </returns> ''' <remarks> ''' <para> ''' Solamente se pueden deshacer/rehacer, las ordenes que estén almacenadas ''' en el almacén de ordenes del invoker.</para> ''' <para>Entonces.. ¿Que ordenes almaceno?</para> ''' <para> ''' Una solución es almacenar todas. Pero hay ordenes que no tiene sentido ''' almacenarlas porque nunca se van a deshacer (Por ejemplo la ''' orden 'seleccionar todo'), si embargo hay otras ordenes ''' (por ejemplo 'Cortar', 'Pegar') que en principio siempre hay que ''' almacenarlas, pero la realidad es que una orden cortar que no ''' corta nada, ¿para que quiero almacenarla si cuando se deshaga la ''' orden tampoco hará nada? ''' </para> ''' <para> ''' Por eso esta función [Reversible] que cada clase ''' 'OrdenConcreta' carga con el valor true o false en función de que ''' esa orden haya que almacenarla o no ''' </para> ''' </remarks> Public MustOverride Function Reversible() As Boolean End Class
Para soportar niveles arbitrarios de deshacer y repetir hay que definir un historial de órdenes, es decir, una lista de las órdenes ejecutadas (y las anuladas).
Conceptualmente, el historial de órdenes se parece a esto:
Cada círculo representa un objeto Orden. En este caso el usuario ha ejecutado cuatro órdenes. La de más a la izquierda fue la primera que se ejecutó, seguida por la segunda de más a la izquierda y así sucesivamente hasta la orden más recientemente ejecutada, que es la de más a la derecha. La línea etiquetada como "presente" lleva la cuenta de cuál fue la última orden ejecutada (o anulada).
Para deshacer la última orden, simplemente llamaremos a Deshacer sobre la orden más reciente:
Después de la operación de deshacer, movemos la línea "presente" una orden hacia la izquierda. Si el usuario elige de nuevo deshacer, se anulará la siguiente orden más recientemente ejecutada de la misma manera que se hizo con ésta, y nos encontraremos en el estado que se muestra a continuación:
Puede observarse cómo simplemente repitiendo este procedimiento obtenemos múltiples niveles de operaciones de deshacer. El número de estos niveles sólo está limitado por la longitud del historial de órdenes.
Para repetir una orden que se acaba de deshacer, hacemos lo mismo pero a la inversa. Las órdenes a la derecha de la línea del presente son las que se pueden deshacer en un futuro. Para repetir la última orden anulada, llamamos a Ejecutar en la orden situada a la derecha de la línea actual:
A continuación movemos la línea del presente de manera que la siguiente operación de repetir llamará a repetir sobre la siguiente orden en el futuro.
Por supuesto, si la siguiente operación no es otro repetir, sino un deshacer, entonces se anulará la orden a la izquierda de la línea "presente". De esta manera el usuario puede ir hacia delante y hacia atrás en el tiempo tanto como sea necesario para recuperarse de los errores.
Public Class InvocadorMenuEdicion Implements IDisposable '---------------------------------------------------------------------------- ' Regla [FxCop] '----------------------------------------------------------------------------- 'Resolution : "Implement IDisposable on 'Invocador' because it " ' creates members of the following IDisposable types: ' 'TextBox'. If 'Invocador' has previously shipped, adding ' new members that implement IDisposable to this type 'Help : http://msdn2.microsoft.com/ms182172(VS.90).aspx (String) '----------------------------------------------------------------------------- '---------------------------------------------------------------- 'Variables de la clase '---------------------------------------------------------------- Private _almacenOrdenes As New Generic.List(Of Orden) 'Private _almacenOrdenes As New ArrayList '---------------------------------------------------------------- 'Este campo lleva la cuenta de cual es la última orden ejecutada (o anulada) Private _indiceUltimaOrdenEjecutada As Integer '---------------------------------------------- ' El Receptor - El textBox sobre el que se realiza la operacion OrdenConcreta Private _unTextBox As System.Windows.Forms.TextBox ''' <summary> ''' Constructor de la clase ''' </summary> ''' <param name = 'unTextBox'> ''' El textBox en el que se realiza la operacion de edicion ''' <see cref = "System.Windows.Forms.TextBox"> ''' (System.Windows.Forms.TextBox)</see> ''' </param> Public Sub New(ByVal unTextBox As System.Windows.Forms.TextBox) _almacenOrdenes = New Generic.List(Of Orden) '_almacenOrdenes = New ArrayList Me._unTextBox = New System.Windows.Forms.TextBox Me._unTextBox = unTextBox ' limpiar el portapaleles ' Problema con el portapapeles, aparece el ToString de un Objeto copiado Servicios.Variado.Portapapeles.Clear() End Sub ''' ---------------------------------------------- ''' <summary> ''' El Receptor - El textBox sobre el que se realiza la operacion OrdenConcreta ''' </summary> ''' <value>System.Windows.Forms.TextBox</value> Public Property UNTextBox() As System.Windows.Forms.TextBox Get Return _unTextBox End Get Set(ByVal value As System.Windows.Forms.TextBox) _unTextBox = value End Set End Property '---------------------------------------------------------------- ''' <summary> ''' Ejecuta una operacion del menu edicion ''' </summary> ''' <param name="ordenEdicion"> ''' La operacion de edicion que se va a realizar, (Copiar, cortar, pegar, etc) ''' Se guardan como valores de una enumeración ''' </param> Public Sub Ejecutar(ByVal ordenEdicion As EOrdenEdicion) '----------------------------------- ' crear una operacion Orden (command) y ejecutarla Dim unaOrden As Orden = Nothing '----------------------------------- ' fabrica de ordenes Select Case ordenEdicion Case EOrdenEdicion.None Call Enumeracion_NoPuedeTomarEseValor() Case EOrdenEdicion.Cortar ' crear una operacion Orden (command) unaOrden = New OrdenConcretaCortar(Me._unTextBox) Case EOrdenEdicion.Copiar unaOrden = New OrdenConcretaCopiar(Me._unTextBox) Case EOrdenEdicion.CopiarTodo unaOrden = New OrdenConcretaCopiarTodoMacro(Me._unTextBox) Case EOrdenEdicion.Pegar unaOrden = New OrdenConcretaPegar(Me._unTextBox) Case EOrdenEdicion.Eliminar unaOrden = New OrdenConcretaEliminar(Me._unTextBox) Case EOrdenEdicion.SeleccionarTodo unaOrden = New OrdenConcretaSeleccionarTodo(Me._unTextBox) Case Else Call Enumeracion_InvalidEnumArgumentException(ordenEdicion.ToString) End Select '----------------------------------- '(Si la orden no es nothing) If unaOrden IsNot Nothing Then '----------------------------------- ' PRIMERO: almacen de ordenes '----------------------------------- ' añadir la orden a la lista de ordenes ' siempre que se pueda deshacer '------------------------------------------------------- ' ¿¿El invoker tiene que almacenar la orden en ' el almacén de ordenes ejecutadas ???. ' Solamente se pueden deshacer/rehacer, las ordenes que estén ' almacenadas en el almacén de ordenes del invoker. ' Entonces.. ¿Que ordenes almaceno? ' Una solución es almacenar todas. Pero hay ordenes que no tiene ' sentido almacenarlas porque nunca se van a deshacer (Por ejemplo ' la orden 'seleccionar todo'), si embargo hay otras ordenes ' (por ejemplo 'Cortar', 'Pegar') que en principio siempre hay que ' almacenarlas, pero la realidad es que una orden cortar que no ' corta nada, ¿para que quiero almacenarla si cuando se deshaga la ' orden tampoco hará nada? ' Por eso hay una funcion [Reversible ] que en cada clase ' 'OrdenConcreta' devuelve el valor true o false en función de que ' esa orden haya que almacenarla o no '------------------------------------------------------- If unaOrden.Reversible = True Then _almacenOrdenes.Add(unaOrden) _indiceUltimaOrdenEjecutada += 1 End If '----------------------------------- 'SEGUNDO: ejecutar la orden '----------------------------------- unaOrden.Ejecutar() '----------------------------------- 'TERCERO cargarme la orden unaOrden = Nothing End If End Sub '---------------------------------------------------------------- ''' <summary> ''' Repite la ejecucio de una orden ''' Ejecutar las órdenes de la derecha del índice de ordenes ''' </summary> Public Sub Repetir() ' Tiene que ser mayor que cero porque ' [_indiceUltimaOrdenEjecutada] funciona con índice base (1) If _indiceUltimaOrdenEjecutada >= 0 Then ' -(almacén funciona en base 0)- If _indiceUltimaOrdenEjecutada < _almacenOrdenes.Count Then ' recuperar la orden Dim unaOrden As Orden = _ Me._almacenOrdenes(_indiceUltimaOrdenEjecutada) _indiceUltimaOrdenEjecutada += 1 ' asegurarme que el indice no toma valores fuera de rango If _indiceUltimaOrdenEjecutada > _almacenOrdenes.Count Then _indiceUltimaOrdenEjecutada = _almacenOrdenes.Count End If ' ejecutar la orden unaOrden.Ejecutar() End If End If End Sub '---------------------------------------------------------------- ''' <summary> ''' Deshace la ejecucio de una orden ''' Ejecutar las órdenes de la Izquierda del índice de ordenes ''' </summary> Public Sub Deshacer() If _indiceUltimaOrdenEjecutada > 0 Then If _indiceUltimaOrdenEjecutada <= _almacenOrdenes.Count Then ' recuperar la orden Dim unaOrden As Orden = _ Me._almacenOrdenes(_indiceUltimaOrdenEjecutada - 1) _indiceUltimaOrdenEjecutada -= 1 ' asegurarme que el indice no toma valores fuera de rango If _indiceUltimaOrdenEjecutada < 0 Then _indiceUltimaOrdenEjecutada = 0 End If ' deshacer la orden unaOrden.Deshacer() End If End If End Sub '-------------------------------------------------------- ' error Case Else de una enumeracion '-------------------------------------------------------- Private Shared Sub Enumeracion_InvalidEnumArgumentException( _ ByVal ValorEnumercion As String) ' Aqui se genera un error ( Ver el codigo completo) End Sub '-------------------------------------------------------- ' Enumeracion - En este punto esta enumeración no puede tomar este valor '-------------------------------------------------------- Private Shared Sub Enumeracion_NoPuedeTomarEseValor() ' Aqui se genera un error ( Ver el codigo completo) End Sub End Class
Úsese el Patrón Command cuando se quiera
Bien eso es lo que dicen los textos que hablan del Patrón, lo que yo considero que pasa es lo siguiente
El siguiente diagrama muestra las interacciones entre estos objetos, ilustrando cómo Orden desacopla el invocador del receptor (y de la petición que éste lleva a cabo).
''' <summary> ''' Relaciona las Operaciones posibles con un menu edicion ''' </summary> Public Enum EOrdenEdicion As Integer None Cortar Copiar CopiarTodo Pegar Eliminar SeleccionarTodo End Enum
'----------------------------------------------------------------- ' Codigo en el cliente '----------------------------------------------------------------- ' Declarar el invocador del menu Private InvocadorMenuEdicion As InvocadorMenuEdicion ' Instanciar el invocador InvocadorMenuEdicion = New InvocadorMenuEdicion(Me.TextBoxTexto) '. . . ' ETC '. . . '----------------------------------------------------------------- ' Ejecutar la orden cortar al pulsar un Boton Private Sub ButtonCortar_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles ButtonCortar.Click InvocadorMenuEdicion.Ejecutar(EOrdenEdicion.Cortar) End Sub
'---------------------------------------------------------------- ''' <summary> ''' Ejecuta una operacion del menu edicion ''' </summary> ''' <param name="ordenEdicion"> ''' La operacion de edicion que se va a relaizar, (Copiar, cortar, pegar, etc) ''' </param> Public Sub Ejecutar(ByVal ordenEdicion As EOrdenEdicion) '----------------------------------- ' crear una operacion Orden (command) y ejecutarla Dim unaOrden As Orden = Nothing '----------------------------------- ' fabrica de ordenes Select Case ordenEdicion Case EOrdenEdicion.None Call Enumeracion_NoPuedeTomarEseValor() Case EOrdenEdicion.Cortar ' crear una operacion Orden (command) y ejecutarla unaOrden = New OrdenConcretaCortar(Me._unTextBox) Case EOrdenEdicion.Copiar unaOrden = New OrdenConcretaCopiar(Me._unTextBox) Case EOrdenEdicion.CopiarTodo unaOrden = New OrdenConcretaCopiarTodoMacro(Me._unTextBox) Case EOrdenEdicion.Pegar unaOrden = New OrdenConcretaPegar(Me._unTextBox) Case EOrdenEdicion.Eliminar unaOrden = New OrdenConcretaEliminar(Me._unTextBox) Case EOrdenEdicion.SeleccionarTodo unaOrden = New OrdenConcretaSeleccionarTodo(Me._unTextBox) Case Else Call Enumeracion_InvalidEnumArgumentException(ordenEdicion.ToString) End Select '----------------------------------- '(Si la orden no es nothing) If unaOrden IsNot Nothing Then '----------------------------------- ' PRIMERO: almacen de ordenes '----------------------------------- ' añadir la orden a la lista de ordenes ' siempre que se pueda deshacer '------------------------------------------------------- ' ¿¿El invoker tiene que almacenar la orden en ' el almacén de ordenes ejecutadas ???. ' Solamente se pueden deshacer/rehacer, las ordenes que estén ' almacenadas en el almacén de ordenes del invoker. ' Entonces.. ¿Que ordenes almaceno? ' Una solución es almacenar todas. Pero hay ordenes que no tiene ' sentido almacenarlas porque nunca se van a deshacer (Por ejemplo ' la orden 'seleccionar todo'), si embargo hay otras ordenes ' (por ejemplo 'Cortar', 'Pegar') que en principio siempre hay que ' almacenarlas, pero la realidad es que una orden cortar que no ' corta nada, ¿para que quiero almacenarla si cuando se deshaga la ' orden tampoco hará nada? ' Por eso hay una funcion [Reversible ] que en cada clase ' 'OrdenConcreta' devuelve el valor true o false en función de que ' esa orden haya que almacenarla o no '------------------------------------------------------- If unaOrden.Reversible = True Then _almacenOrdenes.Add(unaOrden) _indiceUltimaOrdenEjecutada += 1 End If '----------------------------------- 'SEGUNDO: ejecutar la orden '----------------------------------- unaOrden.Ejecutar() '----------------------------------- 'TERCERO cargarme la orden unaOrden = Nothing End If End Sub
Friend Class OrdenConcretaCortar '---------------------------------------------- #Region " Implementacion Interface " '---------------------------------------------- '---------------------------------------------- ''' <summary> ''' Deshace la ejecucion de la orden ''' </summary> Public Overrides Sub Deshacer() 'Recuperar el estado antes de ejecutar la orden UNTextBox.SelectionStart = Me.SelectionStart UNTextBox.SelectedText = Me.SelectedText UNTextBox.Text = Me.TextoCompletoAntesDECumplirLAOrden '------------------------------------------------- ' el portapapeles se pone con el valor que tenia ' antes de ejecutar la orden ' si es un valor vacio se vacia el protapapeles If ContenidoDelClipboardAntesDECumplirLAOrden.Length = 0 Then Servicios.Variado.Portapapeles.Clear() Else Servicios.Variado.Portapapeles. _ SetTexto(ContenidoDelClipboardAntesDECumplirLAOrden) End If '--------------------------------------------------------------- ' Proceso para que el control TextBox muestre el cursor parpadeante Call MostrarCursorDelTextBox() End Sub '---------------------------------------------- ''' <summary> ''' Ejecuta la orden ''' </summary> Public Overrides Sub Ejecutar() If Me.SelectedText.Length > 0 Then ' mover al portapapeles el texto seleccionado Servicios.Variado.Portapapeles.SetTexto(Me.SelectedText) ' quitar del TextBox texto seleccionado Me.UNTextBox.Text = _ Me.UNTextBox.Text.Remove(Me.SelectionStart, Me.SelectedText.Length) End If '--------------------------------------------------------------- ' Proceso para que el control TextBox muestre el cursor parpadeante Call MostrarCursorDelTextBox() End Sub '---------------------------------------------- ''' <summary> ''' Obtiene un valor que indica si el invoker tiene que ''' almacenar la orden o no en el almacén de ordenes ejecutadas. ''' </summary> ''' <returns> ''' <para>Un valor lógico:</para> ''' <para>TRUE --> SI, si hay que almacenar la orden</para> ''' <para>FALSE -> NO, no se almacena</para> ''' </returns> Public Overrides Function Reversible() As Boolean '--------------------------------------------- ' En principio no se graba la orden Dim grabar As Boolean = False '--------------------------------------------- ' Se cumple la condicion para que se ejecute la orden??? If Me.SelectedText.Length > 0 Then 'activar la grabacion de la orden porque se va ha ejecutar grabar = True End If Return grabar End Function #End Region End Class
El Patrón Command tiene las siguientes consecuencias:
A la hora de implementar el Patrón Command deben tenerse en cuenta las siguientes cuestiones:
A)¿Cómo debería de ser inteligente una orden? Una orden puede tener un amplio conjunto de habilidades. Por un lado, simplemente define un enlace entre un receptor y las acciones que lleva a cabo la petición. Por el otro, lo implementa todo ella misma sin delegar para nada en el receptor. Este último extremo resulta útil cuando queremos definir órdenes que sean independientes de las clases existentes, cuando no existe ningún receptor adecuado o cuando una orden conoce implícitamente a su receptor. Por ejemplo, una orden que crea otra ventana de aplicación puede ser tan capaz de crear la ventana como cualquier otro objeto. En algún punto entre estos dos extremos se encuentran las órdenes que tienen el conocimiento suficiente para encontrar dinámicamente sus receptores.
B) Permitir deshacer y repetir. Las órdenes pueden permitir capacidades de deshacer y repetir si disponen de una manera de revertir su ejecución (por ejemplo, mediante una operación Deshacer). Una clase OrdenConcreta podría necesitar almacenar información de estado adicional para hacer esto. Este estado puede incluir
Para permitir un nivel de deshacer, una aplicación sólo necesita guardar la última orden que se ejecutó. Para múltiples niveles de deshacer y repetir, la aplicación necesita un historial de las órdenes que han sido ejecutadas, donde la máxima longitud de la lista determina el número de niveles de deshacer/repetir. El historial guarda secuencias de órdenes que han sido ejecutadas. Recorrer la lista hacia atrás deshaciendo las órdenes cancela sus efectos; recorrerla hacia delante ejecutando las órdenes los repite.
Una orden anulable, debe copiarse en un objeto nuevo y a continuación, guardarse en el historial antes de ejecutarse. Es decir guardo en el historial un objeto (no guardo una referencia). Eso es debido a que el objeto orden que lleva a cabo la petición original desde, supongamos, un ElementoDeMenu, más tarde ejecutará otras peticiones. La copia es necesaria para distinguir entre diferentes invocaciones de la misma orden si su estado puede variar entre invocaciones sucesivas.
Por ejemplo, una OrdenBorrar que borra los objetos seleccionados debe guardar diferentes conjuntos de objetos cada vez que se ejecuta. Por tanto el objeto OrdenBorrar deberá ser copiado antes de su ejecución y esta copia almacenada en el historial. En caso de que el estado de la orden no cambie tras su ejecución no es necesario realizar la copia, basta con guardar en el historial una referencia a la orden.
C) Evitar la acumulación de errores en el proceso de deshacer. La histéresis puede ser un problema a la hora de garantizar un mecanismo de deshacer/repetir fiable, que preserve la semántica. Los errores se pueden acumular a medida que se ejecutan y deshacen las órdenes repetidamente, de manera que el estado de una aplicación finalmente difiera de sus valores originales. Por tanto, puede ser necesario guardar más información con la orden para asegurar que los objetos son devueltos a su estado original. Puede aplicarse el Patrón Memento para dar a la orden acceso a esta información sin exponer las interioridades de otros objetos
D) Uso de plantillas: Para aquellas órdenes que (a) no se pueden deshacer y (b) no necesitan argumentos, podemos usar una clase genérica para evitar crear una subclase de Orden para cada clase de acción y receptor.
Atención… Este texto lo he leído en TODOS los documento que he consultado, como dije al principio de este documento todos están copiados del mismo texto por lo que todos dicen lo mismo, pero yo no he entendido lo que quiere decir exactamente, ni tampoco entiendo el poco código que he visto, así que si tu lo entiendes y puedes mandarme un ejemplo te lo agradecería porque así dejaría este texto completo
E) Uso de Ordenes macro: Una OrdenMacro es una clase que gestiona una secuencia de órdenes y proporciona operaciones para añadir y eliminar subórdenes. No necesita un receptor explicito, ya que las subórdenes ya definen su receptor.
'------------------------------------------------------------------------------- ''' <summary> ''' Clase [OrdenConcretaCopiarTodoMacro] OrdenConcreta MACRO (ConcreteCommand) ''' </summary> ''' <remarks> ''' Patrón Command – ''' [ConcreteCommand] - Define un enlace entre un objeto Receptor y una acción ''' Implementa Ejecutar invocando la correspondiente operación u operaciones del Receptor ''' Las subclases -Órdenes Concretas- especifican un par Receptor-Acción, ''' guardando el receptor (receiver) en una variable de instancia e ''' implementando EJECUTAR para que invoque a la petición [en el receptor] '''</remarks> Friend Class OrdenConcretaCopiarTodoMacro Inherits OrdenConcretaBase Implements IDisposable '------------------------------------------------------- ' Campos de la clase (Son las ordenes que se ejecutaran) '------------------------------------------------------- Private _OrdenSeleccionarTodo As OrdenConcretaSeleccionarTodo Private _OrdenCopiar As OrdenConcretaCopiar '---------------------------------------------- ''' <summary> ''' Constructor ''' </summary> ''' <param name = 'unTextBox'> ''' El textBox cuyo texto seleccinado se mueve al protapapeles ''' <see cref = "System.Windows.Forms.TextBox"> ''' (System.Windows.Forms.TextBox)</see> ''' </param> ''' <remarks></remarks> Public Sub New(ByVal unTextBox As System.Windows.Forms.TextBox) MyBase.New(unTextBox, EOrdenEdicion.CopiarTodo) End Sub ''' ------------------------------------------------- ''' <summary> ''' El metodo Dispose ''' </summary> Protected Overloads Sub Dispose() '---------------------------------------------------- ' Dispose de la clase base MyBase.Dispose(True) '---------------------------------------------------- ' Dispose de los objetos de esta clase If Not (_OrdenSeleccionarTodo Is Nothing) Then ' Comprobar si se puede llamar al dipose del objeto If TypeOf _OrdenSeleccionarTodo Is IDisposable Then _OrdenSeleccionarTodo.Dispose() End If ' objeto a null _OrdenSeleccionarTodo = Nothing End If If Not (_OrdenCopiar Is Nothing) Then ' Comprobar si se puede llamar al dipose del objeto If TypeOf _OrdenCopiar Is IDisposable Then _OrdenCopiar.Dispose() End If ' objeto a null _OrdenCopiar = Nothing End If End Sub '------------------------------------------------------------------ ''' <summary> ''' Deshace la ejecucion de la orden ''' </summary> Public Overrides Sub Deshacer() '--------------------------------------------------------------- _OrdenCopiar.Deshacer() _OrdenSeleccionarTodo.Deshacer() '--------------------------------------------------------------- ' Proceso para que el control TextBox muestre el cursor parpadeante Call MostrarCursorDelTextBox() End Sub '------------------------------------------------------------------ ''' <summary> ''' Ejecuta la orden ''' </summary> Public Overrides Sub Ejecutar() '--------------------------------------------------------------- _OrdenSeleccionarTodo = New OrdenConcretaSeleccionarTodo(Me.UNTextBox) _OrdenSeleccionarTodo.Ejecutar() '---------------------------- _OrdenCopiar = New OrdenConcretaCopiar(Me.UNTextBox) _OrdenCopiar.Ejecutar() '--------------------------------------------------------------- ' Proceso para que el control TextBox muestre el cursor parpadeante Call MostrarCursorDelTextBox() End Sub '---------------------------------------------- ''' <summary> ''' Obtiene un valor que indica si el invoker tiene que ''' almacenar la orden o no en el almacén de ordenes ejecutadas. ''' </summary> ''' <returns> ''' <para>Un valor lógico:</para> ''' <para>TRUE --> SI, si hay que almacenar la orden</para> ''' <para>FALSE -> NO, no se almacena</para> ''' </returns> Public Overrides Function Reversible() As Boolean '--------------------------------------------- ' En principio no se graba la orden Dim grabar As Boolean = False '--------------------------------------------- ' Se cumple la condicion para que se ejecute la orden??? If Me.SelectedText.Length > 0 Then 'activar la grabacion de la orden porque se va ha ejecutar grabar = True End If Return grabar End Function End Class
En este enlace hay un fichero ZIP que contiene un proyecto completo de demostración de este Patrón.
A lo largo de este documento he ido copiando EXTRACTOS de las clases cuyos enlaces internos en este documento son los siguientes
Este código para C# esta en [ http://www.dofactory.com/Patterns/PatternCommand.aspx ]
Public MustInherit Class Command Protected _receiver As Receiver Public Sub New(ByVal pReceiver As Receiver) Me._receiver = pReceiver End Sub Public MustOverride Sub Ejecutar() End Class Public Class ConcreteCommand Inherits Command Public Sub New(ByVal pReceiver As Receiver) MyBase.new(pReceiver) End Sub Public Overrides Sub Ejecutar() Me._receiver.accion() End Sub End Class Public Class Invoker Private _command As Command Public Sub SetCommand(ByVal pCommand As Command) Me._command = pCommand End Sub Public Sub ExecuteCommand() _command.Ejecutar() End Sub End Class Public Class Receiver Public Sub accion() Console.WriteLine("Called Received.Action()") End Sub End Class Public Class Gen_MainApp Public Shared Sub main() Dim _receiver As New Receiver Dim _command As Command = New ConcreteCommand(_receiver) Dim _invoker = New Invoker ' establecer y ejecutar command _invoker.SetCommand(_command) _invoker.ExecuteCommand() ' wait Console.Read() End Sub End Class
Se puede usar el Patrón Composite para implementar OrdenMacro.
Un Patrón Memento puede mantener el estado que necesitan las órdenes para anular sus efectos.
GOF
WikiCommand
Otros Ejemplos de implementación
Gamma, E.; Helm, R.; Johnson, R.; Vlissides, J. Patrones de Diseño Elementos de software orientado a elementos reutilizables Pearson Educación SA. Madrid 200263 ISBN 10:84-7829-059-1 ISBN 13:978-84-7829-059-8 Materia: Informática 681.3 Formato 195x250 Páginas: 384
© 1.997- 2.008 - La Güeb de Joaquín | |||||
Joaquin Medina Serrano
|
|||||
|