logotipo

El Patrón de diseño (De comportamiento) Command



Descripción general

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

[TOC] Tabla de Contenidos


↑↑↑

A modo de introducción

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.


↑↑↑

Problema que vamos a resolver

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


↑↑↑

El Patrón de diseño Command


↑↑↑

1 Propósito

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:


↑↑↑

2 Motivación

El problema a resolver es implementar el código de un menú edición

Ejemplo 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:

  1. No soluciona el problema de deshacer/repetir.
  2. Es difícil asociar el estado a una función. Por ejemplo, una función que cambia la fuente necesita saber qué fuente cambiar.
  3. Las funciones son difíciles de extender y es difícil reutilizar partes de ellas.

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.


↑↑↑

2.1 clase orden y sus subclases

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


↑↑↑

2.2 Como implemento la capacidad de deshacer

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


↑↑↑

2.3 Como implemento el historial de órdenes

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:

Historial de Ordenes, Imagen 01

Implementando el historial de órdenes - Imagen 01

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:

Historial de Ordenes, Imagen 02

Implementando el historial de órdenes - Imagen 02

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:

Historial de Ordenes, Imagen 03

Implementando el historial de órdenes - Imagen 03

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:

Historial de Ordenes, Imagen 04

Implementando el historial de órdenes - Imagen 04

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.

Historial de Ordenes, Imagen 05

Implementando el historial de órdenes - Imagen 05

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.


↑↑↑

2.4. Código del invocador que soporta Deshacer/Rehacer

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


↑↑↑

3 Aplicabilidad

Úsese el Patrón Command cuando se quiera


↑↑↑

4 Estructura

Estructura del patrón Command

Estructura del patrón Command


↑↑↑

5 Participantes


↑↑↑

Orden (Es la clase abstracta)


↑↑↑

OrdenConcreta (OrdenPegar, OrdenAbrir)


↑↑↑

Cliente (Aplicación)


↑↑↑

Invocador (ElementoDeMenu)


↑↑↑

Receptor (Documento, Aplicación, Control TextBox, etc)


↑↑↑

6 Colaboraciones

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

Diagrama de interacción de objetos del patrón Command

Diagrama de interacción de objetos del patrón Command


↑↑↑

Código Enumeracion

''' <summary>
'''  Relaciona las Operaciones posibles con un menu edicion
''' </summary>
Public Enum EOrdenEdicion As Integer
    None
    Cortar
    Copiar
    CopiarTodo
    Pegar
    Eliminar
    SeleccionarTodo
End Enum


↑↑↑

Código en el cliente

'-----------------------------------------------------------------
' 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


↑↑↑

Código en el Invocador

'----------------------------------------------------------------
  ''' <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


↑↑↑

Código en la ordenConcretaCortar

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


↑↑↑

7 Consecuencias

El Patrón Command tiene las siguientes consecuencias:

  1. La Orden (Command) desacopla el objeto que invoca la operación de aquel que sabe como realizarla
  2. Las órdenes son objetos de primera clase. Pueden ser manipulados y extendidos como cualquier otro objeto.
  3. Se pueden ensamblar órdenes en una orden compuesta. Un ejemplo lo constituye la clase [OrdenConcretaCopiarTodoMacro] que se describe mas adelante. En general, las órdenes compuestas son una instancia del Patrón Composite (aunque no siempre).
  4. Es fácil añadir nuevas órdenes, ya que no hay cambiar las clases existentes.


↑↑↑

8 Implementación

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.


↑↑↑

Código OrdenConcretaCopiarTodoMacro

'-------------------------------------------------------------------------------
''' <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


↑↑↑

9 Código de ejemplo

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


↑↑↑

10 Código Esquemático del Patrón

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


↑↑↑

11 Patrones relacionados

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.


↑↑↑

12 – Relacion de Enlaces

GOF

WikiCommand

LENGUAJE_PATRONES


↑↑↑

Mas Enlaces

Otros Ejemplos de implementación


↑↑↑

13 - Datos de catalogación bibliográfica

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

↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Documento Start]
[Imprimir el Documento]
© 1.997- 2.008 - La Güeb de Joaquín
Joaquin Medina Serrano
Ésta página es española