Patrones de diseño: Decorator

Patrones de diseño: Decorator
Sin comentarios Facebook Twitter Flipboard E-mail

En el anterior artículo sobre patrones de diseño, hablábamos del patrón Adapter. Y con la intención de seguir aumentando nuestro catálogo de patrones, continuamos hoy con otro patrón sencillo. Sencillo de entender y sencillo de aplicar, pero igualmente útil. Se trata del patrón Decorator.

Así que como siempre, vamos a describir el patrón y a través de un ejemplo práctico, demostrar como se implementa.

Descripción del patrón Decorator

La utilidad principal del patrón Decorator, es la de dotar de funcionalidades dinámicamente a objetos mediante composición. Es decir, vamos a decorar los objetos para darles más funcionalidad de la que tienen en un principio.

Esto es algo verdaderamente útil cuándo queremos evitar jerarquías de clases complejas. La herencia es una herramienta poderosa, pero puede hacer que nuestro diseño sea mucho menos extensible.

Describiendo el problema

Nuestra compañía se dedica a fabricar ordenadores para venderlos. Nosotros somos los desarrolladores encargados de implementar la lógica del negocio en nuestro lenguaje favorito (en este caso C#). Los ordenadores, de cara al cliente, tienen una configuración determinada. Por ejemplo, existe el Dummy User PC, que tiene un procesador normalito, una memoria RAM limitada y un disco duro corriente. También existe el VeryHard Gammer PC que es mucho más potente. Procesador de última generación, dos tarjetas gráficas con un gritón de RAM, disco duro SSD. Vamos lo último y más caro.

Nuestra aplicación debe calcular el precio final del ordenador basándose en sus componentes y un margen de beneficio en euros que tendrá cada configuración. Los componentes como vemos, no siempre serán los mismos, ya que dependen de la configuración. Además la configuración puede incluir también periféricos como monitores o impresoras.

Para solucionar el problema, y como somos buenos programadores, utilizamos la programación orientada a objetos. Primero creamos una clase abstracta:


    public abstract class Computer
    {        
        public abstract decimal CalculateCost();
    }

Esta clase la utilizamos para que las clases que la hereden, se vean obligadas a implementar su propio método CalculateCost. Usando esa clase, tendríamos la siguiente implementación del Dummy PC. La implementación del VeryHard Gammer PC sería muy parecida.


    public class DummyComputer:Computer
    {
        private decimal processor = 56.00M;
        private decimal hdd = 30.00M;
        private decimal graphics = 41.99M;
        private decimal ram = 23.50M;       

        public override decimal CalculateCost()
        {
            var cost = this.processor + this.hdd + this.graphics + this.ram;

            return cost;
        }      
    }

Como veis la implementación es muy sencilla. Lo lógico sería buscar en base de datos, o realizar cálculos más complejos, pero como no es el objetivo del artículo lo hemos simplificado mucho.

Resulta que debido a su servicio de atención al cliente y a sus excelentes precios, la empresa empieza a crecer. Para aumentar el volumen de negocio se deciden permitir que los clientes puedan elegir su propia configuración. Desde la tienda online de la empresa, los usuarios podrán añadir su procesador favorito, un disco duro muy rápido o cantidades enormes de RAM. Y el precio debe calcularse para que el cliente pueda comprobarlo antes de realizar la compra.

Solución con el patrón Decorator

Al final, decidimos implementar una solución utilizando el patrón Decorator. En este caso, nuestra clase Computer no sufrirá cambios. Es la que queremos extender así que no la vamos a tocar. Pero si vamos a crear una nueva clase, que hereda de ella, a la que llamaremos ComponentDecorator.


    public abstract class ComponentDecorator:Computer
    {
        public override abstract decimal CalculateCost();
    }

Lo importante de esta clase, es que hereda de la clase original por lo que los objetos se podrán convertir a Computer con un simple cast.

Vale, ¿y cómo añadimos funcionalidad? En este caso hemos optado por crear una clase por cada componente hardware. A su vez, cada una de estas clases hereda de ComponentDecorator. Por ejemplo para un disco duro SSD, tendremos el siguiente código:


    public class FastSSD:ComponentDecorator
    {
        private Computer currentComputer;
                
        public FastSSD(Computer computer)
        {          
            this.currentComputer = computer;
        }

        public override decimal CalculateCost()
        {
            return this.currentComputer.CalculateCost() + 255.20M;
        }
    }

Mientras que para el procesador tope de gama, tendremos el siguiente código:


    public class BigProcessor:ComponentDecorator
    {
        private Computer currentComputer;      

        public BigProcessor(Computer computer)
        {            
            this.currentComputer = computer;
        }

        public override decimal CalculateCost()
        {
            return this.currentComputer.CalculateCost() + 120.00M;
        }
    }

Se puede ver que lo que estamos cogiendo un objeto Computer y lo estamos extendiendo para que el método CalculateCost incremente en 120.00 lo que devuelve el método original. Para poder usar el método original, pasamos el objeto Computer como parámetro del constructor.

Para que todo funcione necesitamos crear una clase base desde la que partir, que sería la siguiente:


    public class BaseComputer:Computer
    {
        public override decimal CalculateCost()
        {
            return 0M;
        }
    }

En este caso el equipo base tiene un coste de 0, pero podría tener cualquier otro valor. También podemos crear otras configuraciones base, por ejemplo, para portátiles, barebones, netbooks, etc. Las configuraciones base las iremos decorando con los distintos componentes, para calcular el precio total.

Y para acabar veamos como usaríamos el patrón, en este caso creando un Gammer PC


    Computer gammerPC = new BaseComputer();
    gammerPC = new LotOfRAM(gammerPC);
    gammerPC = new FastSSD(gammerPC);
    gammerPC = new BigProcessor(gammerPC);

    var cost = gammerPC.CalculateCost();

    Console.WriteLine(string.Format("El Coste del Gammer PC es de {0} euros", cost));

Que escribirá en panatalla el siguiente mensaje:

El Coste de la configuración Gammer Pc es de 464,75 euros

Ya veis que es muy sencillo. Nos aprovechamos de que nuestros objetos ComponentDecorator, es decir LotOfRAM, FastSSD o BigProcessor, devuelven objetos Computer. Estos objetos los vamos pasando en el constructor de cada componente. Cuando hemos acabado de decorar la clase, hacemos una llamada al método para calcular el coste. Esta llamada irá llamando a los métodos CalculateCost de todos los objetos creados para al final calcular el precio final.

Aunque podríamos haber conseguido algo similar con una jerarquía de clases, probablemente no tendríamos la flexibilidad y dinamismo que nos proporciona esta solución. Y todo de una manera muy sencilla.

El patrón Decorator en la vida real

El ejemplo que hemos puesto es sencillo, y además muy típico. Si buscáis por internet o hojeáis un libro sobre patrones, encontraréis ejemplos similares con Pizza, café o cosas parecidas. Pero este patrón se utiliza también en la vida real.

**Por ejemplo .NET lo utiliza para decorar los Streams **. En este caso Stream es la clase abstracta, a la que se le añade funcionalidad con este patrón. Si necesitamos manejar un Stream en memoria, usamos un MemoryStream. Si necesitamos tratar con un fichero de texto, usamos FileStream. Y si queremos codificar datos usamos un CryptoStream. Pero además podemos usar varios de estos objetos para decorar lo que al final será un Stream. Por ejemplo, podemos con un código similar al siguiente:


    FileStream fs = File.Open("archivo.txt", FileMode.Open);
    BufferedStream bs = new BufferedStream(fs);
    CryptoStream cs = new CryptoStream(bs, new CryptoAPITransform(), CryptoStreamMode.Read);
    this.PrintBytes(cs);

Resumen para nuestro catálogo de patrones

  • Nombre del patrón: Decorator.
  • Tipo: patrón estructural.
  • Lo usaremos cuándo: necesitamos añadir funcionalidades a una clase de forma dinámica, evitando las jerarquías de clases que se tienen construir en tiempo de compilación.
  • Ventajas: podemos añadir responsabilidades a un objeto de forma progresiva y dinámica. Más flexibilidad que con la herencia.
  • Desventajas: la principal es que el objeto Decorator no es exactamente igual que la clase que está decorando, por lo que tenemos que tener cuidado. Además nos podemos encontrar con un diseño de clases muy pequeñas, pero en gran cantidad.
  • Patrones similares o relacionados: Adapter o Facade.

En Genbeta Dev | Patrones de diseño: qué son y por qué debes usarlos, Los patrones de diseño de software

Imagen | Bon Adrien

Comentarios cerrados
Inicio