25 de abril de 2017

Patrones de diseño - La estrategia

Artículo original: Design Patterns - The Strategy
http://weblogs.foxite.com/andykramek/archive/2006/12/18/3044.aspx
Autor: Andy Kramek
Traducido por: Ana María Bisbé York


¿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 – qué hacer con demandas inesperadas para cambios de implementación.

¿Cómo reconozco dónde necesito una estrategia?

La definición formal de estrategia, dada en "Design Patterns, Elements of Reusable Object-Oriented Software" por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides 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 donde el patrón de estrategia puede ayudar.

Considere el problema de calcular la tasa de impuestos debido por una compra. Podemos asumir que nuestra aplicación conoce los elementos adquiridos, y la cantidad y entonces (a partir de su información de precios) puede determinar el valor de la venta. Ahora tenemos que aplicar la tarifa de impuesto.

Esto no es un problema, si tenemos un entorno donde el impuesto de venta se aplica a una única tarifa independientemente de la localidad ( como con el VAT en el Reino Unido, por ejemplo). El único problema aquí es saber si el elemento tiene impuesto, y si lo tiene cuál es la tarifa actual. Sin embargo, en EEUU, ¡ los impuestos sobre las ventas son locales ! La tarifa aplicada depende incluso de dónde compramos algo (o incluso, dónde reside el vendedor.)

Por ejemplo, en nuestra localidad, la tasa de ventas en ropa 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 = "Ropa"
      lnTaxRate = 5.75
    ELSE
      *** Otros elementos aquí
    ENDIF
  CASE lclocale = "Canton"
    IF lcItemType = "Ropa"
      lnTaxRate = 5.25
    ELSE
      *** Otros elementos aquí
    ENDIF
  CASE lclocale = "Grove City"
    IF lcItemType = "Ropa"
      lnTaxRate = 0.00
    ELSE
      *** Otros elementos aquí
    ENDIF
  OTHERWISE
    *** Aplicar un valor predeterminado
    lnTaxRate = 5.50
ENDCASE

Podemos ver inmediatamente que surgirá un problema. Ocurrirá, cuando necesitemos agregar "Cleveland" a nuestra lista de localidades, o cuando Akron, desalentado por las pérdidas en ventas de Canton, recorte sus tasas para vestuario. Necesitaremos 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, podríamos guardar esta información en una tabla (una columna para localidad, y quizás, una columna para cada elemento) y revisarlo 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, a algo que nos diga qué tasa debemos aplicar. En otras palabras deseamos separar la abstracción (calcular el impuesto debido) de la implementación (basada en la localidad).

Sin embargo, sin cambiar el código, lo cual es re-localizar; pero no cambiar el problema, un puente no es la solución porque solo permite una única implementación y nosotros necesitamos múltiples implementaciones. Necesitamos ser capaces de decidir, en tiempo de ejecución, qué implementación necesita. El patrón de estrategia nos permite definir clases para cada situación que debemos tratar y luego instanciar la adecuada en tiempo de ejecución.

¿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. La estrategia es inicializada por una petición de acción del cliente.

Puede pensar, que una 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 de la información derivada del cliente (por ejemplo, la aplicación).

Este patrón generalmente basa la responsabilidad de instanciar lel objeto estrategia concreto en el cliente, que pasa una referencia al contexto. Para implementar un patrón de estrategia, usted 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?

El siguiente ejemplo ilustra cómo se puede utilizar un patrón de estrategia para implementar una solución a los impuestos de venta que hemos discutido en el preámbulo de este tópico.

Lo primero que necesitamos hacer es definir una clase abstracta que tenga una propiedad para el impuesto aplicable. Tiene además, métodos para calcular el impuesto utilizando el valor pasado a la propiedad. Luego creamos tantas subclases de esta definición como tarifas de impuestos trabajamos - una subclase para cada tarifa.

Ahora necesitamos considerar cómo decidimos cuál de varias subclases necesitamos en cada momento dado. Para determinar esto, necesitamos relacionar las diferentes localidades y las tarifas aplicables (la "localidad") a su subclase apropiada. Existen, por supuesto, muchas formas en que podemos hacer esto y la más sencilla es definir la lista de localidades (por ejemplo, ciudades) y la tarifa de impuesto que se aplica en cada una. Una vez que tenemos una subclase específica para cada tarifa de impuesto, podemos derivar la subclase necesaria para cada localidad.

Aunque aun necesitamos una tabla, un registro para cada ciudad, podemos reducir la duplicación ya que nombramos las clases por la tarifa que aplica. El resultado es algo como esto:

CiudadTarifaSub-clase
Akron5.75Tax575
Canton5.25Tax525
Grove City0.00Tax000
Kent5.75Tax575

En otras palabras, utilizamos la tarifa aplicable para identificar las subclases que necesitamos con la consecuencia de que no necesitamos la tercera columna de la tabla - podemos inferirlo directamente de la Tarifa.

Entonces, ¿cómo podemos programar esto en un formulario? Bien, es muy sencillo. Todo lo que necesitamos es una lista desplegable de las localidades basada en nuestra tabla Ciudad/Tarifa (City/Rate). Cuando se selecciona la ciudad podemos comprobar la imposición fiscal aplicable. Teniendo esto, podemos derivar el nombre de la subclase a instanciar, y debido a que todas las subclases derivan de un padre común sabemos que todo lo que necesita es llamar al método adecuado del objeto, pasando los parámetros esperados - en este caso, dos parámetros - la clave contextual (la que define la tarifa aplicable) y el "precio" al que se calcula el impuesto. La clave contextual es usada para generar el nombre de la subclase requerida. Si la clase instanciada actualmente no es la correcta, simplemente instanciamos la correcta, como sigue.

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

Aunque esta es una forma en que puede ser implementada una estrategia y tiene sentido en este escenario, 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:

  • Descuento al valor del elemento, 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 cuándo aplicar un tipo de descuento específico. En este escenario puede ser enteramente apropiado, y mucho más simple, que cada botón sea 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 tendrían que 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 esbozado 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, que van a compartir una interfaz común, para cada opción y solamente instanciar 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.

No hay comentarios. :

Publicar un comentario