Design Patterns - Decorator

TLDR: El Composite es un patrón que te permite agregar nuevos comportamientos a un objeto existente sin alterarlo.


Este patrón de diseño estructural te permite agregar funcionalidades adicionales a un objeto existente de forma dinámica, siendo una alternativa flexible para extender funcionalidades sin modificar la implementación actual.

¿Porque necesitamos el Decorator?

Muchas veces en tu código se tiene una clase cuya funcionalidad especifica está finalizada, testeada y entregada. Pero se quiere agregar a ese objeto ciertas funcionalidades adicionales, pero sin reescribir o alterar el código existente, y sobre todo manteniendo el principio de diseño Open-Close Principle. Adicionalmente se quiere mantener esa nueva funcionalidad separada, aplicando el principio Single responsibility principle, y teniendo la posibilidad de interactuar con el objeto de que ha sido creado. Hay varias cosas a tener en cuenta con el planteamiento anterior, para esta necesidad hay dos opciones:

  • Herencia desde el objeto requerido. La cual no es la mejor opción porque la herencia es estática, lo que no permite alterar el comportamiento en ejecución.
  • Construir un Decorador. El planteamiento de este patrón es agregar funcionalidades adicionales a un objeto existente por medio del concepto de composición. Esto permitiría que un objeto tenga la referencia de otro, características que con la herencia no es posible.

El patrón Decorator también es llamado por otro nombre y es el de Wrapper, o envoltorio. Este nombre expresa claramente la idea principal de este patrón, la cual es, envolver la funcionalidad actual de un objeto con funcionalidades adiciones.

En definición, ¿que es el patrón Decorator?

Facilita adicionar comportamientos a objetos individuales sin heredar de ellos.

El problema en un ejemplo

Supongamos que tenemos una aplicación donde hay variedad de figuras geométricas, dichas figuras heredan de un objeto superior llamado forma (Shape), y actualmente esas formas tienen un resultado en pantalla, en este caso el método toString(). ¿Pero que pasa si queremos agregarle características adicionales a las figuras geométricas que actualmente tenemos?

En el siguiente ejemplo, se creará una clase base llamada Shape, pero como en Javascript no existen cosas como clases abstractas, la forma de implementarlo aquí es que se creará una clase y se dejan las funciones en blanco. Después de esto crearemos la clase Circle que extiendan de Shape por medio de la palabra reservada extends, e implementaremos la funcionalidad del Circulo.

class Shape {
  /* abstract */
}

class Circle extends Shape {
  constructor(radius = 0) {
    super();
    this.radius = radius;
  }

  resize(factor) {
    this.radius *= factor;
  }

  toString() {
    return `Un circulo de radio ${this.radius}`;
  }
}

Esta clase ya está terminada y tiene su funcionalidad bien definida. Pero digamos que necesitamos agregarle un color a este Circulo. Para esto, un acercamiento un poco invasivo sería agregar en la clase Shape el parámetro Color, como lo veremos a continuación.

class Shape {
  constructor(color) {
    this.color = color;
  }
}

Pero ahora el problema sería, que tendrías que modificar la clase Circle para incluir la implementación del color en su actual definición y básicamente ese es el problema que tiene la herencia. Al final, para poder agregar una acción tendrías que terminar modificando toda la jerarquía de clases para realizar una extensión de esta.

Para resolver esta situación usaremos el patrón decorador, que simplemente se encarga de envolver la clase original y agregarle información adicional. Para aplicar este patrón, primero que todo tendremos que construir un decorador mismo que extienda de nuestra clase base Shape, y este será la clase que envolverá la clase original, como lo veremos a continuación:

class ColoredShape extends Shape {
  constructor(shape, color) {
    super();
    this.shape = shape;
    this.color = color;
  }

  toString() {
    return `${this.shape.toString()} tiene el color ${this.color}`;
  }
}

Entonces en el código anterior ColoredShape que ha sido extendida de la clase Shape (que es la misma que extiende la clase Circle), y tendremos dos parámetros en el constructor, el Shape que es el objeto que se quiere decorar y el color con el que se decorará. Para que el decorador funcione acorde a lo que requerimos, tenemos que re-implementar o realizar un override de la función que queremos decorar.

Entonces, lo que se hace es ejecutar la función del Shape que se quiere decorar, en este caso se está ejecutando this.shape.toString() (Recuerda que esto objeto puede ser cualquier cosa que herede de Shape, Ej: Circle, Square, etc) y allí se agrega la información que se quiere incluir, en este caso sería tiene el color ${this.color}.

Ahora, para utilizar el decorador, realizaremos el siguiente proceso.

let circle = new Circle(2);
console.log(circle.toString());
// Un circulo de radio 2

// Decorador
let redCircle = new ColoredShape(circle, "rojo");
console.log(redCircle.toString());
// Un circulo de radio 2 tiene el color rojo

Ahora podemos analizar el resultado y ver el Decorador en acción. Una cosa bastante útil de los decoradores es que se pueden decorar a si mismo. Vamos a crear otro decorador y ver el comportamiento.

class TransparentShape extends Shape {
  constructor(shape, transparency) {
    super();
    this.shape = shape;
    this.transparency = transparency;
  }

  toString() {
    return `${this.shape.toString()} y una transparencia de ${
      this.transparency * 100.0
    }%`;
  }
}

Y para aplicarlo lo haremos de la siguiente forma:

let circle = new Circle(2);
console.log(circle.toString());
// Un circulo de radio 2

// Decorador
let redCircle = new ColoredShape(circle, "rojo");
console.log(redCircle.toString());
// Un circulo de radio 2 tiene el color rojo

let redHalfCircle = new TransparentShape(redCircle, 0.5);
console.log(redHalfCircle.toString());
// Un circulo de radio 2 tiene el color rojo y una transparencia de 50%

Y así es como se pueden componer diferentes decoradores. Aquí en este resultado podemos ver como se hace un override de la clase toString(), se ejecuta lo de la clase Shape y se agrega el decorador TransparentShape.

Código completo del ejemplo: https://bit.ly/2z37iP5

Estructura del patrón Decorator

En este patrón encontramos varios componentes que interactuaran entre si.

  1. El Componente, en el diagrama llamado Shape. Es la declaración de una interfaz en común para ambos elementos, el wrapper y el objeto al que se le hará wrapper.
  2. El Componente en Concreto, en el diagrama llamado Circle, es la clase de objeto que se le hará wrapper. En esta clase se define el comportamiento básico de la aplicación, que se podría alterar con los decoradores.
  3. Los Decoradores en concreto, que en el diagrama los llamamos TransparentShape y ColoredShape. Son las clases en donde se definirá el comportamiento extra que se le agregarán a los componentes dinámicamente. Los decoradores en concreto sobrescribirán los métodos de la base del decorador y ejecutarán su comportamiento, ya sea, antes o después de llamar el método padre.
  4. El Cliente es la implementación del código en el que se envolverán los componentes en múltiples capas de decoradores, siempre y cuando su jerarquía sea interfaz del componente.

Conclusiones

  • El patrón de diseño estructural Decorador, te permite tener la habilidad de agregarle comportamientos adicionales a objetos en tiempo de ejecución sin tener que modificar el código base existente.
  • Usando el patrón decorator podrás separar lógicas de negocios muy especificas permitiendo aplicar fácilmente el principio de Single responsibility principle.

Ejercicio

Se requiere que se construya una solución para registrar en consola todos los eventos de la clase MySQlDatabase, para las funciones:

  • read(id);
  • save(data);
  • update(id, data);
  • delete(id);

Se requiere crear una solución aplicando el patrón Decorator, sin alterar la clase MySQlDatabase, para poder tener un registro de todas las transacciones de la base de datos.

Estructura Inicial

Solución esperada

Recursos adicionales y bibliografia

Alexander Shvets - Dive into design Patterns

Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides - Design Patterns Elements of Reusable Object-Oriented Software (1994)

Addy Osmani - Learning JavaScript Design Patterns (2012)

https://www.dofactory.com/javascript/Decorator-design-pattern

https://loredanacirstea.github.io/es6-design-patterns/#decorator