Errores y mejoras del patrón de la Cadena de Responsabilidad

Recientemente escribí dos programas Java (para el sistema operativo Microsoft Windows) que deben captar eventos de teclado globales generados por otras aplicaciones que se ejecutan simultáneamente en el mismo escritorio. Microsoft proporciona una forma de hacerlo registrando los programas como un oyente de gancho de teclado global. La codificación no tomó mucho tiempo, pero la depuración sí. Los dos programas parecieron funcionar bien cuando se probaron por separado, pero fallaron cuando se probaron juntos. Otras pruebas revelaron que cuando los dos programas se ejecutaban juntos, el programa que se lanzaba primero no siempre podía captar los eventos clave globales, pero la aplicación que se lanzaba más tarde funcionaba bien.

Resolví el misterio después de leer la documentación de Microsoft. El código que registra el programa en sí como un detector de enganches no tenía la CallNextHookEx()llamada requerida por el marco de enganches. La documentación dice que cada escucha de gancho se agrega a una cadena de gancho en el orden de inicio; el último oyente iniciado estará en la parte superior. Los eventos se envían al primer oyente de la cadena. Para permitir que todos los oyentes reciban eventos, cada oyente debe realizar la CallNextHookEx()llamada para retransmitir los eventos al oyente a su lado. Si algún oyente se olvida de hacerlo, los siguientes oyentes no obtendrán los eventos; como resultado, sus funciones diseñadas no funcionarán. Esa fue la razón exacta por la que mi segundo programa funcionó, ¡pero el primero no!

El misterio se resolvió, pero no estaba satisfecho con el marco del gancho. Primero, me obliga a "recordar" insertar la CallNextHookEx()llamada al método en mi código. En segundo lugar, mi programa podría desactivar otros programas y viceversa. ¿Por qué pasa eso? Porque Microsoft implementó el marco de enlace global siguiendo exactamente el patrón clásico de Cadena de Responsabilidad (CoR) definido por Gang of Four (GoF).

En este artículo, analizo el vacío legal de la implementación del CDR sugerido por el Gobierno de Filipinas y propongo una solución. Eso puede ayudarlo a evitar el mismo problema cuando cree su propio marco de CDR.

CoR clásico

El patrón CoR clásico definido por GoF en Design Patterns :

"Evite acoplar el remitente de una solicitud a su receptor dando a más de un objeto la oportunidad de manejar la solicitud. Encadene los objetos receptores y pase la solicitud a lo largo de la cadena hasta que un objeto la maneje".

La figura 1 ilustra el diagrama de clases.

Una estructura de objeto típica podría verse como la Figura 2.

De las ilustraciones anteriores, podemos resumir que:

  • Varios manejadores pueden manejar una solicitud.
  • Solo un controlador maneja realmente la solicitud
  • El solicitante solo conoce una referencia a un controlador
  • El solicitante no sabe cuántos controladores pueden manejar su solicitud
  • El solicitante no sabe qué manejador manejó su solicitud
  • El solicitante no tiene ningún control sobre los controladores
  • Los controladores pueden especificarse dinámicamente
  • Cambiar la lista de controladores no afectará el código del solicitante

Los siguientes segmentos de código demuestran la diferencia entre el código del solicitante que usa CoR y el código del solicitante que no lo hace.

Código de solicitante que no usa CoR:

manejadores = getHandlers (); for (int i = 0; i <handlers.length; i ++) {handlers [i] .handle (solicitud); if (manejadores [i] .handled ()) rompen; }

Código de solicitante que utiliza CoR:

 getChain (). handle (solicitud); 

A partir de ahora, todo parece perfecto. Pero veamos la implementación que sugiere GoF para el CoR clásico:

Manejador de clase pública {sucesor del Manejador privado; Handler público (HelpHandler s) {sucesor = s; } identificador público (solicitud ARequest) {if (sucesor! = null) successor.handle (solicitud); }} public class AHandler extiende Handler {public handle (ARequest request) {if (someCondition) // Handling: hacer algo más super.handle (request); }}

La clase base tiene un método, handle()que llama a su sucesor, el siguiente nodo de la cadena, para manejar la solicitud. Las subclases anulan este método y deciden si permitir que la cadena continúe. Si el nodo maneja la solicitud, la subclase no llamará super.handle()al sucesor, y la cadena tiene éxito y se detiene. Si el nodo no maneja la solicitud, la subclase debe llamar super.handle()para mantener la cadena en marcha, o la cadena se detiene y falla. Debido a que esta regla no se aplica en la clase base, no se garantiza su cumplimiento. Cuando los desarrolladores olvidan hacer la llamada en subclases, la cadena falla. El defecto fundamental aquí es que la toma de decisiones de ejecución de la cadena, que no es el negocio de las subclases, se combina con el manejo de solicitudes en las subclases.. Eso viola un principio del diseño orientado a objetos: un objeto debe ocuparse únicamente de sus propios asuntos. Al dejar que una subclase tome la decisión, le presenta una carga adicional y la posibilidad de error.

Laguna del marco de enlace global de Microsoft Windows y el marco de filtro de servlet de Java

La implementación del marco de enlace global de Microsoft Windows es la misma que la implementación clásica de CoR sugerida por GoF. El marco depende de los oyentes de hook individuales para realizar la CallNextHookEx()llamada y retransmitir el evento a través de la cadena. Asume que los desarrolladores siempre recordarán la regla y nunca olvidarán hacer la llamada. Por naturaleza, una cadena de ganchos de eventos global no es un CDR clásico. El evento debe entregarse a todos los oyentes de la cadena, independientemente de si un oyente ya lo maneja. Entonces, la CallNextHookEx()llamada parece ser el trabajo de la clase base, no de los oyentes individuales. Dejar que los oyentes individuales hagan la llamada no sirve de nada e introduce la posibilidad de detener la cadena accidentalmente.

El marco del filtro de servlets de Java comete un error similar al del gancho global de Microsoft Windows. Sigue exactamente la implementación sugerida por GoF. Cada filtro decide si lanzar o detener la cadena llamando o no doFilter()al siguiente filtro. La regla se aplica mediante javax.servlet.Filter#doFilter()documentación:

"4. a) Invoque la siguiente entidad en la cadena usando el FilterChainobjeto ( chain.doFilter()), 4. b) o no pase el par solicitud / respuesta a la siguiente entidad en la cadena de filtros para bloquear el procesamiento de la solicitud".

Si un filtro se olvida de hacer la chain.doFilter()llamada cuando debería haberlo hecho, deshabilitará otros filtros en la cadena. Si un filtro hace que la chain.doFilter()llamada cuando debería no tener, invocará otros filtros en la cadena.

Solución

Las reglas de un patrón o marco deben aplicarse a través de interfaces, no de documentación. Contar con que los desarrolladores recuerden la regla no siempre funciona. La solución es desacoplar la toma de decisiones de ejecución de la cadena y el manejo de solicitudes moviendo la next()llamada a la clase base. Deje que la clase base tome la decisión y deje que las subclases solo manejen la solicitud. Al alejarse de la toma de decisiones, las subclases pueden concentrarse completamente en su propio negocio, evitando así el error descrito anteriormente.

CoR clásico: envíe la solicitud a través de la cadena hasta que un nodo maneje la solicitud

Esta es la implementación que sugiero para el CoR clásico:

/ ** * Classic CoR, es decir, la solicitud es manejada por solo uno de los manejadores de la cadena. * / public abstract class ClassicChain {/ ** * El siguiente nodo de la cadena. * / Private ClassicChain siguiente; Public ClassicChain (NextNode de ClassicChain) {next = nextNode; } / ** * Punto de inicio de la cadena, llamado por el cliente o pre-nodo. * Llame a handle () en este nodo y decida si continuar con la cadena. Si el siguiente nodo no es nulo y * este nodo no manejó la solicitud, llame a start () en el siguiente nodo para manejar la solicitud. * @param solicita el parámetro de solicitud * / public final void start (solicitud ARequest) {boolean handlingByThisNode = this.handle (solicitud); if (next! = null &&! handlingByThisNode) next.start (solicitud); } / ** * Llamado por start ().* @param solicita el parámetro de solicitud * @return un booleano indica si este nodo manejó la solicitud * / manejador booleano abstracto protegido (solicitud ARequest); } public class AClassicChain extiende ClassicChain {/ ** * Llamado por start (). * @param solicita el parámetro de solicitud * @return un booleano indica si este nodo manejó la solicitud * / identificador booleano protegido (solicitud ARequest) {boolean handleByThisNode = false; if (someCondition) {// Manejo handleByThisNode = true; } return handleByThisNode; }}if (someCondition) {// Manejo handleByThisNode = true; } return handleByThisNode; }}if (someCondition) {// Manejo handleByThisNode = true; } return handleByThisNode; }}

The implementation decouples the chain execution decision-making logic and request-handling by dividing them into two separate methods. Method start() makes the chain execution decision and handle() handles the request. Method start() is the chain execution's starting point. It calls handle() on this node and decides whether to advance the chain to the next node based on whether this node handles the request and whether a node is next to it. If the current node doesn't handle the request and the next node is not null, the current node's start() method advances the chain by calling start() on the next node or stops the chain by not calling start() on the next node. Method handle() in the base class is declared abstract, providing no default handling logic, which is subclass-specific and has nothing to do with chain execution decision-making. Subclasses override this method and return a Boolean value indicating whether the subclasses handle the request themselves. Note that the Boolean returned by a subclass informs start() in the base class whether the subclass has handled the request, not whether to continue the chain. The decision of whether to continue the chain is completely up to the base class's start() method. The subclasses can't change the logic defined in start() because start() is declared final.

In this implementation, a window of opportunity remains, allowing the subclasses to mess up the chain by returning an unintended Boolean value. However, this design is much better than the old version, because the method signature enforces the value returned by a method; the mistake is caught at compile time. Developers are no longer required to remember to either make the next() call or return a Boolean value in their code.

Non-classic CoR 1: Send request through the chain until one node wants to stop

This type of CoR implementation is a slight variation of the classic CoR pattern. The chain stops not because one node has handled the request, but because one node wants to stop. In that case, the classic CoR implementation also applies here, with a slight conceptual change: the Boolean flag returned by the handle() method doesn't indicate whether the request has been handled. Rather, it tells the base class whether the chain should be stopped. The servlet filter framework fits in this category. Instead of forcing individual filters to call chain.doFilter(), the new implementation forces the individual filter to return a Boolean, which is contracted by the interface, something the developer never forgets or misses.

Non-classic CoR 2: Regardless of request handling, send request to all handlers

For this type of CoR implementation, handle() doesn't need to return the Boolean indicator, because the request is sent to all handlers regardless. This implementation is easier. Because the Microsoft Windows global hook framework by nature belongs to this type of CoR, the following implementation should fix its loophole:

 /** * Non-Classic CoR 2, i.e., the request is sent to all handlers regardless of the handling. */ public abstract class NonClassicChain2 { /** * The next node in the chain. */ private NonClassicChain2 next; public NonClassicChain2(NonClassicChain2 nextNode) { next = nextNode; } /** * Start point of the chain, called by client or pre-node. * Call handle() on this node, then call start() on next node if next node exists. * @param request the request parameter */ public final void start(ARequest request) { this.handle(request); if (next != null) next.start(request); } /** * Called by start(). * @param request the request parameter */ protected abstract void handle(ARequest request); } public class ANonClassicChain2 extends NonClassicChain2 { /** * Called by start(). * @param request the request parameter */ protected void handle(ARequest request) { //Do handling. } } 

Ejemplos

En esta sección, le mostraré dos ejemplos de cadenas que usan la implementación para CoR 2 no clásico descrita anteriormente.

Ejemplo 1