Die Fallstricke und Verbesserungen des Chain of Responsibility-Musters

Kürzlich habe ich zwei Java-Programme (für Microsoft Windows OS) geschrieben, die globale Tastaturereignisse erfassen müssen, die von anderen Anwendungen generiert wurden, die gleichzeitig auf demselben Desktop ausgeführt werden. Microsoft bietet eine Möglichkeit, dies zu tun, indem die Programme als globaler Tastatur-Hook-Listener registriert werden. Das Codieren dauerte nicht lange, das Debuggen jedoch. Die beiden Programme schienen gut zu funktionieren, wenn sie getrennt getestet wurden, scheiterten jedoch, wenn sie zusammen getestet wurden. Weitere Tests ergaben, dass das zuerst gestartete Programm bei der Ausführung der beiden Programme immer nicht in der Lage war, die globalen Schlüsselereignisse zu erfassen, die später gestartete Anwendung jedoch einwandfrei funktionierte.

Ich habe das Rätsel gelöst, nachdem ich die Microsoft-Dokumentation gelesen hatte. Dem Code, der das Programm selbst als Hook-Listener registriert, fehlte der CallNextHookEx()vom Hook-Framework geforderte Aufruf. In der Dokumentation heißt es, dass jeder Hook-Listener in der Reihenfolge des Starts zu einer Hook-Kette hinzugefügt wird. Der zuletzt gestartete Listener befindet sich oben. Ereignisse werden an den ersten Listener in der Kette gesendet. Damit alle Listener Ereignisse empfangen können, muss jeder Listener den CallNextHookEx()Anruf tätigen , um die Ereignisse an den Listener daneben weiterzuleiten. Wenn ein Listener dies vergisst, erhalten die nachfolgenden Listener die Ereignisse nicht. Infolgedessen funktionieren ihre entworfenen Funktionen nicht. Das war genau der Grund, warum mein zweites Programm funktionierte, das erste jedoch nicht!

Das Rätsel wurde gelöst, aber ich war mit dem Hakenrahmen unzufrieden. Zunächst muss ich mich "erinnern", um den CallNextHookEx()Methodenaufruf in meinen Code einzufügen . Zweitens könnte mein Programm andere Programme deaktivieren und umgekehrt. Warum passiert das? Weil Microsoft das globale Hook-Framework genau nach dem klassischen Muster der Verantwortungskette (ChR of Responsibility, CoR) implementiert hat, das von der Gang of Four (GoF) definiert wurde.

In diesem Artikel diskutiere ich die von GoF vorgeschlagene Lücke in der Umsetzung des AdR und schlage eine Lösung dafür vor. Dies kann Ihnen helfen, dasselbe Problem zu vermeiden, wenn Sie Ihr eigenes AdR-Rahmenwerk erstellen.

Klassischer AdR

Das klassische AdR-Muster, das von GoF in Design Patterns definiert wurde :

"Vermeiden Sie es, den Absender einer Anfrage an den Empfänger zu koppeln, indem Sie mehr als einem Objekt die Möglichkeit geben, die Anfrage zu bearbeiten. Verketten Sie die empfangenden Objekte und leiten Sie die Anfrage entlang der Kette weiter, bis ein Objekt sie bearbeitet."

Abbildung 1 zeigt das Klassendiagramm.

Eine typische Objektstruktur könnte wie in Abbildung 2 aussehen.

Aus den obigen Abbildungen können wir Folgendes zusammenfassen:

  • Möglicherweise können mehrere Handler eine Anforderung bearbeiten
  • Nur ein Handler bearbeitet die Anforderung tatsächlich
  • Der Anforderer kennt nur einen Verweis auf einen Handler
  • Der Anforderer weiß nicht, wie viele Handler seine Anforderung bearbeiten können
  • Der Anforderer weiß nicht, welcher Handler seine Anforderung bearbeitet hat
  • Der Anforderer hat keine Kontrolle über die Handler
  • Die Handler können dynamisch angegeben werden
  • Das Ändern der Handlerliste wirkt sich nicht auf den Code des Anforderers aus

Die folgenden Codesegmente zeigen den Unterschied zwischen Anforderercode, der AdR verwendet, und Anforderungscode, der dies nicht tut.

Anforderungscode, der keinen AdR verwendet:

handlers = getHandlers (); für (int i = 0; i <handlers.length; i ++) {handlers [i] .handle (Anfrage); if (handlers [i] .handled ()) break; }}

Anforderungscode, der AdR verwendet:

 getChain (). handle (Anfrage); 

Ab sofort scheint alles perfekt zu sein. Aber schauen wir uns die Implementierung an, die GoF für den klassischen AdR vorschlägt:

Handler der öffentlichen Klasse {Nachfolger des privaten Handlers; öffentlicher Handler (HelpHandler s) {Nachfolger = s; } public handle (ARequest-Anfrage) {if (Nachfolger! = null) successor.handle (Anfrage); }} öffentliche Klasse AHandler erweitert Handler {öffentliches Handle (ARequest-Anforderung) {if (someCondition) // Handling: etwas anderes tun super.handle (Anforderung); }}

Die Basisklasse verfügt über eine Methode, handle()die ihren Nachfolger, den nächsten Knoten in der Kette, aufruft, um die Anforderung zu verarbeiten. Die Unterklassen überschreiben diese Methode und entscheiden, ob die Kette weiterlaufen soll. Wenn der Knoten die Anforderung verarbeitet, super.handle()ruft die Unterklasse nicht auf , die den Nachfolger aufruft, und die Kette ist erfolgreich und stoppt. Wenn der Knoten die Anforderung nicht verarbeitet, muss die Unterklasse aufrufen super.handle(), um die Kette am Laufen zu halten, oder die Kette stoppt und schlägt fehl. Da diese Regel in der Basisklasse nicht erzwungen wird, kann ihre Konformität nicht garantiert werden. Wenn Entwickler vergessen, den Aufruf in Unterklassen durchzuführen, schlägt die Kette fehl. Der grundlegende Fehler hierbei ist, dass die Entscheidungsfindung für die Kettenausführung, die nicht Sache von Unterklassen ist, mit der Bearbeitung von Anforderungen in den Unterklassen verbunden ist. Das verstößt gegen ein Prinzip des objektorientierten Designs: Ein Objekt sollte sich nur um sein eigenes Geschäft kümmern. Indem Sie eine Unterklasse die Entscheidung treffen lassen, belasten Sie sie zusätzlich und die Möglichkeit von Fehlern.

Lücke des globalen Windows Windows-Hook-Frameworks und des Java-Servlet-Filter-Frameworks

Die Implementierung des globalen Microsoft Windows-Hook-Frameworks entspricht der von GoF vorgeschlagenen klassischen CoR-Implementierung. Das Framework hängt von den einzelnen Hook-Listenern ab, um den CallNextHookEx()Anruf zu tätigen und das Ereignis über die Kette weiterzuleiten. Es wird davon ausgegangen, dass Entwickler sich immer an die Regel erinnern und nie vergessen, den Anruf zu tätigen. Eine globale Event-Hook-Kette ist von Natur aus kein klassischer AdR. Das Ereignis muss an alle Listener in der Kette gesendet werden, unabhängig davon, ob ein Listener es bereits verarbeitet. Der CallNextHookEx()Anruf scheint also die Aufgabe der Basisklasse zu sein, nicht der einzelnen Zuhörer. Es hilft nichts, wenn die einzelnen Zuhörer den Anruf tätigen, und es besteht die Möglichkeit, die Kette versehentlich anzuhalten.

Das Java-Servlet-Filter-Framework macht einen ähnlichen Fehler wie der globale Microsoft Windows-Hook. Es folgt genau der von GoF vorgeschlagenen Implementierung. Jeder Filter entscheidet, ob die Kette gerollt oder gestoppt werden soll, indem doFilter()der nächste Filter aufgerufen wird oder nicht . Die Regel wird durch javax.servlet.Filter#doFilter()Dokumentation durchgesetzt :

"4. a) Rufen Sie entweder die nächste Entität in der Kette mit dem FilterChainObjekt ( chain.doFilter()) auf, 4. b) oder geben Sie das Anforderungs- / Antwortpaar nicht an die nächste Entität in der Filterkette weiter, um die Anforderungsverarbeitung zu blockieren."

Wenn ein Filter vergisst, den chain.doFilter()Anruf zu tätigen , wenn er sollte, werden andere Filter in der Kette deaktiviert. Wenn ein Filter den chain.doFilter()Aufruf ausführt, wenn dies nicht der Fall sein sollte , werden andere Filter in der Kette aufgerufen .

Lösung

Die Regeln eines Musters oder eines Frameworks sollten über Schnittstellen und nicht über die Dokumentation durchgesetzt werden. Es funktioniert nicht immer, sich darauf zu verlassen, dass Entwickler sich an die Regel erinnern. Die Lösung besteht darin, die Entscheidungsfindung für die Kettenausführung und die Bearbeitung von Anforderungen zu entkoppeln, indem der next()Aufruf in die Basisklasse verschoben wird. Lassen Sie die Basisklasse die Entscheidung treffen und Unterklassen nur die Anforderung bearbeiten. Durch das Vermeiden von Entscheidungen können sich Unterklassen vollständig auf ihr eigenes Geschäft konzentrieren und so den oben beschriebenen Fehler vermeiden.

Klassischer AdR: Senden Sie eine Anfrage über die Kette, bis ein Knoten die Anfrage bearbeitet

Dies ist die Implementierung, die ich für den klassischen AdR vorschlage:

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

Beispiele

In diesem Abschnitt zeige ich Ihnen zwei Kettenbeispiele, die die oben beschriebene Implementierung für nicht klassisches AdR 2 verwenden.

Beispiel 1