¿Cómo diseñar diversos algoritmos o políticas de negocio que están relacionadas? ¿Cómo diseñar que estos algoritmos o políticas puedan cambiar? ¿Cómo implementar la solución en el lenguaje Ruby? El siguiente problema de diseño que se va a resolver consiste en proporcionar una lógica de fijación de precios en una tienda de puntos de venta acorde con diferentes lógicas de negocio, para posteriormente implementar la solución en Ruby.
El problema de negocio
La estrategia de fijación de precios de una venta puede variar. Durante un período podría ser el 10% de descuento en todas las ventas, después podría ser un descuento de 10 € si el total de la venta es superior a 200 €, y muchas otras variaciones no contempladas al momento que pueda fijar la directiva estratégica de la tienda.[1]
Solución con el patrón Strategy
La solución propuesta por el patrón estrategia para este contexto, expresa definir cada algoritmo/política/estrategia en una clase independiente con una interfaz común[2]. Puesto que el comportamiento de la fijación de precios varía según la política, se deben crear múltiples clases EstrategiaFijarPreciosVenta, cada una con un método polimórfico getTotal. A cada método getTotal se le pasa como parámetro el objeto Venta(objeto responsable de registrar los datos de una venta del mundo real) para aplicar después la regla de descuento. El siguiente diagrama ilustra la idea(se puede hacer clic en la imagen para apreciarla):
Implementación en Ruby
Hasta los momentos la solución parece relativamente sencilla. Sobre todo para quienes se encuentren familiarizados con el lenguaje Java: se pudiese implementar en unos minutos. El siguiente reto es entonces, implementar esta solución de negocio en el lenguaje de programación Ruby. La diferencia más importante, aparte de la sintaxis, es la brecha de filosofía de los lenguajes. En Ruby no existen Interfaces de tipo contrato de manera explícita como lo hacen Java y C# por ejemplo. Lo cual obliga a elegir entre las siguientes opciones:
- Usar herencia para obligar a la sobre-escritura del método getTotal.
- Usar módulos de Ruby para emular las interfaces de contrato.
Ambas opciones son discutidas y debatidas fuertemente en la actualidad por varios desarrolladores. La opción número uno, no permitiría que las clases hijos hereden de cualquier otra clase, pues Ruby no implementa la herencia múltiple. Por otra parte, la segunda opción pese a que mejora el planteamiento anterior, muchos desarrolladores de Ruby la consideran una mala práctica, pues se requiere de un uso excesivo del manejo de excepciones que pudiera tornarse contraproducente. Para efectos de este artículo, se utilizará la alternativa número uno, teniendo en cuenta, que queda cerrada toda posibilidad de extender las clases hijos de cualquier otro contexto distinto a este. Dado el entorno del negocio y el corto alcance del problema, parece un riesgo que vale la pena correr. De manera que la implementación resultaría:
Clase Venta
class Venta attr_reader :fecha, :monto def initialize(monto) @fecha = Time.now @monto = monto end def get_total_antes_descuento @monto end end
Clase de Estrategia de Fijación de Precios General
class EstrategiaFijarPreciosVenta def get_total(venta) raise 'Abstract Method' end end
Clase para la estrategia de descuento porcentual
require 'dominio/estrategia_fijar_precios_venta' require 'dominio/venta' class EstrategiaPorcentajeDescuento < EstrategiaFijarPreciosVenta attr_reader :porcentaje def initialize @porcentaje = 0.1 end def get_total(venta) venta.get_total_antes_descuento - (venta.get_total_antes_descuento * @porcentaje) end end
Clase para la estrategia de descuento sobre Umbral
require 'dominio/estrategia_fijar_precios_venta' require 'dominio/venta' class EstrategiaDescuentoUmbral < EstrategiaFijarPreciosVenta attr_reader :descuento, :umbral def initialize @descuento = 50.0 @umbral = 500.0 end def get_total(venta) tad = venta.get_total_antes_descuento if tad < @umbral return tad else return tad - @descuento end end end
Clase Procesar Venta
require 'dominio/venta' class ProcesarVenta attr_accessor :estrategiaVenta attr_accessor :venta def initialize (estrategia, venta) @estrategiaVenta = estrategia @venta = venta end def finalizar_venta total = @estrategiaVenta.get_total(venta) total end end
Contexto
$:.unshift File.join(File.dirname(__FILE__),'..','lib') require 'dominio/estrategia_fijar_precios_venta' require 'dominio/estrategia_descuento_umbral' require 'dominio/estrategia_porcentaje_descuento' require 'dominio/procesar_venta' require 'dominio/venta' venta = Venta.new(700) transaccion_venta = ProcesarVenta.new(EstrategiaPorcentajeDescuento.new ,venta) puts "*** Transaccion estrategia descuento porcentual ***" puts transaccion_venta.finalizar_venta transaccion_venta = ProcesarVenta.new(EstrategiaDescuentoUmbral.new, venta) puts "*** Transaccion estrategia descuento por Umbral ***" puts transaccion_venta.finalizar_venta
Una vez implementadas las clases, se puede comprobar el funcionamiento de la solución, a través de:
ruby contexto.rb
Tal como se puede apreciar, ésta es una solución bastante fiel a los objetivos del negocio, aunque tiene consigo bastantes oportunidades de mejora de implementación, una de ellas por ejemplo es acompañar el patrón Strategy con una Factoría(u otros patrones combinados), que colaboren en la creación, comunicación y mantenimiento de cara al futuro.
Referencias
[1] Caso de negocio tomado de:
Larman, C. (2003). UML y Patrones: Una Introducción al Análisis y Diseño Orientado a Objetos y al Proceso Unificado. Ciudad de Nueva York, Nueva York, EE.UU.: Pearson.
[2] Olsen, R. (2008). Design Patterns in Ruby.Ciudad de Nueva York, Nueva York, EE.UU.:Addison-Wesley