2 de mayo de 2017

Patrones de diseño - El mediador

Artículo original: Design Patterns - The Mediator
http://weblogs.foxite.com/andykramek/archive/2007/01/14/3126.aspx
Autor: Andy Kramek
Traducido por: Ana María Bisbé York


¿Qué es un mediador y cómo lo utilizo?

El mediador describe una solución como una llave, que todos hemos encontrado siempre que tratamos de diseñar una clase nueva. Cómo tratar con situaciones donde un objeto tiene que responder a cambios, o controlar el comportamiento de otro.

¿Cómo reconozco donde necesito un mediador?

La definición formal del mediador, y dada en "Design Patterns, Elements of Reusable Object-Oriented Software" por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides es:

Define un objeto que encapsula cómo interactúan un conjunto de objetos. El mediador estimula la pérdida de acoplamiento ocultando las referencias explicitas entre los objetos, permitiendo variar su interacción de forma independientemente.

Esto nos lleva a uno de los problemas fundamentales que tenemos que afrontar en cualquier tarea de desarrollo de aplicaciones, asegurar que los objetos pueden comunicar con otros sin tener que incluir referencias directas de código en sus clases.

En ningún lugar es esto más crítico, que al crear una interfaz compleja que la actual generación de usuarios finales de PC, no esperan; pero exigen. Típicamente, tenemos que hacer que toda la interfaz de usuario, responda como una única entidad, habilitando e inhabilitando funciones y controles como respuesta a las acciones del usuario u opciones, o, de acuerdo con sus derechos y permisos. Al mismo tiempo, queremos diseñar y generar clases genéricas, reutilizables. Los dos requerimientos están, aparentemente en conflicto directo uno con el otro.

Considere el problema de tener dos cuadros de texto en un formulario. Cuando se introduce un valor en uno, el otro debe mostrar el resultado de algún tipo de cálculo basado en este valor (por ejemplo, al introducir el precio en uno, mostrar el impuesto de venta en el otro). Obviamente, si no hay valor en la fuente, no habría resultado en el destino; pero ¿cómo podemos asegurarnos que el cálculo, sólo se realiza si el valor es mayor que cero?

Por supuesto, la solución más simple es agregar un par de líneas de código al Valid() del cuadro de texto de este formulario que implemente esta funcionalidad, como, por ejemplo:

IF This.Value > 0
  *** Calcular el impuesto y mostrar
  ThisForm.TxtTax.Value = ( This.Value * (5.75/100) ) 
ELSE
  *** Limpiar el valor del impuesto
  ThisForm.TxtTax.Value = 0 
ENDIF

Este tipo de "acoplamiento ligero" puede ser aceptable cuando estamos tratando con un cuadro de texto y un botón de comandos en un único formulario. Sin embargo, rápidamente tendremos problemas si tratamos de adoptar esta solución al tratar con controles múltiples que tienen que interactuar en combinaciones diferentes y complejas. Incluso, encontrando dónde se especifica el código que controla una interacción particular, puede haber problemas, y simplemente cambiar el nombre de un objeto provoca daños mayores. Es aquí donde juega su papel el patrón mediador.

La idea básica es, que cada objeto se comunica con un "mediador" central, el que conoce de todos los objetos que están al alcance actual, y cómo manipular su estado cuando un evento dado es reportado. De esta forma, evitamos todas los problemas asociados con colocar código específico dentro de un método asociado con el evento Valid(). En su lugar, podemos escribir completamente código genérico en su clase padre. Entonces, si utilizamos un objeto mediador, podemos reemplazar el código específico en la instancia del cuadro de texto precio con este código en la clase padre:

This.oMediator.Notify( This )

Como puede ver, el cuadro de texto no tiene idea de qué hará el mediador con la información, o incluso qué información desea. Todo lo que tiene que hacer es llamar al método "Notify" y pasa una referencia a sí mismo. Cualquier acción subsiguiente es para especificar la implementación del mediador.

¿Cuáles son los componentes de un mediador?

Un mediador tiene dos requerimientos esenciales. Primero, necesitamos una clase "mediador" que defina la interfaz, y funcionalidad genérica, para los "mediadores concretos" que son subclases que definen varias posibles implementaciones. El segundo es, que todos los objetos que se encuentran junto a este, deben estar basados en clases que pueden comunicarse con su mediador. La estructura básica del patrón de mediador se muestra a continuación:

Puede ver en este diagrama, que las clases que necesitan trabajar con el mediador necesitan guardar una referencia al objeto mediador. Está pensando probablemente que, en Visual Foxpro, tenemos una clase nativa, candidata para la clase mediador en el formulario, (en el contexto de cualquier interfaz de usuario). Utilizar el formulario como un mediador es bueno porque está siempre disponible a cualquier objeto a través de la referencia 'ThisForm' sin que necesitemos preocuparnos por sus propiedades específicas. Sin embargo, cuando trabajamos con clases no visuales puede no ser la mejor solución, entonces, es mejor definir una clase mediadora genérica, no visual.

Lo segundo que debe observar desde el diagrama es, que el objeto mediador necesita guardar una referencia a todos los objetos colindantes. De aquí surge la pregunta de cómo adquirir y guardar la referencia – que tiene diferentes respuestas. Posiblemente, lo más sencillo de implementar es que cada control defina un método "RegisterMe()", que es creado en cualquier instancia de clase, y verifica la presencia de un mediador y pasa una referencia de si mismo al mediador. Otra posibilidad es que el mediador defina el método "GoFindEm()", que busca el entorno para descubrir objetos. Otra vía de clase mediadora necesitará también mantener una colección (matriz) para guardar las referencias a sus objetos colindantes.

¿Cómo puedo implementar el mediador?

Para implementar un mediador para nuestro pequeño ejemplo de Impuestos de ventas tenemos que hacer una pequeña preparación. Primero, necesitamos una clase basada en Form que pueda manipular un mediador. Esta clase necesitará tres propiedades para controlar el mediador:

  • cmedclass: Nombre de la clase mediador a instanciar por este formulario
  • cmedlib: Biblioteca de clases de la clase mediador a instanciar por este formulario
  • omediator: Propiedad expuesta para guardar la referencia en el objeto Mediador

Y el siguiente código en su evento Load() (cuando está utilizando Load() debido a que debemos asegurarnos de que el mediador definido esté en su lugar antes de que se instancie el control. En el evento Init() será muy tarde, porque no se dispara hasta que todos los controles no se hayan instanciado).

LOCAL lcMClass, lcMLib
DODEFAULT()
WITH ThisForm
  *** Si es identificada una clase mediadora, la instancia
  lcMClass = ALLTRIM( .cMedClass )
  lcMLib = ALLTRIM( .cMedLib )
  IF NOT EMPTY( lcMClass ) AND NOT EMPTY( lcMLib )
    .oMediator = NEWOBJECT( lcMClass, lcMLib )
  ENDIF
ENDWITH

Por supuesto, debemos ser cuidadosos de limpiar las referencias de objetos, por eso, la clase form incluye el siguiente código en su evento Destroy(), para asegurase de que ocurra la limpieza.

IF VARTYPE( This.oMediator ) = "O"
  This.oMediator.Destroy()
ENDIF

Nota: Esto es necesario, para evitar el problema que surge debido a la secuencia normal de los eventos, que es que los objetos son destruidos en orden inverso a su creación. Debido a que el mediador es creado primero, normalmente será destruido el último, pero, debido a que guarda las referencias de otros objetos, no pueden ser destruidos mientras éste existe. Entonces, necesitamos forzar la destrucción del mediador "fuera de turno" cuando el formulario es liberado, y que los objetos puedan liberarse por si mismos.

Lo siguiente que necesitaremos definir es, una nueva subclase para cada uno de los controles que tendrá que trabajar como mediador. Una nueva propiedad (oMediator) puede ser definida para guardar una referencia de objeto mediador, junto con 3 nuevos métodos:

  • Register(): Llamado desde el Init() del control. Verifica la presencia del mediador. Si es encontrado uno, guarda la referencia local y entonces llama al método mediador Register(), pasando la referencia a ellos.
  • Notify(): Método que llama al método de notificación (también llamado Notify()) en el mediador y pasa una referencia al control actual.
  • UnRegister(): Llamado desde el método Destroy() del control. Verifica la referencia local del mediador. Si encuentra, llama al método UnRegister(), pasando una referencia al mismo y luego libera la referencia local.

Además, los siguientes eventos (cuando se aplican) tienen que ser modificados para llamar al método Notify() del control, ya sean los métodos InterActiveChange() y ProgrammaticChange() para listas, o Valid() para cuadros de textos.

Entonces, necesitamos una clase mediador abstracta, la que define la interfaz, y el comportamiento genérico para el método Register(), Notify() y Unregister() y una colección RegisteredObjects. El código para agregar y quitar objetos a la colección es muy sencillo (yo utilizo típicamente la concatenación de las propiedades "Parent + Name" de un objeto como clave para los elementos en la colección, porque puede haber objetos con el mismo nombre en diferentes contenedores en un formulario) y la única complejidad está en el método Notify().

La forma en que me gustaría implementar esto sería definir métodos propios en subclases concretas para controlar las interacciones para cada objeto con el que hay que trabajar. El código en el método Notify del mediador puede ser genérico y simplemente verificar si tiene un método para el objeto actual. He aquí el código:

LPARAMETERS toObjRef
LOCAL lcKey, lcMethod, lnItem
*** Guarda en una variable local el valor de las propiedades parent + name
lcKey = UPPER( ALLTRIM( toObjRef.Parent ))
lcKey = lcKey + UPPER( ALLTRIM( toObjRef.Name ))
*** y el método destino estará basado en el nombre
lcMethod = "CHANGE_" + UPPER( ALLTRIM( toObjRef.Name ))
*** ¿Tenemos el objeto registrado?
lnItem = This.RegisteredObjects.GetKey( lcKey )
IF lnItem > 0
  *** Lo tenemos, vamos a hacer alguna acción sobre el.
  IF PEMSTATUS( This, lcMethod, 5 )
    *** Llama al método y pasa una referencia al objeto original
    EVALUATE( "This." + lcMethod + "( toObjRef )" )
  ENDIF
ENDIF
*** A partir de aquí, solo salir
RETURN

Entonces, podemos crear una subclase específica para implementar el comportamiento específico que queremos tener en nuestros formularios al implementar métodos propios que van a llamar al método Notify(). Para nuestro sencillo ejemplo, el método ChangeTXTPrice() puede tener este aspecto:

LPARAMETERS toCalledBy
LOCAL lcKey, lnItem, loTgt, lnVal
*** ¿Tenemos un cuadro de texto que se llame TXTTAX?
lcKey = UPPER( ALLTRIM( toObjRef.Parent ))
lcKey = lcKey + "TXTTAX" 
lnItem = This.RegisteredObjects.GetKey( lcKey )
IF lnItem > 0
  ***Sí, está registrado, así que creamos una referencia a el.
  loTgt = This.RegisteredObjects.Item( lnItem )
  *** Tomamos el valor del objeto origen
  lnVal = toObjRef.Value
  IF lnVal > 0
    loTgt.Value = (lnVal* (5.75/100)) 
  ELSE
    loTgt.Value = 0
  ENDIF
  loTgt.Refresh()
ENDIF
RETURN

Existen muchas formas de implementar un mediador concreto; pero la metodología descrita aquí ilustra el principio básico. Finalmente tenemos que crear un formulario que utilice estas clases.

¡Vaya! Parece que es mucho trabajo por hacer. Especialmente porque lo que queremos hacer es actualizar un cuadro de texto basado en el valor de otro. Sin embargo, notará que todo, con excepción de la creación del archivo mediador que implemente el comportamiento específico, es enteramente genérico y es, por tanto, completamente re-utilizable. En otras palabras, sólo tiene que hacerlo una vez.

Es importante enfatizar en cuánta flexibilidad hemos ganado haciendo todo esto. Todo este código que controla la interacción entre cualquier cantidad de objetos en el formulario, está ahora concentrado en un único objeto, que es específico del formulario, pero puede ser mantenido desde fuera de el. Para cambiar el comportamiento, puede modificar la subclase existente, o crear una nueva que la implemente pero sólo cambia un par de propiedades en el formulario.

Resumen de patrón mediador

Implementar el patrón mediador indudablemente requiere más planificación y mucho más preparación que cualquiera de los patrones que hemos discutido antes. Sin embargo, en situaciones donde tenemos que controlar interacciones complejas, esto compensa ampliamente el esfuerzo en tres vías:

  • Primero, simplifica el mantenimiento, al localizar su posible comportamiento, en caso contrario se esparce entre diferentes clases en un mismo objeto.
  • Segundo, evita la necesidad de acoplar los objetos.
  • Tercero, esto simplifica la lógica al reemplazar las interacciones "muchos a muchos" entre controles individuales con interacciones "uno a muchos" entre el mediador y los colegas.

El problema principal del patrón es que, especialmente al tratar con gran número de interacciones, el mediador por sí mismo se puede tornar muy complicado y dificultar su mantenimiento.

No hay comentarios. :

Publicar un comentario