Patrón decorator

Introducción

Si hay un patrón de diseño que suele provocar algunas dudas, es el patrón Decorator. Pero básicamente, este patrón nos permite añadir responsabilidades a objetos de forma dinámica, es decir, que podemos ajustar el comportamiento de los objetos durante la ejecución de la aplicación. Con ello podemos ir añadiendo o quitando los cambios en el comportamiento en tiempo real. Esto por ejemplo, no sería posible con herencia, ya que se realiza de forma estática y no podemos cambiarla en tiempo de ejecución. La herencia tampoco nos permitiría tener varias clases madre (en la mayoría de lenguajes), por lo que no podemos "unir" varios de esos cambios de comportamiento.

Ejemplo

Para entenderlo un poco mejor, partamos de un ejemplo sencillo del mundo "videojueguil". Pensemos en un juego como el reboot de Tomb Raider, en el que las armas tienen modificaciones. Estas modificaciones incrementan el daño de las armas, la precisión, etc.

Lo anterior podríamos resolverlo de muchas formas, pero una de ellas es con decoradores, podemos tener el arco base, al que si le añadimos "Palas Reforzadas", se incrementa el daño, si aparte le añadimos "Cuerda Trenzada" incrementamos más el daño, etc.

Pues bien, cada tipo de "añadido" sería un decorador que modificaría las propiedades del arco, y lo mejor de todo, es que podremos tener varios decoradores de forma simultánea. De forma que un arco, siguiendo el ejemplo anterior podrá tener las "Palas Reforzadas" y la "Cuerda Trenzada" de forma simultánea.

Diagrama

classDiagram class Componente { <<interface>> +operación1() +operaciónN() } class ComponenteConcreto { +operación1() +operaciónN() } class Decorador { +operación1() +operaciónN() } class DecoradorConcretoA { +operación1() +operaciónN() } class DecoradorConcretoN { +operación1() +operaciónN() } Componente --o Decorador Componente <|.. ComponenteConcreto Componente <|.. Decorador Decorador <|-- DecoradorConcretoA Decorador <|-- DecoradorConcretoN

En el diagrama UML, vemos que básicamente hay una interfaz que implementa las operaciones públicas de la clase que queremos "envolver" con decoradores, y que dicha clase, pasará a implementar la interfaz. Por otro lado, tenemos la parte de decoradores, en la que tenemos un decorador base y los decoradores hijos, todos ellos implementando la interfaz inicial.

Paso a paso

Con todo lo anterior, tendríamos los pasos siguientes para implementar el patrón Decorator:

  1. Extraer a una interfaz los métodos de la clase o las clases que queremos decorar.
  2. Crear un decorador base que implemente la interfaz y que reciba además dicha interfaz como parámetro en su constructor.
  3. Crear clases hijas del decorador, que se encargarán de actuar sobre la clase base.

Ejemplo

Ahora que tenemos las nociones, hemos visto el UML, y tenemos "la chuleta" para aplicarlo, vamos a ver un ejemplo muy sencillo a aplicar en un juego de naves. Nuestra nave parte de un arma base que puede modificarse, y también puede tener mini-naves a los lados que le ayuden.

Si seguimos el primer paso del apartado anterior, lo primero que haremos será sacar a una interfaz los métodos públicos de nuestra clase.

interface Nave {
  getDano(): number;
  getMiniNaves(): number;
}

class NaveEspacial implements Nave {
  private dano: number;
  private miniNaves: number;
  
  public constructor(dano: number, miniNaves: number) {
    this.dano = dano;
    this.miniNaves = miniNaves;
  }
  
  public getDano(): number {
    return this.dano;
  }
  
  public getMiniNaves(): number {
    return this.miniNaves;
  }
}

El segundo paso es crear un decorador base:

// Este decorador base vemos que simplemente llama al objeto que ha recibido como parámetro.
class BaseDecorator implements Nave {
  private nave: Nave;
  
  public constructor(nave: Nave) {
    this.nave = nave;
  }
  
  public getDano() {
    return this.nave.getDano();
  }
  
  public getMiniNaves() {
    return this.nave.getMiniNaves();
  }
}

Y finalmente, definimos los decoradores que necesitamos, En nuestro caso, tenemos la posibilidad de añadir misiles y lásers, además de mini-naves. Como podéis ver en este ejemplo, los decoradores no tienen por qué modificar siempre todos los métodos públicos:

class MisilesDecorator extends BaseDecorator {
  public getDano() {
    return super.getDano() + 20;
  }
}

class LaserDecorator extends BaseDecorator {
  public getDano() {
    return super.getDano() + 50;
  }
}

class MiniNaveDecorator extends BaseDecorator {
   public getMiniNaves() {
    return this.nave.getMiniNaves() + 1;
  }
}

Ahora que tenemos los tres decoradores, pasemos a jugar con ellos para ver los resultados.

function logNave(nombre: string, nave: Nave) {
    console.log(`La nave "${nombre}" hace un daño ${nave.getDano()} unidades y tiene ${nave.getMiniNaves()} mini nave/s`);
  }

const nave = new NaveEspacial(100, 0);
logNave("Sencilla", nave);
// La nave "Sencilla" hace un daño 100 unidades y tiene 0 mini nave/s

logNave("Con Mini", new MiniNaveDecorator(nave));
// La nave "Con Mini" hace un daño 100 unidades y tiene 1 mini nave/s

logNave("Con 2 Minis", new MiniNaveDecorator(new MiniNaveDecorator(nave)));
// La nave "Con 2 Minis" hace un daño 100 unidades y tiene 2 mini nave/s

logNave("Con Misiles", new MisilesDecorator(nave));
// La nave "Con Misiles" hace un daño 120 unidades y tiene 0 mini nave/s

logNave("Con Láser", new LaserDecorator(nave));
// La nave "Con Láser" hace un daño 150 unidades y tiene 0 mini nave/s

logNave("Con Misiles y Mini", new MisilesDecorator(new MiniNaveDecorator(nave)));
// La nave "Con Misiles y Mini" hace un daño 120 unidades y tiene 1 mini nave/s

Y listo, ya tenemos nuestro primer ejemplo de patrón Decorator listo para entregar.

Desventajas

  • Hay que tener mucho cuidado con el uso del objeto original, ya que si en algún momento lo hemos decorado, pero en otros sitios del código estamos usando el objeto sin decoración, puede provocar problemas y ser complejo de encontrar si no lo recordamos.
  • Puede que se necesiten generar demasiados decoradores para unas clases, lo que nos generará muchísimas clases pequeñas, que si no están bien organizadas, serán difíciles de seguir.
  • Si se necesitan llamadas a métodos públicos que no van a ser usados por decoradores, tocará implementar igualmente la llamada del decorador base. Por ejemplo, si en tu clase sólo necesitas decorar, 1 o 2 métodos, y tienes otros 10 métodos públicos para los que no es necesario, quizás debas de darle una vuelta al código.
  • En algunos casos los decoradores deben tener un orden específico para usarse, si no provocarán otro tipo de resultados. Es por ello, que estos casos hay que documentarlos muy bien y dejar el mínimo margen de error.
  • Es complejo poder quitar un decorador de la pila aplicada al objeto base. Por lo que o cada vez que haya que cambiar decoradores los aplicas de nuevo uno a uno, o debes implementar un sistema para poder quitar decoradores intermedios.

Conclusiones

El patrón Decorator, como todos los patrones, ofrece mucha potencia para ciertos casos, pero hay que tener cuidado con saber distinguirlos bien, puesto que como hemos visto en las desventajas, su uso libre en todas las ocasiones que lo sugieran puede complicar las cosas en lugar de hacerlas más sencillas. Es por ello, que te recomiendo que experimentes y analices bien cada situación que lo requiera. Eso sí, como habéis podido comprobar, es muy sencillo de llevar a cabo.


Comparte este artículo con quien quieras
AbortController: cancelando operaciones en JavaScript
Faker: usa datos realistas en tus pruebas y demos

Leave a Comment

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.