martes, 1 de abril de 2014

Patrones estructurales: Decorator

Este post corresponde a la serie Patrones de diseño - Patrones estructurales

Decorator

"Agrega responsabilidades adicionales a un objeto de forma dinámica. Los decoradores proveen una alternativa más flexible que la herencia para extender la funcionalidad de un objeto".

Para entender cómo y cuándo aplicar este patrón vamos a empezar por ver un ejemplo de aplicación a desarrollar para entender el problema al que nos enfrentamos.


Desde el fabricante de coches ACME nos solicitan desarrollar una aplicación para que los clientes que visitan su web puedan calcular el precio de un coche en base a un modelo seleccionado y una serie de extras que el cliente puede elegir para el coche. Esto no es más que la típica calculadora de precios que podemos ver en la mayoría de webs de fabricantes de coches.

Para ver un ejemplo simple vamos a ver unas especificaciones del cliente muy simples.

  • Existen dos modelos de coches para elegir: el Sport y el Family
  • Cada modelo puede ser motor Diesel o Gasolina
  • El cliente también puede agregar las siguientes características si lo desea: Cambio automático, Turbo, Frenos ABS, Elevalunas eléctricos, Navegador y Techo solar.

El problema


Una primera aproximación a la solución del problema podría estar basada en calcular el precio en una clase específica fuera de la jerarquía de la clase coche. Podríamos crear la clase CarPriceCalculator que debería tener las propiedades para las opciones que el cliente puede elegir. Así en CarPriceCalculator tendríamos la propiedad Turbo por ejemplo que estableceríamos a true si el cliente selecciona la opción turbo. Algo como lo siguiente:

public class CarPriceCalculator
{
    private Car car;

    public CarPriceCalculator(Car carModel)
    {
        this.car = carModel;
    }

    public bool Automatic { get; set; }
    public bool Turbo { get; set; }
    public bool AbsBrakes { get; set; }
    public bool PowerWindows { get; set; }
    public bool Diesel { get; set; }
    public bool Navigator { get; set; }
    public bool Sunroof { get; set; }

    public double GetPrice()
    {
        var price = car.Price;
        if (Automatic)
            price += 2000D;
        if (Turbo)
            price += 1300D;

        //...
        //...
        // Aquí serguirá una enorme estructura de
        // sentencias if

        return price;
    }
}

Esta solución presenta algunos problemas, pero el más evidente es que viola el principio Open / Close. Si agregamos una nueva característica tendríamos que modificar la clase CarPriceCalculator.

Podríamos pensar en una solución basada en la herencia. Cada tipo de coche Sport y Family heredaría de la clase Car y establece su precio. Esto será un problema por la rigidez que conlleva el uso de herencia, ya que también cada coche podría ser con o sin turbo y tendríamos clases del tipo Sport, SportTurbo, Family, FamilyTurbo, si continuamos agregando características a la jerarquía de clases podríamos acabar con algo como esto:


Y esto para tan tres características (Diesel, Turbo y Frenos ABS). ¿Habéis visto cuantos parámetros tienen las calculadoras de precios de coches? ¡Tienen una cantidad inmensa de opciones! 
Lo que está claro es que por este camino no vamos a acabar bien. La cantidad de clases que deberíamos implementar va a crecer exponencialmente según agregamos opciones al sistema. 

El error fundamental de este planteamiento para calcular el precio del coche es que se basa en la herencia. Mediante herencia estamos agregando responsabilidades (cambio en el precio) a las clases, pero estas responsabilidades se agregan de forma estática, en la definición del modelo. Lo ideal sería que pudiésemos agregar responsabilidades a las clases Sport o Family (que definen los modelos de coches) de forma dinámica, según el cliente selecciona características extra del coche.

La solución


El patrón Decorator como hemos visto en la definición nos permite agregar de forma dinámica responsabilidades a un objeto, así que de entrada parece una base factible para solucionar nuestro problema.

Vamos a ver el diagrama y las clases que intervienen en el patrón Decorator.


  • Component (Car). Define la interfaz de los objetos a los que podemos agregar responsabilidades dinámicamente.
  • ConcreteComponent (Sport, Family). Define objetos concretos a los que se les agregarán responsabilidades.
  • Decorator. Mantiene una referencia a Component. Y define una interfaz que se ajusta a la de Component.
  • ConcreteDecorator (Turbo, Frenos Abs, Diesel, etc.). Agregan responsabilidades la clase Component.

Ahora veremos una solución para nuestra calculadora de precios basada en el patrón Decorator y después iremos comentando algunas cosas sobre la implementación.



/// 
/// Define la interfaz de los modelos de los coches.
/// Si es necesario se puede implementar como una
/// clase abstracta
/// 
public interface ICar
{
    double GetPrice();
}

/// 
/// Define un coche del modelo Sport
/// 
public class Sport : ICar
{
    public double GetPrice()
    {
        // Devuelve el precio de un coche
        // del modelo sport básico
        return 19500D;
    }
}

/// 
/// Define un coche del 
/// modelo Family
/// 
public class Family : ICar
{
    public double GetPrice()
    {
        // Devuelve el precio de un coche
        // del modelo family básico
        return 14800D;
    }
}

/// 
/// Clase base para los decoradores.
/// Mantiene una referencia a ICar (Component)
/// y define una interfaz igual a Componenet
/// 
public abstract class Decorator : ICar
{
    protected ICar carModel;
    protected Decorator(ICar carModel)
    {
        this.carModel = carModel;
    }

    public abstract double GetPrice();
}

/// 
/// Define la responsabilida 
/// para coches con frenos Abs.
/// La responsabilidad
/// en este caso es alterar el precio de la 
/// referencia ICar que mantiene su clase base.
/// 
public class AbsBrakes : Decorator
{
    public AbsBrakes(ICar car)
        : base(car)
    {

    }

    public override double GetPrice()
    {
        // Obtiene el precio de la referencia
        // del coche de la clase base y suma el
        // precio extra de los frenos Abs.
        return this.carModel.GetPrice() + 800D;
    }
}

/// 
/// Define la responsabilida 
/// para coches con cambio automático.
/// La responsabilidad
/// en este caso es alterar el precio de la 
/// referencia ICar que mantiene su clase base.
/// 
public class Automatic : Decorator
{
    public Automatic(ICar car)
        : base(car)
    {

    }

    public override double GetPrice()
    {
        // Obtiene el precio de la referencia
        // del coche de la clase base y suma el
        // precio extra del cambio automático.
        return this.carModel.GetPrice() + 2100D;
    }
}

/// 
/// Define la responsabilida 
/// para coches diesel. La responsabilidad
/// en este caso es alterar el precio de la 
/// referencia ICar que mantiene su clase base.
/// 
public class Diesel : Decorator
{
    public Diesel(ICar car)
        : base(car)
    {

    }

    public override double GetPrice()
    {
        // Obtiene el precio de la referencia
        // del coche de la clase base y suma el
        // precio extra del motor diesel.
        return this.carModel.GetPrice() + 1800D;
    }
}

/// 
/// Define la responsabilida 
/// para coches con Turbo.
/// La responsabilidad
/// en este caso es alterar el precio de la 
/// referencia ICar que mantiene su clase base.
/// 
public class Turbo : Decorator
{
    public Turbo(ICar car)
        : base(car)
    {

    }

    public override double GetPrice()
    {
        // Obtiene el precio de la referencia
        // del coche de la clase base y suma el
        // precio extra del turbo.
        return this.carModel.GetPrice() + 1100D;
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Precio de un modelo sport básico
        ICar car = new Sport();
        Console.WriteLine(string.Format("Precio del modelo sport básico: {0}", GetPriceOfAnyCar(car)));

        // Precio de un modelo family decorado con Diesel y Turbo
        ICar family = new Family();
        ICar diesel = new Diesel(family);
        ICar turbo = new Turbo(diesel);

        Console.WriteLine(string.Format("Precio del modelo family, diesel, turbo: {0}", GetPriceOfAnyCar(turbo)));

        // Precio de un modelo sport decorado con Turbo y Abs
        ICar sport = new Sport();
        ICar spTurbo = new Turbo(sport);
        ICar spAbs = new AbsBrakes(spTurbo);

        Console.WriteLine(string.Format("Precio del modelo sport, turbo con Abs: {0}", GetPriceOfAnyCar(spAbs)));

        Console.ReadLine();
    }

    static double GetPriceOfAnyCar(ICar car)
    {
        return car.GetPrice();
    }
}

Veamos algunas cosas interesantes a tener en cuenta de este código:

  • Los decoradores son del mismo tipo (ICar) que los objetos a los que decoran. Esto es fundamental porque nos permite usar del mismo modo un coche básico y uno decorado. He agregado el método GetPriceOfAnyCar que muestra esto. 
  • Los decoradores normalmente reenvían las llamadas del cliente a la referencia que mantienen del componente (ICar) y pueden hacer operaciones adicionales antes o después de reenviar la llamada. En nuestro caso después de reenviar la llamada al método GetPrice alteran el precio.
  • El patrón Decorator modifican los objetos Component (ICar) desde fuera (veremos otros patrones que modifican los objetos desde dentro). Realmente crean un envoltorio (wrapper) del objeto original. Lo interesante es que el envoltorio (Turbo, Diesel, etc) es del mismo tipo que el objeto original, permitiéndonos aplicar un número ilimitado de envoltorios. 
Una descripción del patrón Decorator que creo que es muy gráfica es que conceptualmente podemos verlo como que en tiempo de ejecución se parece a las capas de una cebolla envolviendo al objeto Component (ICar). El siguiente gráfico muestra esto para el ejemplo del modelo Sport con Turbo y Abs:


Ventaja e inconvenientes del uso del patrón Decorator


La aplicación del patrón Decorator nos ofrece las siguientes ventajas:
  • Promueve la composición de objetos y la delegación frente a la herencia. Como ya hemos mencionado en post anteriores esto se traduce en diseños más flexibles.
  • Mayor flexibilidad que la herencia estática. Con decoradores podemos agregar responsabilidades a los objetos en tiempo de ejecución. Por el contrario, la herencia requiere crear nuevas clases para agregar una responsabilidad. Además como hemos visto si creamos varias responsabilidades para un mismo objeto podemos aplicar varias de esas responsabilidades a la vez.
  • Evita "responsabilidades pre-cargadas" en las clases de la parte alta de la jerarquía. En nuestro ejemplo evita tener por ejemplo en la interfaz ICar propiedades del tipo HasTurbo, IsDiesel, etc. para poder calcular el precio posteriormente. Por el contrario, los decoradores permiten agregar esas responsabilidades en tiempo de ejecución justo cuando las necesitamos.
Además de ventajas, el uso de decoradores tiene ciertos riesgos o limitaciones que también deberemos tener en cuenta:
  • Los decoradores (Turbo, Diesel, etc) y los componentes (Sport, Family) no son idénticos. Como ya hemos visto los decoradores crean un wrapper transparente que nos permiten usarlos como si fueran componentes, pero realmente no son del mismo. Esto es muy importante tenerlo en cuenta por que no podremos crear operaciones basadas en la identidad de los objetos si usamos decoradores. En el ejemplo del código Sport, Turbo y Abs hemos creado la variable spAbs de tipo AbsBreaks. Pero aunque la usamos como si fuera un modelo Sport la operación spAbs as Sport devolverá null porque AbsBread y Sport no son idénticos.
  • Muchos objetos pequeños. Los diseños basados en decoradores suelen acabar en muchos objetos pequeños todos del mismo tipo y con muy pocas diferencias entre ellos. Aunque teniendo un poco de experiencia es muy fácil configurarlos, para desarrolladores más inexpertos puede ser complejo entenderlo y depurarlo.

Consideraciones a tener en cuenta al implementar el patrón Decorator.


Hay unas cuantas cosas que deberíamos tener en cuenta a la hora de implementar una solución basada en el patrón Decorator. 

No siempre es necesario implementar la clase Decorator. Como hemos visto esta clase se encarga de mantener una referencia a la clase Component. Sobre todo en los casos en que vamos a implementar el patrón en una jerarquía heredada (no creada nueva) es posible que sea más fácil de implementar si damos la responsabilidad de mantener la referencia al Component en los decoradores concretos que crear una clase Decorator.

Mantener la clase Component lo más ligera posible. Teniendo en cuenta que tanto los ConcreteComponents (Sport, Family) como los decoradores heredan de la misma clase común (Component) debemos tratar de tener esa clase común lo más ligera posible. Lo ideal es que su responsabilidad se exclusivamente definir la interfaz y no almacenar datos. Si la clase Component es muy pesada hará que los decoradores (que heredan de ella) sean también muy pesados y eso puede limitar el número de decoradores que podríamos usar.

Cambiar "la piel" o cambiar "las tripas". Relacionado con el punto anterior. Hemos visto que los decoradores cambian "la piel" de un objeto para alterar su comportamiento. Es decir, cambian el comportamiento de un objeto desde fuera. Por otro lado acabamos de ver que una clase Component muy pesada creará decoradores muy pesados y puede limitar su uso. 
En los casos en los que la clase Component sea intrínsecamente pesada y no podamos cambiar eso una mejor opción sería aplicar una solución basada en el patrón State (veremos este patrón en un futuro post). 
El patrón State cambia el comportamiento de un objeto desde dentro y no se verá afectado por que la clase Component sea muy pesada.
Por consiguiente, si la clase Component es ligera elegiremos la opción del patrón Decorator, por el contrario si no podemos "adelgazar" a la clase Component elegiremos el patrón State.

La elección entre Decorator y State puede venir marcada por el tamaño intrínseco de la clase Component (Car)

Usos conocidos del patrón Decorator

Son muchos los entornos gráficos que hacen uso del patrón Decorator para añadir adornos a los objetos; como los tiradores del objeto seleccionado en editores gráficos.

Pero el uso de Decorator no se restringe a la interfaz gráfica. Como hemos visto en nuestro ejemplo nosotros lo hemos usado para añadir responsabilidades de cálculo de precio de forma dinámica. 

En .NET una implementación típica de Decorator son las clases
  • System.IO.Stream
    • System.IO.BufferedStream
    • System.IO.FileStream
    • System.IO.MemoryStream
    • System.Net.Sockets.NetworkStream
    • System.Security.Cryptography.CryptoStream
Todas ellas son de tipo Stream y pueden recibir un Stream en su constructor.


Hasta aquí el patrón Decorator. El próximo post será sobre Facade.


2 comentarios: