En este artículo vamos a hablar del patrón de diseño "Chain of Responsability". Como todos los patrones de diseño, nos ayudan a solucionar problemas comunes hablando un lenguaje habitual entre los desarrolladores. Personalmente, me gusta interiorizar los patrones de diseño y aunque hay bastantes ejemplos teóricos, no valoras lo que te aporta hasta que lo llegas a implementar en un caso real.

Así que hoy le toca el turno a "Chain of Responsability", uno de mis favoritos, ya que me ha ayudado en muchas ocasiones a escribir un código legible y mantenible. Esta semana, he tenido que implementar una funcionalidad donde me ha vuelto a encajar a la perfección. El objetivo de este artículo es presentaros el patrón, su aplicabilidad y ver el caso de uso real.

La definición de Chain of Responsability

Chain of Responsability es un patrón de diseño de comportamiento que permite pasar solicitudes a lo largo de una cadena de manejadores. Al recibir una solicitud, cada manejador decide procesar la solicitud o pasarla al siguiente controlador de la cadena.

¿Cómo traducimos esta definición? Básicamente, este patrón ayuda a encapsular acciones secuenciales sobre un objeto. Por ejemplo, en un sistema de pedidos donde hay que realizar una secuencia de pasos para una determinada acción. Un caso que se me ocurre es el de realizar un proceso de scoring sobre un pedido que puede partirse en distintos pasos.

Aunque la teoría suele mostrar un diagrama y un caso de uso específico, me gusta adaptar los patrones a las necesidades, por lo que no debemos de ser tan puristas. En este patrón, los handlers pueden directamente parar la cadena, pero también puedes hacer que pase la responsabilidad al siguiente habiendo concluida ya su acción. También puede ser que pase por toda la cadena pero solo ciertos handlers ejecuten pasos. Las posibilidades son diversas y van a depender de la necesidad de tu caso de uso.

¿Cómo sabemos cuando usarlo?

En mi caso suelo identificar la necesidad si tenemos un proceso basado en pasos, que tenemos que ir añadiendo verificaciones y cada vez que aumenta la funcionalidad el código empieza a estar plagado de ifs. Este patrón ayuda a separar las responsabilidades que realizar en cada paso. Por ejemplo, en el caso del scoring de un pedido, podemos identificar distintos pasos como pueden ser:

  • ¿El cliente del pedido ya tiene pedidos previos?
  • ¿El cliente del pedido realiza el pago con método de pago habitual?
  • ¿El pedido es superior a cierta cantidad?

Lo ideal es que cada paso del proceso pueda separarse en distintas clases con una sola responsabilidad, haciendo que el pedido/petición recorra la cadena de manejadores y obtengamos un resultado final.

¿Cómo lo implementamos?

Al igual que muchos otros patrones de diseño de comportamiento, la solución se basa en la transformación de comportamientos particulares en objetos independientes llamados Handlers. Básicamente cada verificación del proceso debe extraerse a su propia clase con un único método que realice la acción correspondiente. La solicitud, junto con sus datos y contexto, se pasa a este método como argumento para inicializar la cadena.

Existen un montón de ejemplos de implementaciones. Recomiendo echar un vistazo a la de refactoring.guru, igualmente al final del artículo incluiré un ejemplo concreto de implementación del patrón en ES6.

Ventajas del uso del patrón

Una vez que tenemos implementado nuestro patrón, ¿qué ventajas recibimos? ¿La jerarquía de clases que hemos creado para aplicarlo nos proporciona realmente utilidad? A continuación identifico las ventajas principales y qué principios SOLID cumplimos con el uso de este patrón:

  • Manejas el orden de los pasos: controla el orden en el que se ejecutan los pasos de manera dinámica.
  • Single Responsibility Principle: desacopla en clases la invocación de las operaciones y la realización de la propia acción, pudiendo testear fácilmente cada acción por separado.
  • Open/Closed Principle: permite ampliar con nuevos manejadores sin romper código existente, mejorando así la mantenibilidad.

Caso de uso real

A continuación voy a describir el caso de uso real donde he implementado este patrón. Desarrollando una aplicación React, tenía que implementar una serie de filtros sobre una serie de elementos. Por el volumen de esta serie de elementos se decidió implementar el filtrado directamente en el frontend y evitar llamadas al backend por cada interacción del usuario.

Estos filtros requerían distintos tipos de acciones, pudiendo tener dependencias entre ellos y actuando todos juntos. Además de acciones de filtrado había que implementar una serie de ordenados sobre los elementos.

Al final me abstraje del problema y lo que identifiqué es un conjunto de elementos sobre el que debía de implementarse una serie de pasos secuenciales para obtener un resultado final. De tal manera que le tocó el turno a Chain of Responsability.

Implementación

El primer paso siempre es identificar al handler, implementando una clase abstracta con los métodos que requiera para el caso de uso. En mi caso: aplicar el filtro y un método que identifique si ese handler debe ser ejecutado en función de los filtros activos por el usuario.

class AbstractFilterHandler {
  constructor() {}
 
  applyFilter() {
    this._WARNING("applyFilter(elements, filters)");
  }
 
  canIHandle() {
    this._WARNING("canIHandle(elements, filters)");
  }
 
  _WARNING(fName = "unknown method") {
    console.warn(
      'WARNING! Function "' + fName + '" is not overridden in ' + this.constructor.name
    );
  }
}
 
export default AbstractFilterHandler;

Una vez tenemos la clase abstracta debemos de implementar cada manejador específico. Para este artículo vamos a mostrar el ejemplo del manejador que filtra aquellos elementos que incluyen ciertos modelos:

import AbstractFilterHandler from "./filter";
 
class ModelsFilterHandler extends AbstractFilterHandler {
  applyFilter(elements, filters) {
    return elements.filter((a) => filters.models.includes(a.model.slug));
  }
 
  canIHandle(filters) {
    return filters.models && filters.models.length > 0;
  }
}
 
export default ModelsFilterHandler;

En el método canIHandle() indicamos que el handler solo se invocará cuando en los filtros contengan modelos. Por otro lado el método applyFilter() implementa la acción realizada sobre los elementos.

El siguiente paso es construir la cadena que se encarga de ejecutar el filtrado sobre los elementos y dar de alta los manejadores específicos:

import * as Handlers from "./filters";
 
class FilterChain {
  constructor(elements, filters) {
    this.elements = elements;
    this.filters = filters;
    this.handlers = [];
  }
 
  addHandler(handler) {
    this.handlers.push(handler);
  }
 
  applyFilters() {
    let elements = this.elements;
    this.handlers.forEach((handler) => {
      if (handler.canIHandle(this.filters)) {
        elements = handler.applyFilter(elements, this.filters);
      }
    });
    return elements;
  }
}
 
export default FilterChain;

Esta clase se encarga de recibir los elementos sobre los que se realizan la acción, el contexto de los filtros activos por el usuario y los manejadores que deben de ejecutarse. El método applyFilters() será el punto de entrada para la ejecución de la cadena.

Para ejecutar el patrón en nuestro código simplemente tenemos que dar de alta el chain y sus handlers:

let chain = new FilterChain(elements, filters);
chain.addHandler(new Handlers.ModelsFilterHandler());
chain.addHandler(new Handlers.OrderByPriceFilterHandler());
let result = chain.applyFilters();

Con estos sencillos pasos, hemos abstraído cada lógica de filtrado/ordenando en una clase distinta, evitando un código acoplado y sin una responsabilidad clara.

Espero que el caso de uso real os ayude a interiorizar mejor este patrón de diseño y se comience a darle más uso. ¿Has usado alguna vez este patrón? ¿Qué casos concretos te ha ayudado a solucionar?

Más información

Chain of Responsibility — Refactoring Guru

Dive into Chain of Responsibility pattern with detailed explanations, examples in multiple languages, and real-world analogies.

refactoring.guru

DesignPatternsPHP — Chain of Responsibilities

PHP implementation of the Chain of Responsibilities behavioral design pattern.

github.com