Les écueils et améliorations du modèle de chaîne de responsabilité

Récemment, j'ai écrit deux programmes Java (pour Microsoft Windows OS) qui doivent capturer les événements clavier globaux générés par d'autres applications exécutées simultanément sur le même bureau. Microsoft fournit un moyen de le faire en enregistrant les programmes en tant qu'écouteur de hook clavier global. Le codage n'a pas pris longtemps, mais le débogage l'a fait. Les deux programmes semblaient bien fonctionner lorsqu'ils étaient testés séparément, mais échouaient lorsqu'ils étaient testés ensemble. D'autres tests ont révélé que lorsque les deux programmes s'exécutaient ensemble, le programme qui s'était lancé en premier était toujours incapable de capturer les événements clés mondiaux, mais l'application lancée plus tard fonctionnait très bien.

J'ai résolu le mystère après avoir lu la documentation Microsoft. Le code qui enregistre le programme lui-même en tant qu'écouteur de hook manquait l' CallNextHookEx()appel requis par le framework hook. La documentation lit que chaque écouteur de hook est ajouté à une chaîne de hook dans l'ordre de démarrage; le dernier auditeur démarré sera en haut. Les événements sont envoyés au premier auditeur de la chaîne. Pour permettre à tous les écouteurs de recevoir des événements, chaque auditeur doit effectuer l' CallNextHookEx()appel pour relayer les événements à l'auditeur à côté. Si un auditeur oublie de le faire, les écouteurs suivants n'obtiendront pas les événements; par conséquent, leurs fonctions conçues ne fonctionneront pas. C'était la raison exacte pour laquelle mon deuxième programme a fonctionné mais le premier n'a pas fonctionné!

Le mystère a été résolu, mais je n'étais pas satisfait du cadre de crochet. Tout d'abord, il me faut "me souvenir" d'insérer l' CallNextHookEx()appel de méthode dans mon code. Deuxièmement, mon programme pourrait désactiver d'autres programmes et vice versa. Pourquoi cela arrive-t-il? Parce que Microsoft a implémenté le framework de hook global en suivant exactement le modèle classique de Chain of Responsibility (CoR) défini par le Gang of Four (GoF).

Dans cet article, je discute de la faille de la mise en œuvre du CdR suggérée par le GoF et je propose une solution. Cela peut vous aider à éviter le même problème lorsque vous créez votre propre cadre CdR.

CoR classique

Le modèle classique de CoR défini par le GoF dans les modèles de conception :

"Évitez de coupler l'expéditeur d'une demande à son récepteur en donnant à plus d'un objet une chance de traiter la demande. Chaîne les objets de réception et transmettez la demande le long de la chaîne jusqu'à ce qu'un objet la gère."

La figure 1 illustre le diagramme de classes.

Une structure d'objet typique peut ressembler à la figure 2.

À partir des illustrations ci-dessus, nous pouvons résumer que:

  • Plusieurs gestionnaires peuvent être en mesure de traiter une demande
  • Un seul gestionnaire gère réellement la demande
  • Le demandeur ne connaît qu'une référence à un gestionnaire
  • Le demandeur ne sait pas combien de gestionnaires sont capables de traiter sa demande
  • Le demandeur ne sait pas quel gestionnaire a traité sa demande
  • Le demandeur n'a aucun contrôle sur les gestionnaires
  • Les gestionnaires peuvent être spécifiés dynamiquement
  • La modification de la liste des gestionnaires n'affectera pas le code du demandeur

Les segments de code ci-dessous illustrent la différence entre le code du demandeur qui utilise le CoR et le code du demandeur qui ne le fait pas.

Code du demandeur qui n'utilise pas CoR:

gestionnaires = getHandlers (); for (int i = 0; i <handlers.length; i ++) {handlers [i] .handle (request); if (handlers [i] .handled ()) break; }

Code du demandeur qui utilise CoR:

 getChain (). handle (requête); 

À partir de maintenant, tout semble parfait. Mais regardons la mise en œuvre suggérée par le GoF pour le CdR classique:

public class Handler {successeur du gestionnaire privé; gestionnaire public (HelpHandler s) {successeur = s; } public handle (ARequest request) {if (successor! = null) successor.handle (request); }} classe publique AHandler étend Handler {public handle (ARequest request) {if (someCondition) // Manipulation: faire autre chose super.handle (request); }}

La classe de base a une méthode handle(),, qui appelle son successeur, le nœud suivant de la chaîne, pour gérer la demande. Les sous-classes remplacent cette méthode et décident d'autoriser ou non la chaîne à avancer. Si le nœud gère la demande, la sous-classe n'appellera pas super.handle()qui appelle le successeur, et la chaîne réussit et s'arrête. Si le nœud ne gère pas la demande, la sous-classe doit appeler super.handle()pour maintenir la chaîne en mouvement, ou la chaîne s'arrête et échoue. Étant donné que cette règle n'est pas appliquée dans la classe de base, sa conformité n'est pas garantie. Lorsque les développeurs oublient de faire l'appel dans les sous-classes, la chaîne échoue. Le défaut fondamental ici est que la prise de décision d'exécution de la chaîne, qui n'est pas l'affaire des sous-classes, est associée au traitement des demandes dans les sous-classes. Cela viole un principe de conception orientée objet: un objet ne doit s'occuper que de ses propres affaires. En laissant une sous-classe prendre la décision, vous lui imposez une charge supplémentaire et la possibilité d'une erreur.

Faille du framework de hook global Microsoft Windows et du framework de filtre de servlet Java

L'implémentation du framework de hook global Microsoft Windows est identique à l'implémentation CoR classique suggérée par le GoF. Le cadre dépend des écouteurs de hook individuels pour passer l' CallNextHookEx()appel et relayer l'événement à travers la chaîne. Cela suppose que les développeurs se souviendront toujours de la règle et n'oublieront jamais de passer l'appel. Par nature, une chaîne de crochet événementielle mondiale n'est pas un CdR classique. L'événement doit être livré à tous les écouteurs de la chaîne, indépendamment du fait qu'un écouteur le gère déjà. L' CallNextHookEx()appel semble donc être le travail de la classe de base, pas des auditeurs individuels. Laisser les auditeurs individuels passer l'appel ne sert à rien et introduit la possibilité d'arrêter accidentellement la chaîne.

Le framework de filtre de servlet Java fait une erreur similaire à celle du hook global Microsoft Windows. Il suit exactement la mise en œuvre suggérée par le GoF. Chaque filtre décide s'il faut lancer ou arrêter la chaîne en appelant ou non doFilter()le filtre suivant. La règle est appliquée via la javax.servlet.Filter#doFilter()documentation:

"4. a) Soit invoquer l'entité suivante de la chaîne en utilisant l' FilterChainobjet ( chain.doFilter()), 4. b), soit ne pas transmettre la paire demande / réponse à l'entité suivante de la chaîne de filtrage pour bloquer le traitement de la demande."

Si un filtre oublie de faire l' chain.doFilter()appel quand il aurait dû, il désactivera les autres filtres de la chaîne. Si un filtre effectue l' chain.doFilter()appel alors qu'il n'aurait pas dû, il invoquera d'autres filtres de la chaîne.

Solution

Les règles d'un modèle ou d'un framework doivent être appliquées via des interfaces, pas de la documentation. Compter sur les développeurs pour se souvenir de la règle ne fonctionne pas toujours. La solution est de découpler la prise de décision d'exécution de la chaîne et la gestion des requêtes en déplaçant l' next()appel vers la classe de base. Laissez la classe de base prendre la décision et laissez les sous-classes gérer uniquement la demande. En évitant la prise de décision, les sous-classes peuvent se concentrer complètement sur leurs propres activités, évitant ainsi l'erreur décrite ci-dessus.

CoR classique: envoyez la demande via la chaîne jusqu'à ce qu'un nœud gère la demande

Voici l'implémentation que je suggère pour le CdR classique:

 /** * Classic CoR, i.e., the request is handled by only one of the handlers in the chain. */ public abstract class ClassicChain { /** * The next node in the chain. */ private ClassicChain next; public ClassicChain(ClassicChain nextNode) { next = nextNode; } /** * Start point of the chain, called by client or pre-node. * Call handle() on this node, and decide whether to continue the chain. If the next node is not null and * this node did not handle the request, call start() on next node to handle request. * @param request the request parameter */ public final void start(ARequest request) { boolean handledByThisNode = this.handle(request); if (next != null && !handledByThisNode) next.start(request); } /** * Called by start(). * @param request the request parameter * @return a boolean indicates whether this node handled the request */ protected abstract boolean handle(ARequest request); } public class AClassicChain extends ClassicChain { /** * Called by start(). * @param request the request parameter * @return a boolean indicates whether this node handled the request */ protected boolean handle(ARequest request) { boolean handledByThisNode = false; if(someCondition) { //Do handling handledByThisNode = true; } return handledByThisNode; } } 

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. } } 

Exemples

Dans cette section, je vais vous montrer deux exemples de chaîne qui utilisent l'implémentation pour CoR 2 non classique décrite ci-dessus.

Exemple 1