Patrón Decorador

El patrón decorador es similar al Patrón Estrategia, ya que, vamos a preferir composición sobre herencia, cuando estamos diseñando nuestra aplicación. Empecemos con el ejemplo, tenemos una aplicación que se encarga de calcular el precio de una laptop.

Existen 3 tipos de laptop: pequeña, mediana y grande, según la cantidad de pulgadas. Para cada tipo existe un precio específico, podemos ver el diagrama sugerido.

Diseño inicial de aplicación - Patrón Decorador

Más adelante, se nos comunica que es necesario especificar los detalles de cada laptop, y además, conocer el precio según las diferentes combinaciones posibles para las máquinas, es decir: RAM, Tarjeta de Video y Disco Duro. Aquí podríamos optar por seguir realizando extensiones de la clase principal Laptop, pero esto nos daría lo que llamamos “explosión de clases”.

Diseño de clases que ejemplifica explosión de clases - Patrón Decorador

En el diagrama anterior, cada clase realiza el cálculo del precio según cada componente. Por ejemplo, una Laptop pequeña con RAM va a tener un costo específico, luego una Laptop grande con tarjeta de video otro costo y así para cada implementación.

¿Pueden identificar el problema con esta propuesta?

Cada vez que nos pidan agregar un componente nuevo, habría que agregarlo a los 3 tipos de portátiles básicos. Si algún componente actual cambia de precio (por ejemplo, la memoria RAM subió $20 dólares), entonces también habría que modificar TODAS las clases en donde se utilice ese componente.

Esto introduce más posibles errores y además un caos en toda la aplicación.

Un segundo intento puede ser agregar más métodos y atributos a la clase base Laptop y dejar que esta clase maneje el costo de los componentes adicionales. Con el método getPrice(), podemos evaluar si la Laptop (cualquier tipo) tiene o no memoria RAM, disco duro o tarjeta de video. De esta forma, la lógica está en un sólo lugar en nuestra clase base, y ésta nos va a indicar cuanto es el costo de los diferentes componentes.

Súper clase que contiene atributos y métodos generales - Patrón Decorador

Logramos recudir la cantidad de clases necesarias, pero… Aún tenemos el problema de agregar más componentes. Si en un futuro nos piden agregar el tipo de tarjeta madre, si tiene o no puertos USB, si incluye cámara, etc. Vamos a tener que modificar el método getPrice(), y agregar la lógica una y otra vez, haciendo el código poco mantenible y propenso a errores.

El principio “Abierto – Cerrado” del patrón Decorador, nos da una pista para poder solucionar este caos en nuestro diseño:

Las clases deben estar abiertas para extenderlas pero cerradas a modificaciones.

Como se menciona en el libro Head First Design Patterns, nuestras clases deben de aceptar extensiones, con el fin de agregar nuevos comportamientos, sin la necesidad de modificar el código actual.

Si logramos diseñar con este principio en mente, nuestras aplicaciones serán flexibles, y podríamos adoptar nuevos requerimientos rápidamente, sin modificar el código que ya funciona.

¿Cómo aplicamos este patrón a nuestro ejemplo?

Podemos dividirlo en 3 pasos principales:

  1. Utilice los componentes más básicos (Laptop Pequeña, Mediana y Grande)
  2. “Decórelo” con uno o más componentes (RAM, Tarjeta de video, disco duro, etc)
  3. Utilice el método getPrice() para obtener el costo dinámicamente.

Para explicar los pasos anteriores es mejor utilizar una imagen.

Diagrama que muestra los diferentes niveles

Definimos el componente base, en este caso una Laptop Pequeña, e implementamos el método getPrice(). El precio para este componente es de $500.

Luego, decoramos la Laptop Pequeña con la memoria RAM. Aquí el método getPrice() va a utilizar el precio de la RAM, por ejemplo, $50 y va a sumar el precio de la Laptop Pequeña ($500).

Volvemos a decorar el objeto con la tarjeta de video, getPrice() retorna el valor de la tarjeta de video ($75) sumando el resultado anterior ($550).

Finalmente, decoramos una vez más con el disco duro, getPrice() retorna el valor del disco duro ($100) sumando el resultado anterior ($625). En este paso se calcula el precio final: $725.

A continuación, el diagrama de clases propuesto para este patrón según el libro Head First Design Patterns.

Diagrama de clases de ejemplo - Patrón Decorador

En este libro también nos dan una definición oficial para este patrón: “El patrón decorador agrega a un objeto, responsabilidades adicionales de forma dinámica. Los decoradores brindan una alternativa flexible a herencia, cuando se requiere extender funcionalidades“.

En nuestro ejemplo, el Componente sería la clase Laptop. Los componentes concretos serían la Laptop Pequeña, mediana y grande. Es necesario crear una nueva clase que se encarga de agrupar los componentes llamada PartDecorator.

Las implementaciones del decorador, es decir, los decoradores concretos, serían las partes que se pueden agregar a una Laptop. Por ejemplo RAM, tarjeta de video y disco duro. El nuevo y mejorado diagrama sugerido sería así.

Diagrama de clases implementando el patrón decorador

Veamos las definiciones de las principales clases en código.

// Laptop.java file
public abstract class Laptop {
    // this will get implemented by all childs to get specific price
    public abstract double getPrice();
}

// SmallLaptop.java
public class SmallLaptop extends Laptop {
    // override this method
    @Override
    public double getPrice() {
        // return just the small laptop price
        return 500.0;
    }
}

// PartDecorator.java file
// We make this abstract to avoid instances of this class
// This class is used to wrap all decorators only
public abstract class PartDecorator extends Laptop {
}

// RAM.java file
public class RAM extends PartDecorator {
    // hold a composition relation with Laptop
    private Laptop laptop;

    // create constructor to force decorator by composition
    public RAM(Laptop _laptop) {
        this.laptop = _laptop;
    }

    // Get current RAM price first, then call the composition (decorator) price
    @Override
    public double getPrice() {
        return 50.0 + laptop.getPrice();
    }
}

// DecoratorPatternDemo.java file
public class DecoratorPatternDemo {
    public static void main (String[] args) {
        // create a concrete component
        Laptop smallLaptop = new SmallLaptop();

        // decorate small laptop with RAM part
        smallLaptop = new RAM(smallLaptop);
        // decorate small laptop, RAM with video card part
        smallLaptop = new VideoCard(smallLaptop);
        // decorate small laptop, RAM, video card with hard drive part
        smallLaptop = new HardDrive(smallLaptop);

        // getPrice will call all the decorator methods and it will add all prices
        System.out.println("Price: " + smallLaptop.getPrice());
    }
}

El resultado de ejecutar el código anterior es similar al de abajo. La diferencia, son los mensajes de log para visualizar mejor el proceso.

Resultado de ejecutar el código de ejemplo

Como podemos ver, el precio final se calcula dinámicamente, al delegar a cada decorador el llamado anidado del método getPrice(). Con este diseño podemos seguir decorando la portátil pequeña con más y nuevas partes, sin necesidad de modificar el código actual.

El código de ejemplo del patrón decorador lo pueden encontrar en este link.

En el próximo post vamos a analizar el patrón Fábrica Simple.

Recordá suscribirte aquí para recibir las últimas actualizaciones todas las semanas.

Referencias:
Libro Head First Design Patterns. Versión digital.

Leave a Reply

Your email address will not be published. Required fields are marked *