Patrones Estructurales (III): Patrón Decorator


Objetivo:

«Añadir responsabilidades a un objeto de forma dinámica. Los decoradores proporcionan una alternativa flexible a la herencia para extender funcionalidad.»

Design Patterns: Elements of Reusable Object-Oriented Software

El siguiente de los patrones estructurales que veremos sera el patrón Decorator o decorador. Su filosofía consiste en añadir responsabilidades de forma dinámica con el principal objetivo de evitar la conocida como «explosión de clases», es decir, la generación de un número elevado de subclases a partir de una superclase común.

Como podemos observar en el gráfico superior, la clase Decorator hereda de la misma clase que el componente que se quiere decorar. Así, cada decorador es capaz de encapsular una instancia de cualquier otro objeto que herede del componente común, bien un componente concreto u otro decorador. Este comportamiento recuerda al que vimos previamente en el patrón Adapter, con la diferencia de que la clase Decorator, a diferencia de la clase Adapter, no transforma una interfaz, sino que añade cierta funcionalidad.

La encapsulación puede ser iterativa, de modo que un componente concreto puede ser encapsulado por un decorador, que a su vez puede ser encapsulado por otro decorador… y así sucesivamente, añadiendo nueva funcionalidad en cada uno de los pasos. Resumiendo: el patrón Decoratorsustituye la herencia por un proceso iterativo de composición.

El objeto con el que el objeto cliente interactuará será aquel que se encuentre en la capa más externa (en este caso, DecoratorC), que se encargará de acceder a los objetos contenidos e invocar su funcionalidad, que será devuelta a las capas exteriores.

Para comenzar, por tanto, debemos tener claros los siguientes conceptos sobre este patrón:

  • Un decorador hereda de la misma clase que los objetos que tendrá que decorar.
  • Es posible utilizar más de un decorador para encapsular un mismo objeto.
  • El objeto decorador añade su propia funcionalidad, bien antes, bien después, de delegar el resto del trabajo en el objeto que está decorando.
  • Los objetos pueden decorarse en cualquier momento, por lo que es posible decorar objetos de forma dinámica en tiempo de ejecución.

La razón por la que la clase Decorator hereda de la misma clase que el objeto que tendrá que decorar no es la de añadir funcionalidad, sino la de asegurarse de que ambos comparten el mismo tipo y puedan intercambiarse: un decorador podrá sustituir a un objeto decorado, basándonos en el principio SOLID del Principio de sustitución de Liskov.

Declarando las clases funcionales

Como viene siendo habitual, ilustraremos nuestro patrón haciendo uso de vehículos. En este caso, utilizaremos una case abstracta, llamada Vehiculo, del que heredarán las clases funcionales a las que llamaremos «Berlina» y «Monovolumen», y los decoradores, que se limitarán a añadir funcionalidad a estas clases funcionales. Los decoradores que diseñaremos serán «Diesel», «Gasolina», «Inyeccion», «CommonRail» y «Turbo».

Estos decoradores se caracterizarán por:

  • Disponer de una referencia a un vehículo que será inyectada en el constructor.
  • Modificar el funcionamiento original de la clase que decoran, sobrecargando los métodos y llamando a los métodos de las clases encapsuladas para modificar su información o funcionamiento.

Comencemos codificando nuestra clase abstracta Vehiculo de la cual heredarán el resto de clases.


    public abstract class Vehiculo
    {
        // Atributo común a todos los objetos que heredarán de esta clase
        protected string descripcion = "Vehículo genérico";

        // Método no abstracto que devolverá el contenido de la descripción
        // Se declara como virtual para que pueda sustituirse en las clases derivadas
        public virtual string Descripcion()
        {
            return descripcion;
        }

        // Métodos abstractos
        public abstract int VelocidadMaxima();
        public abstract double Consumo();
    } 

Hecho esto, añadiremos nuestras clases funcionales: Monovolumen y Berlina.

Monovolumen


    public class Monovolumen : Vehiculo
    {
        public Monovolumen()
        {
            descripcion = "Monovolumen";
        }

        // Funcionalidad básica
        public override int VelocidadMaxima()
        {
            return 160;
        }

        // Funcionalidad básica
        public override double Consumo()
        {
            return 7.5;
        }
    }

Berlina


    public class Berlina : Vehiculo
    {
        public Berlina()
        {
            descripcion = "Berlina";
        }

        public override int VelocidadMaxima()
        {
            return 180;
        }

        public override double Consumo()
        {
            return 6.2;
        }
    }

Como vemos, ambas clases heredan un atributo común, «descripcion», y la funcionalidad VelocidadMaxima y Consumo. Asumiremos que un monovolumen posee una velocidad máxima de 160km/h y un consumo de 7,5 litros/100km, mientras que una berlina podrá alcanzar los 180km/h con un consumo de 6,2 litros/100km. Esta funcionalidad será modificada por nuestras clases decoradoras, que podrán aumentar o disminuir estas características.

Con esto habríamos codificado nuestra rama «funcional». Aún no hay rastro del patrón Decorator, ya que únicamente hemos hecho uso de la herencia de la manera habitual (de hecho, de una forma un tanto escueta).

Creando las clases decoradoras

Es hora, por tanto, de añadir una nueva «rama» a nuestro árbol, añadiendo los decoradores. Comenzaremos por crear una nueva clase abstracta que herede de Vehiculo (para que pueda ocupar su lugar) y de la cual heredarán todos los decoradores.

Decorator


    public abstract class Decorator : Vehiculo
    {
        // Declaramos el método como abstracto para que todos los decoradores lo
        // reimplementen.
        public override abstract string Descripcion();
    }

A continuación añadiremos los decoradores, que incluirán una referencia a un vehículo y que se construirán mediante la inyección de éste. Por tanto, las características de estos decoradores, además de heredar de Decorator, serán las siguientes:

  • Contienen una referencia a un Vehiculo, que se insertará en el constructor.
  • Modifican el funcionamiento de las clases que encapsulan, accediendo a sus atributos y métodos y adaptándolos a la nueva funcionalidad deseada.

Comenzaremos añadiendo un decorador llamado «Gasolina». La gasolina, al ser más explosiva, proporciona mayor velocidad punta, pero al ser menos energética que el gasoil, también conlleva tener un consumo más elevado. Esta clase tendrá, por tanto, el siguiente aspecto:

Gasolina


    public class Gasolina : Decorator
    {
        // Instancia de la clase vehiculo
        private Vehiculo vehiculo;

        // Constructor que recibe el vehículo que encapsulará el decorator
        public Gasolina(Vehiculo vehiculo)
        {
            this.vehiculo = vehiculo;
        }

        // Los métodos utilizan la información del objeto encapsulado y le
        // incorporan su propia funcionalidad.
        public override string Descripcion()
        {
            return vehiculo.Descripcion() + " Gasolina";
        }

        // Un vehículo gasolina proporciona más potencia, por lo que "decora" el
        // vehiculo añadiendo mayor velocidad máxima
        public override int VelocidadMaxima()
        {
            return vehiculo.VelocidadMaxima() + 60;
        }

        // La gasolina es menos energética que el diesel, por lo que el consumo
        // de combustible es mayor. Decoraremos el vehículo añadiéndole un consumo
        // de 1.2 litros adicionales a los 100 km.
        public override double Consumo()
        {
            return vehiculo.Consumo() + 1.2;
        }
    }

Podemos utilizar un razonamiento análogo para un motor diesel, que tendrá el funcionamiento opuesto.

Diesel


    public class Diesel : Decorator
    {
        // Instancia de la clase vehiculo
        private Vehiculo vehiculo;

        // Constructor que recibe el vehículo que encapsulará el decorator
        public Diesel(Vehiculo vehiculo)
        {
            this.vehiculo = vehiculo;
        }

        // Los métodos utilizan la información del objeto encapsulado y le
        // incorporan su propia funcionalidad.
        public override string Descripcion()
        {
            return vehiculo.Descripcion() + " Diesel";
        }

        public override int VelocidadMaxima()
        {
            return vehiculo.VelocidadMaxima() + 20;
        }

        public override double Consumo()
        {
            return vehiculo.Consumo() - 0.8;
        }
    }

¡Ojo! Según nuestro diseño, un vehículo podría ser a la vez diesel y gasolina, ya que ambos heredan de la clase Vehiculo. Las reglas de negocio no deberían permitir que esto fuera así, por lo que deberíamos utilizar reglas adicionales para evitar que ambos decoradores estuviesen presentes en un mismo objeto. No obstante, ignoraremos este hecho en este ejemplo.

A continuación añadiremos nuevos decoradores a nuestro vehículo. Por ejemplo, el turbo.

Turbo


    public class Turbo : Decorator
    {
        private Vehiculo vehiculo;

        public Turbo(Vehiculo vehiculo)
        {
            this.vehiculo = vehiculo;
        }

        public override string Descripcion()
        {
            return vehiculo.Descripcion() + " Turbo";
        }

        public override int VelocidadMaxima()
        {
            return vehiculo.VelocidadMaxima() + 30;
        }

        public override double Consumo()
        {
            return vehiculo.Consumo() + 0.4;
        }
    }

Otra posible modificación al vehículo original podría ser la inyección de combustible, que no afectará a la velocidad pero mejorará notablemente el consumo de combustible:

Inyeccion:


    public class Inyeccion : Decorator
    {
        private Vehiculo vehiculo;

        public Inyeccion(Vehiculo vehiculo)
        {
            this.vehiculo = vehiculo;
        }

        public override string Descripcion()
        {
            return vehiculo.Descripcion() + " Inyección";
        }

        public override int VelocidadMaxima()
        {
            return vehiculo.VelocidadMaxima();
        }

        public override double Consumo()
        {
            return vehiculo.Consumo() - 1.2;
        }
    }

Finalmente, incluiremos un decorador que implemente la tecnología CommonRail, que disminuirá la velocidad punta pero a cambio mejorará ligeramente el consumo.

CommonRail


    public class CommonRail : Decorator
    {
        private Vehiculo vehiculo;

        public CommonRail(Vehiculo vehiculo)
        {
            this.vehiculo = vehiculo;
        }

        public override string Descripcion()
        {
            return vehiculo.Descripcion() + " Common Rail";
        }

        public override int VelocidadMaxima()
        {
            return vehiculo.VelocidadMaxima() - 15;
        }

        public override double Consumo()
        {
            return vehiculo.Consumo() - 0.4;
        }
    }

Tras nuestra última adquisición, nuestra jerarquía de clases tendrá el siguiente aspecto:

Ahora es posible componer, en tiempo de ejecución, un objeto que combine algunas o todas estas características sin necesidad de codificar una clase por cada posible combinación.

Utilizando el patrón Decorator

Es hora de hacer uso del patrón que acabamos de explicar. Comenzaremos creando un vehículo monovolumen y otro vehículo de tipo berlina, mostrando por pantalla sus características:


    class Program
    {
        static void Main(string[] args)
        {
            Vehiculo monovolumen = new Monovolumen();
            Vehiculo berlina = new Berlina();

            MostrarCaracteristicas(monovolumen);
            MostrarCaracteristicas(berlina);
        }

        private static void MostrarCaracteristicas(Vehiculo v)
        {
            Console.WriteLine(string.Format("{0}\n\t- Velocidad punta de {1} km/h \n\tConsumo medio de {2} l/100km\n",
                                            v.Descripcion(),
                                            v.VelocidadMaxima(),
                                            v.Consumo()));
        }
    }

Nuestro programa nos mostrará el siguiente resultado:

Como vemos, se nos ofrece una versión «básica» de nuestros objetos, que aún no han sido decorados. Probemos a decorar nuestro monovolumen añadiéndole un motor gasolina:


            Vehiculo monovolumen = new Monovolumen();
            
            // Decoramos el monovolumen añadiéndole un motor gasolina a través
            // del decorador "Gasolina"
            monovolumen = new Gasolina(monovolumen);

            Vehiculo berlina = new Berlina();

            MostrarCaracteristicas(monovolumen);
            MostrarCaracteristicas(berlina);

Esto modificará el comportamiento de nuestro monovolumen, que presentará las siguientes características:

Se ha modificado, por tanto, su descripción, velocidad punta y consumo medio. Hagamos lo propio con el vehículo de tipo Berlina, al que convertiremos en un vehículo diesel turbo inyección common-rail:


            Vehiculo berlina = new Berlina();

            berlina = new Diesel(berlina);
            MostrarCaracteristicas(berlina);

            berlina = new Turbo(berlina);
            MostrarCaracteristicas(berlina);

            berlina = new Inyeccion(berlina);
            MostrarCaracteristicas(berlina);

            berlina = new CommonRail(berlina);
            MostrarCaracteristicas(berlina);

Como podemos observar, la propia instancia de Vehiculo es pasada como parámetro al constructor, que devuelve la misma instancia decorada con la nueva funcionalidad.

Así, con una única referencia, hemos conseguido modificar el comportamiento de la instancia en tiempo de ejecución sustituyendo la capacidad de especialización de la herencia por un proceso horizontal de composición.

¿Cuándo utilizar este patrón? Ejemplos reales

No debemos hacer caso omiso de que la referencia «berlina» ocupará ahora en memoria el quíntuple de lo que originalmente ocupaba, ya que se trata en realidad de una referencia a CommonRail que contiene un objeto Inyeccion que contiene un objeto Turbo que contiene un objeto Diesel que a su vez contiene la instancia Berlina original. Por tanto, conviene estudiar bien el contexto en el que se utilizará este patrón, ya que el ahorro que obtendremos en diseño lo pagaremos en memoria.

Un ejemplo claro de este patrón en la API de .NET es la familia de clases Stream. La clase Stream es una clase abstracta que expone la funcionalidad básica que será implementada por las clases concretas y decoradas por las clases que componen el patrón.

Algunas de las clases concretas que podemos encontrar en esta familia son las siguientes:

  • FileStream: representa un flujo (stream) que se encargará de realizar operaciones E/S sobre un fichero físico.
  • MemoryStream: representa un flujo que realizará operaciones E/S en memoria. Se usa como una proyección temporal en memoria de otro flujo.
  • BufferedStream: representa una sección de un flujo que realizará operaciones E/S en memoria. La diferencia con el anterior es que el MemoryStream representa un flujo completo (por ejemplo, una proyección en memoria de un FileStream), mientras que BufferedStream se usa en conjunción con otros Streams para realizar operaciones de E/S que posteriormente serán leídas o volcadas desde/hacia el flujo original.

Estas clases serían nuestra Berlina y Monovolumen. Pero ¿y los Decorator? Las clases que actúan como decoradores son fácilmente identificables porque reciben un Stream como parámetro a la hora de crear una nueva instancia, a la vez que extienden la funcionalidad del objeto de la clase original.

Algunos de los decoradores que podemos encontrar, y que son aplicables a cualquiera de las tres clases anteriores son:

  • CryptoStream: define una secuencia que vincula los flujos de datos a las transformaciones criptográficas.
  • AuthenticatedStream: proporciona métodos para pasar las credenciales a través de una secuencia y solicitar o realizar la autenticación para las aplicaciones de cliente-servidor.
  • GZipStream: proporciona los métodos y propiedades que permiten comprimir y descomprimir secuencias.

Cada uno de estos Decorator reciben en su constructor un Stream, añadiéndole nuevas funcionalidades y permitiendo que sigan actuando como Stream. Debido al polimorfismo, un CryptoStream que decore un MemoryStream seguirá pudiendo sustituir a cualquier objeto que se pase como parámetro como una referencia a Stream.

Podemos ver un ejemplo similar en Java, donde la clase InputStream actuaría como la clase base abstracta, clases como FileInputStream, StringBufferInputStream y ByteArrayInputStream actuarían como clases concretas, FilterInputStream actuaría como la clase abstracta de la que heredan todos los decoradores (clase que no existe en la familia Stream de .NET) y clases como BufferedInputStream, DataInputStream o LineNumberInputStream actuarían como decoradores, recibiendo un objeto de la clase InputStream como parámetro en su constructor.

Tras leer la aplicación de este patrón seguro que el uso de Streams se entiende un poco mejor, ¿a que sí?

Fuentes:

9 comentarios

  1. Hola Daniel,

    Muchas gracias por el post del nuevo patrón. Me ha encantado la idea de este patrón y la explicación que has desarrollado para su comprensión ha sido excepcional, como siempre haces.

    Ahora a machacar el concepto con unos cuantos ejemplos más. 🙂

    1. Muchas gracias, José Miguel. Con comentarios como este da gusto publicar 😀

      Me alegro de que estos artículos te sean útiles, intentaré añadir un nuevo patrón en cuanto disponga de un rato.

      ¡Saludos!

  2. Muy buenas tus explicaciones y la forma en que defines cada concepto, es una maravilla haber encontrado tus post.

    Muchas gracias por tu tiempo, y por liberar tus conocimientos.
    ¡Un saludo!

  3. Hola Daniel, muy buena explicación, quería hacerte una pregunta que patrón o patrones me recomiendas usar para resolver el tipico problema de una funcionalidad de login con perfiles y roles. Gracias

  4. Hola Daniel, solo quería felicitarte por las explicaciones que brindas en tu sitio. Son muy concisas y claras. Estoy complementando mis estudios con tus post y la verdad que son de gran ayuda. Muchas gracias!

  5. Hola Daniel! te quiero agradecer por todo el conocimiento que compartis en estas paginas, la verdad tus explicaciones son espectaculares!
    Saludos

    Cristian

Deja un comentario