DEV Community

Cover image for Patrón decorador (parte 1)
leobar37
leobar37

Posted on • Updated on

Patrón decorador (parte 1)

El patrón decorador es un patrón estructural que nos ayuda a poder agregar funcionalidades a una clase de manera dinámica.

Si quisiéramos compararlo con la vida real podría ser con una hamburguesería. Primero tendríamos el proceso general, el cual es tomar el pedido y entregarlo al cliente, el cliente pide una hamburguesa, una gaseosa y que de todas las cremas solo quiere mayonesa.

Como el proceso general sigue siendo el mismo, que es entregar el pedido todos estos procesos reciben el pedido como si fueran el "cliente", pero en realidad no lo son, solo van a agregar algo al pedido, por ejemplo, salió la hamburguesa, la de las gaseosas tomo el pedido y le puso una gaseosa ahora lo delega para que le pongan cremas, cada proceso que afecte al pedio la agregue cosas a este serán decoradores.

Sé que es un poco tonto el ejemplo. Pero fue lo único que se me ocurrió, llevemos esto a código.

type Order = string[];
interface Detail {
  creams: {
    mayonnaise: boolean;
    ketchup: boolean;
  };
  soda: boolean;
}

interface ICustomerService {
  receiveOrder(detail: Detail): void;
  deliverOder(): Order;
}
Enter fullscreen mode Exit fullscreen mode

Este sería mi planteamiento inicial, Ahora implementemos esto en una clase.

class DeliverHamburguer implements ICustomerService {
  receiveOrder(detail: Detail): void {
    console.log('details');
    console.log(detail);
  }
  deliverOder(): Order {
    return ['A hamburguer'];
  }
}
Enter fullscreen mode Exit fullscreen mode

Aquí es donde entra en juego el patrón decorador. ¿Por qué?. Bueno quizás en un ejemplo real, planteaste desde un inicio poder entregar gaseosas y cremas, pero aquí vamos a manejar las gaseosas y las cremas como funcionalidades por aparte suponiendo que recibir y entregar la hamburguesa es un montón de código y agregar la venta de gaseosas sería a aún más código.

Entonces ahora quieres poder agregar mayonesas a tu hamburguesa. Como solución aquí se podría plantear la herencia extendiendo la clase y agregar otra funcionalidad, pero después de un tiempo quieres vender gaseosas, esta sería otra funcionalidad, emplear herencia traería consigo ciertos problemas:

  1. La herencia es estática: No podemos alterar las clases en tiempo de ejecución, si quitar o agregar una funcionalidad se tendría que hacer algún cambio en el código.

  2. Solo se puede extender de una sola clase: En el caso de JavaScript solo podemos extender desde una sola clase. Si quisiéramos agregar funcionalidad se tendría que heredar clase tras clase.

 Ahora volvamos código. El patrón decorador propone crear una clase a la cual llamaremos wrapper. Lo que hace es tomar el objeto y delega sus solicitudes, el sentido de esto es que en vez utilizar herencia, se haga otra clase con la funcionalidad y poder llegar utilizarla empleando agregaciòn o composiciòn por eso tenemos una referencia a dicho objeto la cual se pasa por parámetro.

Ahora para delegar las solicitudes y que este siga teniendo la misma estructura, implementamos la misma interfaz que en este caso sería ICustomerService.

class CustomerServiceBaseDecorator implements ICustomerService {
  wrappee: ICustomerService;
  constructor(obj: ICustomerService) {
    this.wrappee = obj;
  }
  receiveOrder(detail: Detail): void {
    this.wrappee.receiveOrder(detail);
  }
  deliverOder(): Order {
    return this.wrappee.deliverOder();
  }
}
Enter fullscreen mode Exit fullscreen mode

Como se puede observar wrappe es cualquier objeto que implemente la interfaz ICustomerService.

class CreamsDecorator extends CustomerServiceBaseDecorator {
  detail: Detail;
  deliverOder() {
    const order = super.deliverOder();
    if (this.detail.creams.ketchup) {
      order.push('Add ketchup');
    }
    if (this.detail.creams.mayonnaise) {
      order.push('Add mayonnaise');
    }
    return order;
  }
  receiveOrder(details: Detail) {
    this.detail = details;
    super.receiveOrder(details);
  }
}
Enter fullscreen mode Exit fullscreen mode

Todos los objetos que extiendan al wrapper son decoradores, en este caso este es un decorador encargado de manejar las cremas, veamos un poco a detalle esto.

  • EL método deliverOrder ejecuta primero el método del objeto objetivo, manipula la orden y la retorna.

  • En el método receiveOrder primero guarda el detalle de la orden, después ejecuta el método del objeto objetivo. Recuerda que estás llamando el método de la clase padre puedes probar
      poniendo this y ocasionar una recursión infinita.

Esta es la razón de existir de decorador, puedes manipular las solicitudes antes y después y con base en esto agregar más funcionalidades. Ahora implementemos las sodas.

class SodasDecorator extends CustomerServiceBaseDecorator {
  detail: Detail;
  deliverOder() {
    const order = super.deliverOder();
    if (this.detail.soda) {
      order.push('Add Soda');
    }
    return order;
  }
  receiveOrder(details: Detail) {
    this.detail = details;
    super.receiveOrder(details);
  }
}
Enter fullscreen mode Exit fullscreen mode

Listo ahora veamos como funcionaria esto

let detail: Detail = {
  creams: {
    ketchup: true,
    mayonnaise: true
  },
  soda: true
};

const services = {
  sodas: true,
  creams: true
};

let obj = new DeliverHamburguer();

if (services.creams) {
  const creamsDecorator = new CreamsDecorator(obj);
  obj = creamsDecorator;
}

if (services.sodas) {
  const sodasDecorator = new SodasDecorator(obj);
  obj = sodasDecorator;
}

obj.receiveOrder(detail);

console.log(obj.deliverOder());
// OUTPUT: [ 'A hamburguer', 'Add ketchup', 'Add mayonnaise', 'Add Soda' ]
Enter fullscreen mode Exit fullscreen mode

Bien, ahora supongamos que por A/B razón ya no se puede ofrecer gaseosas, ya te puedes imaginar lo fácil que es quitar esa funcionalidad.

Segundo ejemplo

Ahora veamos un segundo ejemplo. Supongamos que estamos haciendo una aplicación de mensajería y te toca programar la parte del envío.

interface Sender {
  send(data: string, receiver: number): void;
}

class SenderMessage implements Sender {
  send(data: string, receiver: number) {
    console.log('data send');
    console.log(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

Ahora apliquemos decorador para poder extender las funcionalidades de una clase a futuro.

class SenderMessageDecorator implements Sender {
  private wrapper: Sender;
  constructor(sender: Sender) {
    this.wrapper = sender;
  }
  send(data: string, receiver: number): void {
    this.wrapper.send(data, receiver);
  }
}
Enter fullscreen mode Exit fullscreen mode

Listo, ahora se te pide realizar un backup de los mensajes.

class BackupMessages extends SenderMessageDecorator {
  backup = new Map<number, string>();
  getBackup() {
    return Array.from(this.backup.values()).join('\n');
  }
  send(data: string, receiver: number) {
    this.backup.set(receiver, data);
    super.send(data, receiver);
  }
}
Enter fullscreen mode Exit fullscreen mode

Listo, ahora se nos ocurre que sería bueno implementar filtros de palabras, por si alguien se le ocurre decir groserías.

type Filter = (data: string) => boolean;
class DecoratorFilters extends SenderMessageDecorator {
  filters: Filter[] = [];
  setFilters(...filters: Filter[]) {
    this.filters = filters;
  }
  send(data: string, receiver: number) {
    const canbe = this.filters.every(filter => filter(data));
    if (!canbe) {
      console.error(
        data + ' is not permitted by the filters and will not be sent'
      );
      return;
    }
    super.send(data, receiver);
  }
}
Enter fullscreen mode Exit fullscreen mode

Ahora no contentos con eso se nos ocurre agregar una estructura al mensaje de manera que sea receiver:message.

class NormalizeText extends SenderMessageDecorator {
  send(data: string, receiver: number) {
    const normalized = `${receiver}:${data}`;
    super.send(normalized, receiver);
  }
}
Enter fullscreen mode Exit fullscreen mode

Aún no contentos con eso, se nos ocurre agregar eventos para notificar él antes y después del envío del mensaje.

class EventsDecorator extends SenderMessageDecorator {
  beforeSendObserver = new Subject<void>();
  afterSendObserver = new Subject<void>();
  onBeforeSend(callback: () => void) {
    this.beforeSendObserver.suscribe(callback);
  }
  onAfterSend(callback: () => void) {
    this.afterSendObserver.suscribe(callback);
  }
  send(data: string, receiver: number) {
    this.beforeSendObserver.next();
    super.send(data, receiver);
    setTimeout(() => {
      this.afterSendObserver.next();
    }, 1000);
  }
}
Enter fullscreen mode Exit fullscreen mode

Por cierto la clase Subject viene del patrón observer del anterior post, código aquí y listo suficiente :).

Ahora probemos lo que hemos hecho.

let options = {
  backup: true,
  events: true,
  normalize: true,
  filters: true
};
let sender = new SenderMessage();

if (options.backup) {
  const backup = new BackupMessages(sender);
  sender = backup;
  setTimeout(() => {
    console.log('backup');
    console.log((backup as BackupMessages).getBackup());
  }, 1500);
}

if (options.events) {
  const events = new EventsDecorator(sender);
  sender = events;
  events.onBeforeSend(() => {
    console.log('after send');
  });
  events.onBeforeSend(() => {
    console.log('before send');
  });
}

if (options.normalize) {
  sender = new NormalizeText(sender);
}

if (options.filters) {
  const filters = new DecoratorFilters(sender);
  sender = filters;
  const barWords = (data: string) => {
    return data !== 'shit';
  };
  filters.setFilters(barWords);
}

sender.send('Hello', 1);
sender.send('Hello', 2);
sender.send('Hello', 3);
sender.send('Hello', 4);
sender.send('shit', 5);
Enter fullscreen mode Exit fullscreen mode

La data ha sido normalizada, los eventos están funcionando, el backup se realiza y un aviso de que la alerta de que la última palabra no se envió por el filtro que se puso, si ahora se quisiera deshabilitar alguna funcionalidad, no hay ningún problema solo manipula options y listo.

Happy code :)
Codigo completo aquí

Discussion (0)