30 de abril de 2017

Patrones de diseño - La cadena de responsabilidad

Artículo original: Design Patterns - The Chain of Responsibility
http://weblogs.foxite.com/andykramek/archive/2006/12/24/3060.aspx
Autor: Andy Kramek
Traducido por: Ana María Bisbé York


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

En mi artículo anterior hemos constatado que un 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 la manipule.

En el ejemplo previo mostramos como utilizar una estrategia sin que importe el problema de aplicar una localización específica de tarifas de impuesto 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 responsabilidad 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. Además, cada objeto en la cadena, sólo necesita conocer también sobre un objeto, el siguiente en la cadena. La cadena de responsabilidad puede ser implementada utilizando una cadena predefinida o estática, o las cadenas pueden ser dinámicas, construidas 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. Sin embargo, no existen requerimientos absolutos para todos los miembros de la cadena de responsabilidad, por 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).

Nuevamente, vemos el patrón de puente básico. Esto es, porque cada enlace en la cadena es en realidad un puente entre una abstracción y una implementación. La diferencia con un puente sencillo 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 objeto controlador concreto, como la implementación. Sin embargo, el segundo enlace tiene ahora el primer objeto controlador desempeñando el rol de abstracción, que es a su vez, de implementación. En su turno, el segundo enlace se convierte en abstracción para la implementación del tercer enlace. Este patrón puede, en teoría al menos, repetirse hasta el infinito.

Con el objeto de implementar un patrón de cadena de responsabilidades, necesitamos definir primero una clase controladora abstracta y tantas subclases específicas como necesite. La diferencia clave entre la Cadena de responsabilidad y el patrón de estrategia es que la clase abstracta debe definir el mecanismo, por el cual un objeto puede determinar si puede controlar el requerimiento. Adicionalmente necesita una propiedad para guardar una referencia al objeto siguiente en la cadena. Como he mostrado antes, 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 responsabilidad?

La pregunta que puse en el contexto del patrón Estrategia fue: "¿Cómo solucionar el problema de calcular impuestos cuando el cálculo depende de la localidad donde se realiza la venta?" Usted recordará que la solución fue definir subclases específicas para calcular cada tipo de impuesto y luego determinar qué subclase es la requerida basándose en una tabla que enlazaba la localidad con el tipo de impuesto aplicable.

Para controlar este mismo problema con una cadena de responsabilidad podemos utilizar la clase para calcular el impuesto original que definimos para el patrón Estrategia y agregarle nuevas propiedades:

  • cCanHandle Define el contexto controlado por esta subclase.
  • cNextObj Nombre de la clase para instanciar como el próximo objeto en la cadena.
  • cNextObjLib Biblioteca de clases para el próximo objeto de la cadena
  • oNext Referencia del objeto para el próximo objeto de la cadena

La propiedad "cCanHandle" define el "contexto" que aceptará la instancia específica. Mientras las "futuras" propiedades se utilizan para definir qué objeto debe seguir a cual. Esto podría ser con un prellenado para crear una cadena predefinida o podría incluso determinar los valores relevantes en tiempo de ejecución para crear una cadena extensible infinitamente que cambie de acuerdo a las necesidades.

Por ejemplo, supongamos que el contexto del primer objeto fue definido como un tipo de impuesto del 7.00% entonces, podría determinar que si ha pasado un contexto que es mayor que sí mismo, entonces no se trata instanciar un objeto con un valor de contexto que sea menor que si mismo. ¿Cómo se implementa esto? Una forma podría ser tener una tabla que liste para cada valor de contexto la clase relevante (y su biblioteca). Entonces, cada objeto en la cadena podría simplemente buscar la información cuando necesite.

Además de las propiedades descritas antes, necesitamos al menos dos métodos:

  • ProcessRequest: Método expuesto que se utiliza para llamar al objeto y que determina si una petición específica es procesada por el objeto.
  • CalcTax: Método real que realiza el cálculo y devuelve el resultado.

El objeto cliente debe crear, u obtener una referencia al primer objeto en la cadena. Entonces, llamará al método ProcessRequest() de ese objeto y pasará el contexto y el precio de venta para el que se ha requerido un impuesto. Este código asume que la cadena de Responsabilidad va a devolver o un Impuesto o un NULL:

WITH ThisForm
  *** Verificar que tenemos disponible el primer objeto de la cadena
  IF VARTYPE( This.oCalc ) # "O"
    *** No lo tenemos, por tanto, lo creamos
    .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

El código en el método ProcessRequest(), está definido en el nivel de la clase abstracta y es completamente genérico. Es el responsable de determinar si la petición entrante es manipulada localmente. Si es así, llama simplemente el método predeterminado CalcTax() (también definido en la clase abstracta la que usa el precio pasado y la tarifa de impuesto integrada). 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 lo tenemos, pero ya existía antes 
      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 es necesario y la subclase individual para este ejemplo sencillo no necesita personalizar código alguno, todo está controlado por propiedades de configuración (incluyendo la propiedad nTaxRate definida en la clase raíz original.)

¿Cuándo debo utilizar una Cadena en lugar de una Estrategia?

En este momento se puede estar preguntando ¿por qué preocuparse? - especialmente, una vez que hemos visto realmente una buena solución controlada por datos a un problema en el patrón de Estrategia. Sin embargo, la limitación del patrón de estrategia es que solamente UNA subclase puede existir, y por tanto no es apropiado cuando necesitamos múltiples operaciones. El ejemplo empleado aquí ¡es para una cadena corta! Tan pronto como un objeto controla la tarea el resultado es devuelto y ningún otro objeto es necesario.

Sin embargo, la Cadena de Responsabilidad realmente logra su máximo rendimiento cuando pueden ser requeridas múltiples operaciones. Una extensión de nuestro sencillo problema de Impuestos podría ser controlar problemas como "Gastos de Envío y Manipulación", "Descuentos" o incluso impuestos múltiples (por ejemplo sobrecargos locales).

En esta situación, una Estrategia no es suficiente; pero la Cadena de responsabilidad controla esto fácilmente. Todo lo que necesita es, en lugar de detenerse ante el primer objeto que puede controlar la tarea, pasamos la petición a cada objeto en la cadena inequívocamente, y permitimos a todos la oportunidad de contribuir a la solución final. Por supuesto, podemos necesitar más de un único valor devuelto en algún escenario; pero los objetos parámetros también son una forma sencilla y conveniente para controlar esto.

Entonces, tendríamos objetos en nuestra cadena basados en clases "Calculadora de Impuestos de venta", "Calculadora de Gastos de envío" y "Calculadora de descuentos". Todo lo que ellos necesitan son las PEMs necesarias para participar en la cadena.

Resumen de patrón de cadena de responsabilidad.

La cadena de responsabilidad nos proporciona otra vía para solucionar el problema de proporcionar la funcionalidad sin la necesidad de programarlo explícitamente. Quizás, 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.