Patrones de diseño: Chain of Responsability

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

¿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 identificar 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 clase con una sola responsabilidad, haciendo que el pedido/petición por 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 corresponde. 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 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

Y 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 que 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
  • Aplicas el principio 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
  • Aplicas el principio 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. A continuación la clase abstracta AbstractFilterHandler implementada:

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 implantar 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;

Como vemos en la implementación, 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 el de construir la cadena que se encarga de ejecutar el filtrado sobre los elementos y dar de alta los manejadores específicos. Para ello hemos creado la clase FilterChain:

import * as Handlers from "./filters";

class FilterChain {
constructor(elements, filters) {
this.elements = elements;
this.filters = filters;
this.handlers = [];
}

/**
*
* @param {AbstractFilterHandler} filter
*/
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;

Como vemos, 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 y llamar al método applyFilters:

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

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. Y tú ¿Has usado alguna vez este patrón de diseño? ¿Que casos concretos te ha ayudado a solucionar? ¿Que ventajas te ha aportado?

Más información del patrón

https://refactoring.guru/design-patterns/chain-of-responsibility

https://github.com/domnikl/DesignPatternsPHP/tree/master/Behavioral/ChainOfResponsibilities

Artículo publicado en https://adrianalonso.es/arquitectura-del-software/patrones-de-diseno-chain-of-responsability/

Full Stack Web Developer — adrianalonso.es