10 de enero de 2015

Patrones de diseño en Visual FoxPro (Parte 1/2)

Texto original: Design Patterns in Visual FoxPro
http://www.tightlinecomputers.com/Documents/Des_Patterns.ZIP

Autor: Andy Kramek
Traducido por: Ana María Bisbé York

Resumen

Los patrones de diseño ofrecen un lenguaje estándar para reconocer, definir, y describir soluciones a problemas de la creación de aplicaciones. El conocimiento de los patrones de diseño, hace más fácil entender los sistemas existentes y describir los requerimientos para nuevos sistemas complejos. Sin embargo, es importante indicar que los patrones de diseño, no son, de por sí, la solución a problemas específicos. Son vías sencillas de identificación de los problemas y una descripción genérica de soluciones, que han sido probadas por la experiencia. La implementación actual del diseño de patrones sigue siendo el trabajo de un desarrollador de aplicaciones.

¿Qué son patrones de diseño?

Antes de comenzar a bucear en ejemplos específicos de cómo implementar patrones de diseños específicos en Visual FoxPro, debemos comenzar por definir qué entendemos por "Patrones de Diseño". Se han propuesto varias definiciones y quizás la más reconocida es la que aparece en el trabajo "Design Patterns, Elements of Reusable Object-Oriented Software" (Patrones de Diseño, elementos de reutilización de aplicaciones orientadas a objetos) escrito por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides (comúnmente conocidos por la pandilla de los cuatro o simplemente como "GoF"). Ellos ofrecen en el primer capítulo de su libro "What is a Design Pattern" (¿Qué es el diseño de patrones?) la siguiente definición:

"Un diseño de patrones nombra, abstrae e identifica los aspectos claves de una estructura de diseño común que lo hace útil para la creación de un diseño orientado a objetos reutilizable. El diseño de patrones identifica la participación de clases e instancias, sus roles y colaboraciones, y la distribución de responsabilidades."

Esta es una buena definición, porque encapsula los cuatro elementos de cualquier diseño de patrones.

Primero, que tiene un nombre. Es vital, porque permite a los desarrolladores superar uno de los problemas fundamentales del diseño de aplicaciones – ¿Cómo comunicar lo que haces a otros? Recordamos bien, cómo hemos empleado cerca de tres cuartos de hora sentados en un hotel de Alemania, debatiendo sobre un problema de una aplicación con un colega. Fue tortuosamente difícil la explicación, que incluyó varios bocetos (si, por supuesto, ¡en servilletas de papel!) y batallamos para comprender de qué se trataba, cuando… algo ocurrió de repente. ¡El estaba implementando una estrategia de patrones! Si nuestro amigo hubiera empezado por ahí, hubiéramos ahorrado mucho tiempo, porque todos hubiéramos sabido inmediatamente (al menos en términos generales) cuál era el problema, y la estrategia que debíamos utilizar para tratar de solucionarlo.

Segundo, abstrae el problema. Esto es referido por "GoF" como el "objetivo". Nos dice la naturaleza del problema y la solución descrita por el patrón. Considere el problema frecuente de advertir a los usuarios de no crear múltiples instancias de una aplicación. Los usuarios, por lo general, tratan de comenzar nuevas instancias de la aplicación porque, al tener minimizada la pantalla principal, se olvida que está activa y hace clic en el icono del escritorio, en lugar de de maximizar la instancia existente. Podemos ver inmediatamente que el patrón Singleton es relevante porque su objetivo se da como "Asegura una clase que tiene una sola instancia y provee un punto global de acceso a ella".

Tercero, define un diseño de estructura. Es importante darse cuenta que el diseño de patrones no proporciona soluciones. Describe estructuras que permiten solucionar un problema de manera que resulte más fácil de reutilizar que si se escribe simplemente el código para solucionar el problema en el contexto en el que surge. Todos hemos pasado por estas experiencias de darnos cuenta que hemos solucionado ya un problema particular en algún lugar en nuestro código, pero no podemos re-utilizarlo, porque no hicimos una solución aislada de la situación. El objetivo del diseño de patrones es ayudarnos a reconocer las situaciones como estas y evitarlas.

Cuarto, que identifica la distribución de responsabilidades. Esto es, por supuesto, la clave a todos los tópicos de diseño y no está limitado al diseño de patrones. Después de todo, una vez conocido lo que tiene que hacer una clase (u objeto), escribir el código para hacerlo es relativamente fácil. La ventaja del diseño de patrones es que una vez que haya reconocido el problema y lo haga corresponder con un patrón, el patrón nos dirá cómo debemos asignar las responsabilidades y por consiguiente nos ayudará a crear rápidamente una solución.
Nuestra intención, en esta sesión, no es ofrecer una revisión exhaustiva de todos los patrones de diseño conocidos (existen libros enteros dedicados a eso). A continuación discutiremos algunos de los patrones más comúnmente encontrados y mostraremos cómo puede utilizar el Visual FoxPro para implementar una solución. Comencemos por ver lo que se conoce como la madre de los patrones, el puente (the Bridge).


¿Qué es un puente (bridge) y cómo se debe utilizar?

El puente es el patrón más básico de todos y lo encontrará, en la medida que se familiarice más con los patrones, en general. Notará entonces, que el puente (en alguna forma) se encuentra en la cima de casi todos los otros patrones. Es por esto que se le conoce como la "madre" de los patrones de diseño.

¿Cómo puedo reconocer cuando necesito un puente?

La definición formal de un puente, tomada del Gof es:

"Desacoplar una abstracción de su implementación de tal forma que las dos puedan variar independientemente."

Espectacular, ¿eh? ¿Hacemos esto realmente en nuestro código? La respuesta corta es que probablemente nosotros lo hacemos más de lo que nos damos cuenta. Vamos a tomar un ejemplo común.

Al escribir código en nuestros formularios y clases queremos naturalmente atrapar las cosas que pueden salir mal y usualmente (considerándonos desarrolladores) queremos decir a los usuarios cuándo van a ocurrir. La solución más obvia y sencilla, es incluir un par de líneas de código en el método apropiado. El resultado puede verse así:

IF NOT <Una función que devuelve True/False>
  lcText = "Chequeo fallido para devolver el valor correcto" + CHR(13)
  lcText = lcText + "Presione cualquier tecla para re-entrar el valor"
  WAIT lcText WINDOW
  RETURN .F. 
ENDIF 

En toda nuestra aplicación pueden existir docenas de situaciones donde mostramos una ventana Wait de este tipo, para mantener al usuario al corriente de lo que está ocurriendo, y esto funciona perfectamente bien mientras ocurre una de las dos cosas. Cualquier usuario le dirá que realmente odia estas molestas ventanas de espera "wait windows" y preferirá una ventana estilo "message box", o peor, tendremos que implantar el código en un entorno que no soporta una ventana tipo "wait windows". Es posible lograrlo con un componente COM, o en un formulario Web. Ahora podemos ir y buscar cada ocurrencia de este código por nuestra aplicación, para cambiarla para que garantice el nuevo requerimiento. No sabemos usted; pero en nuestra experiencia, las posibilidades de hacer esta tarea correcta la primera vez (no olvide ninguna ocurrencia y re-codifique cada una perfectamente) están muy cerca de cero. Incluso, si confiamos en que nuestros resultados son correctos, tendremos que hacer un grupo de verificaciones para asegurarnos.

Entonces, ¿qué tiene que ver esto, con el patrón Puente? Bueno, la razón de que tenemos ese problema es porque hemos fallado al reconocer que estábamos acoplando una abstracción (mostrando un mensaje al usuario) y su implementación (la "Wait window"). Si hubieramos hecho un puente en su lugar, hubiéramos evitado el problema. Así es como se muestra el mismo código si lo hubiéramos implementado, utilizando un patrón de puente.

IF NOT <Una función que devuelve True/False>
  lcText = " Chequeo fallido para devolver el valor correcto" + CHR(13)
  loMsgHandler = This.oMsgHandler
  loMsgHandler.ShowMessage( lcText )
  RETURN .F. 
ENDIF 

¿Ve la diferencia? No sabemos más, ni nos preocuparemos por cómo se mostrará el mensaje (por eso tampoco necesitamos la línea "Presione cualquier tecla", que puede ser agregada en el manipulador de mensaje si es requerido). Todo lo que necesita conocer es dónde tomar una referencia al objeto que va a manipular el mensaje por nosotros, (por supuesto, esto es posible, porque en este requerimiento todo posible manipulador implementará la interfaz apropiada, en este caso el método ShowMessage()).

Este es el origen de la referencia, que es un puente. En este ejemplo la propiedad "oMsgHandler" proporciona el puente entre el código que requiere un mensaje y el mecanismo para manipularlo. Ahora, todo lo que necesitamos es cambiar la forma en que nuestro mensaje es manipulado, para cambiar el objeto de referencia guardado en esta propiedad. Esto es algo que pudo haberse hecho incluso en tiempo de ejecución, independientemente del entorno en el que el objeto padre se haya instanciado. Esta estrategia desacopla exitosamente la abstracción de la implementación y, como resultado, nuestro código, es mucho más reutilizable.

¿Cuáles son los componentes de un puente?

Un puente tiene dos componentes esenciales, la "abstracción", que es el objeto responsable de inicializar una operación, y la "implementación", que es el objeto que la lleva a cabo. (Figura 1) La abstracción conoce su implementación porque guarda una referencia a la misma, o porque lo posee (por ejemplo, lo contiene). Observe que, a pesar de esta estructura, el diagrama denota que la abstracción crea la implementación, y que no es, en absoluto, un requerimiento del patrón. Un puente también puede hacer uso de una referencia ya existente para una misma implementación de objeto. De hecho, diseñar puentes de esta forma, puede ser muy eficiente porque diferentes objetos abstracción pueden ser utilizados en un mismo objeto implementación.


Figura 1. El patrón de puente básico

Debido a que Visual FoxPro tiene un modelo de contenedores muy bueno, la mayoría de los desarrolladores notarán que lo han utilizado (no intencionalmente) para implementar Puentes con más frecuencia de lo que imaginan. Sin embargo, no confunda el envío de mensajes con el puente. Si un método de un botón de comandos llama a un método en su formulario padre, esto implementa directamente la acción necesaria, que es enviar un mensaje, no un puente. Sin embargo, si el método del formulario va a llamar a otro objeto para que lleve a cabo la acción, entonces tenemos un puente.

Interesante, otra posibilidad ocurre cuando un formulario (u otro contenedor) tiene una propiedad que guarda una referencia a un objeto de implementación. Un objeto contenedor (por ejemplo un botón de comandos en un formulario) puede acceder a esta propiedad directamente y tomar una referencia local al objeto de implementación. Puede entonces, llamar a los métodos del objeto de implementación directamente. Ahora, ¿este es un puente, o un mensaje enviado? De hecho, probablemente podría argumentar ambos lados del caso con igual justificación. Sin embargo, la respuesta en realidad no importa, Este asunto surge sólo porque Visual FoxPro expone las propiedades como "Públicas" de forma predeterminada, y también implementa puntos hacia atrás, los que permiten a los objetos dirigirse a sus padres directamente. En la mayoría de los entornos orientados a objeto, es sencillamente imposible para los objetos comportarse tan toscamente.

Entonces, para implementar un puente necesita identificar qué objeto va a realizar la función de abstracción y cuál la implementación. Una vez que lo haya definido, todo lo que queda por decidir es cómo el puente entre los dos será codificado y esto dependerá enteramente del escenario.

¿Cómo implementar un puente? (Ejemplo: FrmBridge.scx, CH15.vcx::cntTaxRoot)

El formulario de ejemplo (Figura 2) ilustra un ejemplo en acción del "puente contenedor" en Visual FoxPro. En este caso, el formulario es la abstracción y es el responsable de manipular la visualización de un valor calculado. Una clase custom, llamada cntRoot, es la implementación que es responsable de calcular el valor. El formulario tiene una instancia de la clase (que es un contenedor sencillo con una propiedad protegida nTaxRate y métodos llamados SetRate(), GetRate() y CalcTax().) Un método DoCalc() del formulario implementa el puente para calcular el objeto que toma el valor calculado y luego actualiza la visualización en concordancia.

El código existente es trivial

WITH ThisForm
  *** Toma el precio base
  lnPrice = .txtPrice.Value
  *** ¡Aquí está el puente! Calcula el tax (impuesto)   lnTax = .oCalc.CalcTax( lnPrice )
  *** Y el total
  lnTotal = lnPrice + lnTax
  *** Actualiza la visualización
  .txtTax.Value = lnTax
  .txtTotal.Value = lnTotal
ENDWITH 

Como puede ver, los objetos del formulario, y el formulario por sí mismos, son totalmente aislados de los detalles del proceso de calcular la tasa de impuesto.


Figura 2. El patrón de puente en uso (FrmBridge.scx)

Todos los formularios necesitan conocer si tienen una llamada al método CalcTax(), esto devolverá un valor que puede entonces, ser utilizado. Por ahora, el objeto oCalc no tiene conocimiento de cuál es el otro final del puente y por qué necesita la información. Todo lo que hace es responder al requerimiento de la información.

Un proceso similar es utilizado por el método personalizado del formulario DoRate(), al que llaman los métodos GetRate() o SetRate() del objeto oCalc, en función de si hay o no, un valor introducido en el cuadro de texto "Current rate". Nuevamente, el código a nivel de formulario es trivial.

WITH ThisForm
  lnRate = .txtRate.Value
  IF EMPTY( lnRate )
    *** Toma la tasa de impuesto
    lnRate = .oCalc.GetRate()
  ELSE
    *** Establece la tasa
    .oCalc.SetRate( lnRate )
  ENDIF
  *** Actualiza la visualización
  .txtRate.Value = lnRate
ENDWITH 

Sin embargo, lo importante de este ejemplo no tiene nada que ver con este código. Lo que importa es, que tenemos desacoplado el mecanismo para mostrar la información de la tasa de impuesto, del mecanismo de la visualización de la información de la tasa y el proceso que la calcula – y esto es la esencia del patrón de puente. Por ejemplo, podemos fácilmente utilizar el objeto que calcula la tasa de impuesto desde una línea de comando, de esta forma:

oCalc = NEWOBJECT( "cntTaxRoot", "ch15.vcx" )
oCalc.SetRate( 5.75 )
? oCalc.CalcTax( 27.54 ) 

Y podemos, igualmente bien, cambiar el objeto que utiliza el formulario, garantizando sólo que cualquier objeto utilizado por el formulario se configura para la interfaz esperada (lo que significa que expone métodos llamados CalcTax() y GetRate() y devuelve valores numéricos y un método llamado SetRate() que acepta un valor numérico). Los dos elementos en este ejemplo son realmente independientes unos de otros.

Resumen de patrón de puente

Hemos ilustrado este puente al utilizar un objeto arrastrando y soltándolo en un formulario y nombrándolo específicamente en tiempo de diseño. Sin embargo, recuerde que podemos igualmente tener una propiedad definida para guardar el nombre del objeto, o incluso, creado el objeto en tiempo de ejecución y guardar la referencia a el. Los distintos mecanismos son meramente detalles de implementación, el patrón se mantiene en cada situación, y eso es lo importante.

¿Qué es una estrategia y cómo la utilizo?

La estrategia describe una solución al problema principal inherente a escribir el código principal – como accionar con demandas inesperadas para cambios de implementación.

La definición formal de estrategia, fue dada por el "GoF" es:

"Define una familia de algoritmos, encapsula cada uno, y los hace intercambiables. La estrategia permite al algoritmo variar en dependencia del cliente que la utilice."

Esto resulta algo oscuro en una primera lectura, vamos a verlo en un ejemplo frecuente donde el patrón de estrategia puede ayudar.

En el ejemplo anterior mostramos cómo utilizar un puente para desacoplar los procesos de calcular la tasa de impuestos de una cantidad, a las tareas de visualizarla. Esta solución, trabaja bien cuando existe sólo una implementación; pero no puede cubrir situaciones cuando se requieren diferentes implementaciones en tiempos diferentes.

Por ejemplo, en nuestra localidad, la tasa de ventas en vestuario es 5.75%; pero si viajamos 20 millas al sur, la tasa base es sólo del 5.25%, mientras, que si viajamos dentro del mismo estado (sólo a 50 millas) no existen tasas para vestuario de ningún tipo. El mismo elemento de vestuario etiquetado como $29.95 puede, sin embargo, costarnos $31.67, $31.52 o $29.95 según donde compremos.

Teniendo esto en cuenta para cualquier otro elemento, la tasa aplicable a una transacción depende del contexto (la combinación específica de tipo de elemento y localización), en el cual tiene lugar la transacción. Si hubiéramos escrito el código en la aplicación para controlarlo podríamos comenzar con algo como esto:

DO CASE
  CASE lclocale = "Akron"
    IF lcItemType = "Vestuario"
      lnTaxRate = 5.75
    ELSE
      *** Otros elementos aquí
    ENDIF
  CASE lclocale = "Canton"
    IF lcItemType = "Vestuario"
      lnTaxRate = 5.25
    ELSE
      *** Otros elementos aquí
    ENDIF
  CASE lclocale = "Grove City"
    IF lcItemType = "Vestuario"
      lnTaxRate = 0.00
    ELSE
      *** Otros elementos aquí
    ENDIF
  OTHERWISE
    *** Aplicar un valor predeterminado
    lnTaxRate = 5.50
ENDCASE 

Podemos ver inmediatamente que un problema surgirá. Ocurrirá, cuando necesitemos agregar "Cleveland" a nuestra lista de locales, o cuando Akron, desalentado por las pérdidas en ventas de Canton, corta sus tasas para vestuario. Necesitamos cambiar nuestro código con todos los riesgos que conlleva. Más adelante, como vamos a aumentar la cantidad de localidades, este código será inmanejable. Por supuesto, debemos guardar esta información en una tabla (una columna para localidad, y quizás, una columna para cada elemento) y volver a mirarlo cada vez que sea necesario. De esta forma, lo único que tenemos que hacer es, modificar registros cuando cambie el dato; pero la tabla podría crecer rápidamente y puede haber muchos datos duplicados y redundancia de los datos, lo cual no es tampoco la solución ideal.

Lo que queremos realmente que haga nuestra aplicación es que sea capaz de pasar el precio del ticket, y la información de la localidad, algo que nos diga qué tasa debemos aplicar. Sin embargo, sin mover el código (lo cual es re-localizar; pero no cambiar el problema) un puente no es la solución porque solo tiene una única implementación.

El patrón de estrategias nos permite definir clases para cada situación que podemos encontrar e instanciar la apropiada en tiempo de ejecución. Esto podría significar que podemos reemplazar todo el código citado anteriormente en nuestra aplicación con solo una línea que nunca necesitará ser modificada independientemente de que cambie la situación.

lnTax = This.oCalcóTax( nPrice, lcLocale , lcItemType ) 

¿Cuáles son los componentes de una estrategia?

Una estrategia tiene tres componentes esenciales: "la estrategia abstracta", que es una clase que define la interfaz, y funcionalidad genérica para las "estrategias concretas", que son subclases que definen varias implementaciones posibles. El tercer componente, el "Contexto", es responsable de controlar la referencia a la implementación actual (Figura 3).


Figura 3. El patrón de estrategia básico.

Si está pensando que este diagrama se ve muy similar al puente, está en lo cierto. Puede pensar incluso, que el patrón de estrategia es como un "puente dinámico" en el cual un final (el contexto) es estático; pero el otro (la estrategia) está creado por varias variantes con el propósito de entregar una implementación específica en un momento específico. La esencia del patrón es tal que la decisión como establecer qué subclase debe ser instanciada en cualquier momento dependa del estado del "cliente". El ejemplo clásico del patrón descansa en la responsabilidad de instanciar la estrategia concreta del objeto en el cliente, con la que pasa la referencia al contexto.

Para implementar un patrón de estrategia, necesita definir la clase de estrategia abstracta y cuántas subclases especificas necesita. El objeto, que va a jugar el rol de contexto (en VFP es típicamente el formulario, o contenedor padre), necesita exponer una interfaz apropiada a sus clientes potenciales y además, requiere una propiedad que es utilizada para guardar la referencia actualmente activa de implementación de objeto.

¿Cómo implemento una estrategia? (Ejemplo: FrmStrat.scx, CH15.vcx::cntTaxStrat01…04)

El formulario de ejemplo (Figura 4) ilustra cómo se puede utilizar un patrón de estrategia para implementar una solución a las tasas de venta cosas que hemos discutido en el preámbulo de este tópico. En este caso el botón Add Tax (Agregar Tasa) es el cliente y el formulario, el "contexto".
La clase cntTaxRoot que utilizamos en el ejemplo del puente en la sección precedente había sido subclaseada cuatro veces (clases cntTaxStrat01 hasta cntTaxStrat 04) y cada subclase está predefinida con una tasa de impuesto específica (por establecer la propiedad nTaxRate en la subclase). Finalmente, el método Load() del formulario, es utilizado para crear y llenar un único cursor con una lista de ciudades las que son cerradas (keyed) a una de las cuatro subclases cntTaxStrat – este cursor está utilizado como la fuente de la lista desplegable.


Figura 4. El patrón de estrategia en uso (FrmStrat.scx)

Cuando se hace clic sobre el botón "Add Tax", el código siguiente, en su método OnClick(), es ejecutado. Simplemente toma el ID de localización y el precio actual desde el formulario y los pasa al método personalizado del formulario DoCalc(). El método personalizado DoCalc() devuelve el monto apropiado de tasa, y el resto del código actualiza la visualización correspondiente.

LOCAL lnPrice, lnTax, lnTotal
STORE 0 TO lnPrice, lnTax, lnTotal
WITH ThisForm
  *** Toma el "contexto" (Localización y precio)
  lcLocn = ALLTRIM( .cbolocation.value )
  lnPrice = .txtPrice.Value
  *** Obtener la tasa utilizando la estrategia   lnTax = .DoCalc( lcLocn , lnPrice )
  *** Calcular el total
  lnTotal = lnPrice + lnTax
  *** Modificar la visualización
  .txtTax.Value = lnTax
  .txtTotal.Value = lnTotal
ENDWITH

Todo el trabajo, obviamente comienza con el método DoCalc() del formulario. Sin embargo, no existe tanto código como podría esperar. Espera recibir dos parámetros – la llave del contexto (la cual, debido a que viene de la lista desplegable, será un carácter de cadena) y el "precio" sobre el que es calculada la tasa.

La llave de contexto es utilizada para generar un nombre de la subclase requerida. Si la clase instanciada actualmente no es correcta, sencillamente instanciaremos la correcta. Naturalmente, todas las subclases heredan del método CalcTax(). Así, va a re-llamar, y utiliza cualquier valor configurado en la propiedad nTaskRate, si no hay tasa pasada explícitamente. Entonces, podemos obtener la cantidad correcta de tasa simplificando la llamada del método de la subclase CalcTax() con el precio que tienen. El valor retornado del método, es entonces retornado al cliente.

LPARAMETERS tcContext, tnPrice
WITH ThisForm
  *** Define el nombre del objeto de la etrategia
  lcStrategy = "CNTTAXSTRAT" + PADL( tcContext, 2, '0' )
  *** Ya está ahi
  IF ISNULL( .oStrategy ) OR NOT UPPER( .oStrategy.Name ) == lcStrategy
    *** Necesitamos crear este
    .oStrategy = NEWOBJECT( lcStrategy, 'Ch15.vcx' )
  ENDIF
  *** Ahora, es sólo llamar al método CalcTax() method y pasar el precio
  lnTax = .oStrategy.CalcTax( tnPrice )
  RETURN lnTax
ENDWITH 

El ejemplo ilustra una vía de implementar una estrategia – en el cual tenemos el objeto de contexto responsable de ambos, la instanciación y el mantenimiento del objeto estrategia. Esto tiene sentido en el escenario y con la interfaz ilustrada. Otros escenarios pueden requerir diferentes implementaciones.

Por ejemplo, considere la aplicación de descuentos en una orden. La orden de entrada del formulario (el contexto) puede necesitar la aplicación de diferentes tipos de descuentos a una orden:
  • Elemento de cantidad de descuentos, aplicado a una línea de un elemento cada vez.
  • Descuento del valor de la orden, aplicado al valor total de la orden.
  • Elemento de descuento especial o promoción.
  • Descuento a clientes, aplicado al valor de la orden basado en el usuario
Una posible interfaz utilizará botones de comandos (los clientes), de tal forma que el operador pueda determinar cuando aplicar un tipo de descuento específico. En este escenario puede ser enteramente apropiado, y mucho más simple, para cada botón de ser el responsable de instanciar la subclase de estrategia del descuento apropiado y almacenar su referencia a la propiedad del formulario. (Recuerde que mientras podemos hacer este tipo de cosas fácilmente en Visual FoxPro, otros lenguajes podrían pasar una referencia explícitamente) El formulario puede manipular el cálculo actual y ocuparse de que el resultado sea tal que el código no tenga que ser duplicado en cada botón.

Resumen de patrones de estrategia

Hemos mostrado una implementación posible del patrón de estrategia y bosquejado otro. La ventaja de la estrategia es tal que evita la necesidad de tener que incrustar implementaciones alternativas en el código permitiéndonos crear objetos separados, la cual va a compartir una interfaz común, para cada opción y para instanciar solamente el objeto necesario en tiempo de ejecución. Como otros patrones los detalles de la implementación pueden variar con las circunstancias pero el patrón como tal no cambia.

¿Qué es una cadena de responsabilidad y cómo utilizarlo?

En la sección precedente hemos constatado que el patrón de estrategia describe una solución del problema de tratar con implementaciones alternativas en tiempo de ejecución. La cadena de responsabilidades es otra vía de afrontar básicamente el mismo problema.

¿Cómo puedo reconocer cuando necesito una cadena de responsabilidad?

La definición formal de cadena de responsabilidad dada por "GoF" es:

"Evitar el acoplamiento del emisor del requerimiento y el receptor, al obtener más de un objeto con posibilidad de tratar el requerimiento. Es la cadena de objetos que reciben y pasan la petición a lo largo de la cadena hasta que un objeto los manipule."

En el ejemplo previo mostramos como utilizar una estrategia sin que importe el problema de aplicar una localización específica de índices de ventas en tiempo de ejecución. Sin embargo, como hemos visto, para implementar una estrategia de algunos objetos, en algún lugar tiene que decidir, en tiempo de ejecución, cuáles son las posibles subclases a ser implementadas. Esto puede no ser siempre deseado, o incluso, posible.

En una cadena de responsabilidades cada objeto sabe cómo evaluar la petición para una acción y, si no la puede controlar por si mismo, sabe solamente cómo pasarla a otro objeto, por ello la "cadena". La consecuencia de esto es que el cliente, (que inicia la petición de la acción) ahora sólo necesita conocer sobre el primer objeto en la cadena. Por otra parte, cada objeto en la cadena, solo necesita conocer también sobre un objeto, el siguiente en la cadena. La cadena de responsabilidades puede ser implementada utilizando una cadena predefinida o estática, o las cadenas se pueden construir en tiempo de ejecución teniendo cada objeto su propio sucesor cuando sea necesario.

¿Cuáles son los componentes de una cadena de responsabilidad?

Una cadena de responsabilidad puede ser implementada al crear una clase "manipuladora" (handler) abstracta (para especificar la interfaz y funcionalidad genérica) y crear subclases concretas para definir varias posibles implementaciones (Figura 5). Sin embargo, no existen requerimientos absolutos para todos los miembros de la cadena de responsabilidades, para descender a partir de la misma clase, proporcionando que todos ellos soporten la interfaz necesaria para integrar con otros miembros de la cadena. Los objetos clientes necesitan una referencia a la subclase específica, la cual es su punto de entrada individual a la cadena. (Observe que no todos los clientes necesitan utilizar el mismo punto de entrada).

Observe que, hasta ahora, hemos visto el patrón de puente básico, porque cada enlace en la cadena es en realidad un puente entre una abstracción y una implementación. Lo diferente es que un único objeto puede desempeñar varios roles en dependencia con su situación. De esta manera, el primer enlace tiene el cliente como abstracción y el primer handler concreto, como la implementación. Sin embargo, el segundo enlace tiene ahora el primer handler desempeñando el rol de abstracción y el segundo handler como implementación. Este patrón puede, en teoría al menos, repetirse hasta infinito.


Figura 5. El patrón de Cadena de responsabilidades básico.

Con el objeto de implementar un patrón de cadena de responsabilidades, necesita definir una clase handler abstracta y tantas subclases diferentes como necesite. La diferencia con el patrón de estrategia es que la clase abstracta debe definir el mecanismo, por el cual un objeto puede determinar si puede manipular el requerimiento para todos los manipuladores o pasarlos a una propiedad para guardar una referencia al objeto siguiente en la cadena. De hecho, no existen requerimientos absolutos para todos los manipuladores que hereden del mismo manipulador abstracto, proporcionando que ellos adhieran la mínima interfaz definida.

¿Cómo implementar una cadena de responsabilidades? (Ejemplo: FrmChor.scx, CH15.vcx::cntTaxChain01…04)

El formulario de ejemplo (Figura 6) ilustra cómo podemos utilizar un patrón de cadena dinámica de responsabilidad para implementar una solución para temas de índices de ventas que ya hemos debatido en secciones previas. En este caso, el botón "Add Tax" (Agregar índice) delega responsabilidad al formulario, llamando a su método personalizado DoCalc(). El formulario actúa como el cliente y guarda una referencia al primer objeto en la cadena, el que es creado, cuando necesita, explícitamente en el método DoCalc().

LPARAMETERS tcContext, tnPrice
LOCAL lnTax
WITH ThisForm
  *** Verificar que tenemos disponible el primer objeto de la cadena
  IF VARTYPE( This.oCalc ) # "O"
    *** We don't so create it
    .oCalc = NEWOBJECT( 'cntTaxChain01', 'ch15.vcx' )
  ENDIF
  *** Llamamos al método ProcessRequest() y pasamos el contexto y precio
  lnTax = .oCalc.ProcessRequest( tcContext, tnPrice )
  IF ISNULL( lnTax )
    *** No es posible procesar la petición
    MESSAGEBOX( "No es posible procesar esta localidad", 16, 'Error' )
    lnTax = 0
  ENDIF
  RETURN lnTax
ENDWITH 

Observe que este código, en el botón Add Tax en el formulario, es idéntico a la petición de estrategia (frmStrat.scx); pero el código en el método DoCalc() de este formulario es ligeramente diferente.


Figura 6. La cadena de responsabilidades en uso (FrmChor.scx)

La diferencia está en la clase utilizada para realizar el cálculo del índice. Para este ejemplo, hemos creado una nueva subclase de la clase cntTaxRoot, llamada cntTaxChain. Esta clase agrega cuatro propiedades protegidas (Tabla 2) y un método llamado "ProcessRequest"


PropiedadesDescripción
cCanHandlePropiedades utilizadas para definir el manipulador contexto de esta subclase.
cNextObjNombre de la clase para instanciar como el próximo objeto en la cadena.
cNextObjLibBiblioteca de clases para el próximo elemento de la cadena.
oNextReferencia del objeto para el próximo elemento de la cadena.
Tabla 2. Propiedades de la clase handler de la cadena de responsabilidades

El método ProcessRequest(), es responsable por determinar si la petición entrante es manipulada localmente. Si es así, llama simplemente el método predeterminado CalcTax() en la clase original raíz. Si no, la acción depende de si es definido otro objeto, y disponible, para manipular la petición, como sigue:

LPARAMETERS tcContext, tnPrice
LOCAL lnTax
WITH This
  *** ¿Podemos tratar la petición aquí?
  lcCanHandle = CHRTRAN( .cCanHandle, "'", "" )
  IF tcContext = lcCanHandle
    *** Sí, entonces llamamos al método CalcTax(), pasando el precio
    lnTax = .CalcTax( tnPrice )
  ELSE
    *** No podemos tratarla, ¿Hay un objeto definido para pasársela?     IF NOT EMPTY( .cNextObj ) AND NOT EMPTY( .cNextObjLib )
      *** Si, pero ya existía antes (Yes we do. but does it already exist)
      IF VARTYPE( This.oNext ) # "O"
        *** Crea el objeto y lo llama
        .oNext = NEWOBJECT( .cNextObj, .cNextObjLib )
      ENDIF
      *** Llamo al objeto especificado
      lnTax = This.oNext.ProcessRequest( tcContext, tnPrice )
    ELSE
      *** No hay a donde ir, devolvemos NULL
      lnTax = NULL
    ENDIF
  ENDIF
  RETURN lnTax
ENDWITH 

Este es todo el código que necesitamos. Las subclases individuales para este sencillo ejemplo, no tienen código personalizado alguno, todo está manipulado por las propiedades establecidas (incluyendo la propiedad nTaxRate definida en la clase raíz original). Observe que si ejecuta el ejemplo tal y como está y selecciona "Cleveland" como localidad, puede recibir un mensaje de error definido por el método DoCalc(). Esto es porque todo el código es necesario. Esto es porque para procesar la localidad de Cleveland necesitaremos la clase llamada "cntTaxChain04" el cual no se nombra en ningún lugar en la cadena. La cadena finaliza porque la clase cntTaxChain03 no tendrá sus propiedades definidas "cNextObj" y "cNextObjLib". Para remediar esto simplemente establece estas dos propiedades al punto de "cntTaxChain04" y 2CH15.vcx" respectivamente "Cleveland" para convertirse en una localidad disponible.

A pesar de que hemos definido la cadena secuencialmente, no existe una razón real para hacerlo, ni existe ningún requerimiento absoluto para utilizar las propiedades para definir el siguiente objeto de la forma en la cual lo hemos hecho. Otra implementación posible (y muy probable), sería utilizar una tabla de datos para almacenar los detalles de la clase manipuladora y simplemente tener un objeto manipulador que busca una tecla para buscar los detalles del siguiente objeto para instanciarlo.

Resumen de patrón de cadena de responsabilidades

La cadena de responsabilidad nos proporciona otra vía para solucionar el problema de proporcionar la funcionalidad sin la necesidad del código explícito. Sin embargo, la mayor ventaja de la cadena de responsabilidades es la facilidad con que puede extenderse, mientras su inconveniente fundamental es que, puede crecer dramáticamente la cantidad de objetos activos en el sistema. Como siempre, termino recordándole que siempre debe pensar que la implementación actual de detalles puede cambiar, el patrón como tal, no cambia.

No hay comentarios. :

Publicar un comentario

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.