Saltar al contenido principal

Chain Of Responsibility

Nombres

  • COR

PROBLEMA

Supongamos que tenemos una solicitud. Esa solicitud debe de pasar por diferentes niveles jerárjicos para establecer quién o quiénes pueden manejarla. Una solución a este problema es la utilización del patrón Chain of Responsibility.

También, imaginemos que usted tiene un sistema de solicitudes, el cual puede ser manejado por diferentes Handlers.

Sistema simple de manejadores

Figura 1. Elaboración propia

Sin embargo su sistema crece demasiado y ocupa una solución que le provea flexibilidad. Es decir, que sea simple añadir manejadores o remover. El patrón chain of responsibility es una excelente opción.

Sistema complejo de solicitudes

Figura 2. Elaboración propia

SOLUCIÓN

Para dicho sistema, tal y como se mencionó, lo ideal es establecer una cadena de responsabilidad, en el cual, la solicitud se va pasando de manejador en manejador, hasta que uno o varios, manejen la solicitud.

Manejadores pasándose la solicitud

Figura 3. Imagen tomada de Refactoring Guru

Básicamente se establece una cadena de objetos encadenados. Cada objeto tiene la opción de manejar la solicitud o pasarla al siguiente objeto en la cadena. Esto reduce el acoplamiento, ya que el remitente solo necesita enviar la solicitud al primer objeto de la cadena, y cada objeto decide si puede manejarla o pasarla al siguiente objeto. Esto permite una mejor mantenibilidad y flexibilidad del código, ya que los objetos de manejo pueden agregarse o eliminarse sin afectar al remitente.

Estructura

Estrucutra de COR

Figura 4. Imagen tomada de Gamma et al

En sí, la estrucutra esta formada por los siguientes elementos:

  • Una interfaz llamada Handler, la cual se utiliza como base para los concreteHandlers, establece el método HandleRequest() y un apuntador hacia su sucesor de tipo Handler.
  • ConcreteHandlers, son los manjadores concretos, implementan la interfaz Handler.
  • Cliente, es quien interactúa con la cadena enviando la solicitud.

Ejemplo con la UCR: Sistema de Solicitudes

Imagine que la universidad tiene un sistema de solicitudes, la cual reparte las solicitudes entre todos sus órganos (Registro, Oficina de Becas, Oficina de Bienestar y Salud, ...)

Imagen del ejemplo de la UCR. Imagen 1

Figura 5. Elaboración propia

Sin embargo, cada vez que se cree un órgano en la institución, se deberá integrar en el sistema o bien, si uno cierra se deberá de eliminar, mismamente una solicitud puede ser tratada por varios órganos a la vez. Por lo tanto una vía sencilla de manejar este sistema es por medio de la implementación del patrón: Chain of Responsibility.

Imagen del ejemplo de la UCR. Imagen 2

Figura 6. Elaboración propia

El sistema principal sería el cliente, y cada uno de los órganos de la UCR, sería un concreteHandler.

COLABORACIONES

El sistema de colaboración es muy sencillo. El cliente envía la solicitud y cada concrete Handler propaga la solicitud entre los sucesores.

Colaboraciones en COR

Figura 6. Elaboración propia

Implementación

A la hora de implementar una aplicación con un patrón de diseño chain of responsibility, se deben tomar en cuenta las siguientes consideraciones:

Dos formas de implementar la cadena de sucesión

Hay dos principales variaciones a la hora de implementar la cadena de sucesión:

  1. Crear nuevos links: es básicamente cuando no existe ningún tipo de relación entre los objetos de la cadena que se desea.
  2. Usar links existentes: a veces, ya existe una jerarquía que define relaciones entre los objetos que se quieren relacionar, por lo que se pueden reutilizar estos links. Un ejemplo de esto son jerarquías de clases donde cada instancia tiene una propiedad parent, y yo puedo usar ese parent como referencia al sucesor.

Conectar sucesores

Si no existe una relación entre clases que me permita crear una cadena, yo debo crearla manualmente. Esto significa que ahora el Handler no solo define una interfaz para los requests, sino que también tiene un sucesor. El tener este sucesor como atributo le permite tener una implementación default para el método de Handle. Entonces, si un ConcreteHandler no está interesado en manejar el request, simplemente no sobreescribe el método Handle, porque por default ya llama al sucesor a manejarlo.

Imagen

Representación de requests

Hay dos formas de manejar los requests a la hora de utilizar el patrón the chain of responsibility. La primera es hacer un método Handle para cada request posible. Esto es conveniente y muy seguro, sin embargo, es poco flexible, pues habría que crear un método por cada request.

La otra alternativa es más flexible pero más insegura. Se trata de crear un único método Handle llamado HandleRequest. A este método se le pasa el tipo de request como parámetro (ya sea como un entero, string, etc). El único requisito es que tanto el receptor como el emisor estén de acuerdo con el formato de request. Las desventajas son que ahora se necesita una lógica de ifs y no se puede asegurar que el parámetro que se pase sea de un tipo de datos correcto.

Una solución a este problema del tipado del parámetro es crear una clase abstracta Request, que va a ser heredada por los diferentes tipos de requests. Finalmente, se pasa un puntero de un objeto Request al método HandleRequest y este lo desempaca dependiendo del tipo de request que sea (el tipo de request se obtiene al agregar un método a la clase Request que retorne el tipo).

Imagen

Ejemplo en código

Supongamos que se quiere programar un ATM. Este por dentro tiene la capacidad de dispensar 3 tipos de billetes: 50$, 20$ y 10$. Para cada tipo de billete hay un dispensador diferente. Entonces, por dentro del ATM se tiene un dispensador de 50$, uno de 20$ y uno de 10$. Por convención, el ATM quiere dispensar la menor cantidad de billetes posibles, o sea, entre más grande el billete, mejor. Para eso, el sistema debería primero dispensar billetes de 50$, luego de 20$ y finalmente de 10$. Esto empieza a sonar como una cadena de responsabilidades. Lo que se quiere entonces es que el primer dispensador siempre sea el de 50$. Y conforme sea necesario, vaya bajando en la cadena hasta que termine en los billetes más pequeños. Se tiene entonces el siguiente código:

Se crea una clase wrapper para el monto solicitado:

Aquí tienes las líneas corregidas con el formato correcto de Markdown:

Imagen

Ahora, se crea la clase que va a servir como Handler y cuyos hijos serán los Handlers concretos. Esta va a tener el método para definir su sucesor y el método para procesar el request.

image

Ahora se realizan las 3 clases de ConcreteHandlers, que corresponden a los 3 dispensadores. Cada una sobreescribe los dos métodos anteriores:

image

image

image

Y finalmente, se crea la cadena y se configura el orden de la misma:

image

Consecuencias

Malas

El recibimiento de los requests no está garantizado: Como no hay un receptor explícito, no hay garantía de que un request vaya a ser manejado. Puede ser que se caiga al final de la cadena y nunca fue manejado por nadie. Esto también puede pasar si no se configura bien la cadena.

Buenas

Menos acoplamiento: Este patrón libera al objeto de tener que saber a quién le manda el request, cómo funciona la cadena de responsabilidades, o cómo está estructurada. Ningún objeto sabe de la existencia del otro, solo sabe que si él no puede manejar un request, el sucesor puede que sí.

Flexibilidad al asignar responsabilidad a los objetos: El orden de responsabilidades es muy flexible, tanto que se puede alterar en tiempo de ejecución. Y las responsabilidades se pueden distribuir entre objetos muy fácilmente.

Relación con otros patrones

Composite

En Composite, el parent de un componente puede actuar como su sucesor, al igual que en ciertos casos con Chain of Responsibility.

Principios SOLID

PrincipioDescripción
Single ResponsibilityCada handler en la cadena tiene una única responsabilidad, que es manejar un tipo específico de mensaje.
Open/Close PrincipleEl patrón permite extender el comportamiento de la cadena agregando nuevos handlers sin modificar los existentes.
Liskov’s Substitution PrincipleLos handlers pueden ser sustituidos por subclases o implementaciones concretas sin alterar el funcionamiento del patrón.
Dependency InversionPermite que los clientes dependan de abstracciones (interfaz de handler) en lugar de depender de implementaciones concretas.

Principios de Diseño asociados

El patrón de Chain of Responsibility (COR) se relaciona con los siguientes patrones de diseño de software:

  1. KISS (Keep It Simple, Stupid): COR permite mantener la simplicidad al evitar acoplamientos innecesarios entre el remitente de una solicitud y los objetos que pueden manejarla, lo que facilita la comprensión y el mantenimiento del código.

  2. OOP (Object-Oriented Programming): COR se basa en los principios de la programación orientada a objetos, como la encapsulación y la abstracción, al separar el procesamiento de una solicitud en objetos independientes y permitir que se agreguen o modifiquen.

  3. DRY (Don't Repeat Yourself): COR promueve la reutilización del código al permitir que múltiples objetos en la cadena manejen diferentes aspectos de una solicitud sin duplicar la lógica en cada uno de ellos, evitando así la repetición y mejorando la mantenibilidad.

Bibliografía

Gamma, Erich et.al. “Design Patterns”. Addison-Wesley, 1995.

Refactoring.Guru. (2023). Chain of Responsibility. Refactoring.Guru.