Heute nun Teil 4, der sich mit der Zuständigkeitskette beschäftigt.
Hinweis: Die Überschriften der jeweiligen Kapitel sind direkt mit Andy's Originalbeitrag verknüpft.
Einführung in Entwurfsmuster (Design Patterns)
Von Andy Kramek
Die Zuständigkeitskette (Chain of Responsibility)
Was ist eine Zuständigkeitskette und wie setze ich sie ein?
Im vorangegangenen Kapitel wurde mit dem Strategiemuster eine Lösung aufgezeigt wie mit verschiedenen alternativen Implementierungen zur Laufzeit umgegangen werden kann. Die Zuständigkeitskette beschreibt ein alternatives Muster das dieses Problem von einer anderen Seite aus beleuchtet.
Wie erkenne ich, wann ich eine Zuständigkeitskette benötige?
Die formale Definition der Zuständigkeitskette gemäß „GoF“ lautet:
Vermeide die Kopplung des Auslösers einer Anfrage mit seinem Empfänger, indem mehr als ein Objekt die Möglichkeit enthält, die Aufgabe zu erledigen. Verkette die empfangenden Objekte und leite die Anfrage an der Kette entlang, bis ein Objekt sie erledigt.Im vorangegangenen Beispiel habe ich aufgezeigt, wie eine Strategie eingesetzt wird, um ortsspezifische Mehrwertsteuersätze zu verarbeiten. Hierbei haben wir jedoch gesehen, dass zur Implementierung einer Strategie irgendein Objekt zur Laufzeit entscheiden muss, welche der möglichen Unterklassen implementiert werden soll. Dies dürfte nicht immer erwünscht oder gar möglich sein.
Innerhalb einer Zuständigkeitskette kann jedes Objekt selbst beurteilen, wie es eine Anfrage zu verarbeiten hat und, sollte es die Anfrage nicht selbst verarbeiten können, weiß es nur, wie diese an ein anderes Objekt weitergereicht wird, und somit haben wir eine Kette. Die Konsequenz daraus ist, dass der Klient (welcher die Aktionsanfrage initiiert) somit nur noch das erste Objekt innerhalb der Kette kennen muss. Darüber hinaus muss jedes Objekt innerhalb der Kette nur das nächste Glied der Kette kennen. Die Zuständigkeitskette kann sowohl als vordefinierte (statisch) oder dynamische Kette (zur Laufzeit erzeugt jedes Objekt im Bedarfsfall seinen eigenen Nachfolger) implementiert werden.
Welches sind die Komponenten einer Zuständigkeitskette?
Eine Zuständigkeitskette kann durch die Erzeugung einer abstrakten Steuerungsklasse (‚Handler‘) implementiert werden, welche das Interface und die generischen Funktionalitäten spezifiziert, und durch anschließendes Erzeugen der konkreten Unterklassen um die diversen möglichen Implementierungen zu definieren. Es ist jedoch keinesfalls eine starre Vorgabe für die Definition aller Glieder einer Zuständigkeitskette, dass diese von derselben Klasse abgeleitet sein müssen um sicherzustellen, dass sie alle das über notwendige Interface zu Integration anderer Kettenglieder verfügen. Klienten Objekte benötigen eine Referenz auf die spezifische Unterklasse, welche ihren individuellen Einstiegspunkt in die Kette darstellt. Dies bedeutet jedoch wiederum nicht, dass alle Klienten denselben Einstiegspunkt benutzen müssen.
Und wieder können wird das grundsätzliche Brückenmuster erkennen. Dies liegt daran, dass jede Verknüpfung innerhalb der Kette letztlich eine Brücke zwischen einer Abstraktion und einer Implementierung darstellt. Der einzige Unterschied zur simplen Brücke liegt darin, dass jedes einzelne Objekt in Abhängigkeit seiner jeweiligen Situation, beide Rollen übernehmen kann.
Wie implementiere ich eine Zuständigkeitskette?
Die Frage, die ich im Zusammenhang mit dem Strategiemuster gestellt habe war „Wie gehe ich die Berechnung der Umsatzsteuer an, wenn die Berechnung vom Verkaufsort abhängt“. Sie werden sich erinnern, dass die Lösung so aussah, dass spezielle Unterklassen definiert wurden um jede Steuerrate gezielt verarbeiten zu können. Im Anschluss wurde dann auf Basis einer Tabelle entschieden, welche Unterklasse für welchen Ort benötigt wird.
Um dasselbe Problem mit einer Zuständigkeitskette zu lösen können wir die ursprüngliche Steuerberechnungsklasse heranziehen, die wir für die Strategie definiert haben und fügen die folgenden Eigenschaften hinzu:
cCanHandle Definiert den Kontext der mit dieser Klasse verarbeitet werden kann
cNextObj Name des als nächstes zu instanziierenden Objektes in der Kette
cNextObjLib Klassenbibliothek des nächsten Objektes in der Kette
oNext Objektreferenz des nächsten Objektes in der Kette
Die Eigenschaft ‚cCanHandle‘ definiert den ‚Kontext‘ den die spezielle Instanz akzeptiert, wohingegen die ‚Next‘-Eigenschaften genutzt werden, um das nachfolgende Objekt zu definieren. Dieses könnte zum einen vorbesetzt sein, um eine vordefinierte Kette zu erzeugen, oder wir könnten sogar zur Laufzeit die relevanten Werte festlegen um eine frei erweiterbare sich den Bedürfnissen anpassende Kette zu erzeugen.
Stellen wir uns beispielsweise einmal vor, dass als Kontext des ersten Objektes eine 7.00%ige Steuerrate definiert ist. In diesem Fall könnte dieses Objekt feststellen, dass bei einer übergebenen Steuerrate die über dem eigenen Kontext liegt, es keinen Sinn macht, ein Objekt zu instanziieren, dass eine niedrigere Steuerrate verarbeitet. Wie implementieren wir so etwas? Eine Möglichkeit wäre, eine Tabelle mit einer Liste der Kontexte der relevanten Klassen (und Bibliotheken) vorzuhalten. Dann könnte jedes Objekt in der Kette einfach die benötigte Information nachschauen.
Zusätzlich zu den bereits ober beschriebenen Eigenschaften benötigen wir mindestens zwei Methoden:
ProcessRequest: Sichtbare Methode die genutzt wird um das Objekt aufzurufen
und die entscheidet ob eine spezielle Anfrage durch das Objekt
verarbeitet wird.
CalcTax: Die eigentliche Methode welche die Berechnung durchführt und
das Ergebnis zurückgibt.
Das Klient Objekt muss entweder über eine Referenz auf das erste Objekt der Kette verfügen oder diese erzeugen können. Es wird dann die ProcessRequest()-Methode des Objektes aufrufen und sowohl den Kontext als auch den Verkaufspreis übergeben, dessen Mehrwertsteuer benötigt wird. Der folgende Code geht davon aus, dass die Zuständigkeitskette entweder einen gültigen Mehrwertsteuerbetrag oder NULL zurück gibt:
WITH ThisForm
* Prüfen, ob das erste Objekt der Kette verfügbar ist
IF VARTYPE( This.oCalc ) # [O]
* Objekt muss erzeugt werden
.oCalc = NEWOBJECT( [TaxChain01], [chor.vcx] )
ENDIF
* Nun die ProcessRequest Methode aufrufen und sowohl Kontext als auch Preis übergeben
lnTax = .oCalc.ProcessRequest( cContext, nSalePrice )
IF ISNULL( lnTax )
* Berechnung konnte nicht durchgeführt werden
MESSAGEBOX( [Lokation konnte nicht berechnet werden], 16, [Fehlgeschlagen] )
lnTax = 0
ENDIF
RETURN lnTax
ENDWITH
Der Code innerhalb der ProcessRequest Methode ist innerhalb der abstrakten Klassendefinition hinterlegt und komplett generisch. Er entscheidet darüber, ob eine ankommende Berechnungsanforderung lokal abgearbeitet werden kann. Ist das der Fall ruft er einfach die Methode CalcTax() auf (ebenfalls innerhalb der abstrakten Klasse definiert und verarbeitet den übergebenen Preis und die eingebettete Steuerrate). Kann die Berechnung nicht lokal durchgeführt, so werden hängt die nachfolgende Aktion davon ab, ob ein anderes Objekt definiert und verfügbar ist um die Anforderung entgegenzunehmen:
LPARAMETERS tcContext, tnPrice
LOCAL lnTax
WITH This
* Können wir die Anforderung verarbeiten?
lcCanHandle = CHRTRAN( .cCanHandle, ['], [] )
IF tcContext == lcCanHandle
* Ja, also die Standardmethode CalcTax aufrufen und den Preis übergeben
lnTax = .CalcTax( tnPrice )
ELSE
* Nein, können wir nicht. Haben wir ein definiertes Folgeobjekt?
IF !EMPTY( .cNextObj ) AND !EMPTY( .cNextObjLib )
* Ja, haben wir. Aber existiert es schon?
IF VARTYPE( This.oNext ) # [O]
* Objekt erzeugen
.oNext = NEWOBJECT( .cNextObj, .cNextObjLib )
ENDIF
* Objekt aufrufen
lnTax = .oNext.ProcessRequest( tcContext, tnPrice )
ELSE
* Kein Folgeobjekt definiert, also NULL zurückliefern
lnTax = NULL
ENDIF
ENDIF
RETURN lnTax
ENDWITH
Mehr Code wird nicht benötigt und die individuellen Unterklassen für dieses sehr einfache Beispiel benötigen letztlich überhaupt keinen individuellen Code, da alles über die Einstellungen der Eigenschaften gesteuert wird (inklusive der Eigenschaft nTaxRate welche in der original Basisklasse definiert ist).
Wann sollten wir eine Kette anstelle einer Strategie benutzten?
An diesem Punkt könnten Sie vielleicht denken, „Warum soll ich mir darüber Gedanken machen?“ – insbesondere da wir bereits eine perfekte, einfache datengetriebene Lösung für das Problem im Strategiemuster gesehen haben. Nun, die Beschränkung des Strategiemusters ist, dass nur EINE Unterklasse existieren kann und deswegen ist es nicht sinnvoll, wenn wir mehrere Operationen benötigen. Das hier gezeigte Beispiel benutzte eine Ein-Schuss-Kette! Sobald ein Objekt die Anforderung bearbeiten kann wird ein Ergebnis zurückgeliefert und es wird kein weiteres Objekt mehr benötigt.
Die Zuständigkeitskette ergibt sich immer dann, wenn unter Umständen mehrere Operationen notwendig sind. Eine Erweiterung unseres einfachen Steuerproblems könnte die Verarbeitung weiterer Aufgaben wie ‚Versandkosten und Bearbeitungsgebühren‘, ‚Rabatte‘ oder sogar mehrere Steuersätze (bspw. örtliche Preisaufschläge) sein.
In solchen Situationen reicht eine Strategie nicht aus. Die Zuständigkeitskette verarbeitet sie jedoch problemlos. Alles was benötigt wird ist, anstatt beim ersten Objekt, das die Anforderung bearbeiten kann, zu stoppen, die Anforderung an jedes weitere Objekt in der Kette eindeutig weiterzureichen und jedem die Möglichkeit zu geben, an der endgültigen Lösung zu partizipieren. Natürlich kann es sein, dass wir bei einem solchen Szenario mehr als einen Rückgabewert benötigen, aber Parameterobjekte stellen eine einfache und zuverlässige Möglichkeit dar, dies zu handhaben.
Und nun hätten wir in unserer Kette Objekte die auf der Berechnung der „Mehrwertsteuersätze“, der „Versandkosten“ und der „Rabatte“ basieren. Alles was sie benötigen sind die relevanten PEMs (Eigenschaften –Properties, Ereignisse –Events, Methoden –Methods) um innerhalb der Kette aktiv zu werden.
Zuständigkeitskette Zusammenfassung
Die Zuständigkeitskette offeriert uns einen anderen Weg zur Lösung des Problems, Funktionalitäten bereitzustellen ohne diese explizit ausprogrammieren zu müssen. Der größte Vorteil der Zuständigkeitskette liegt vielleicht in der einfachen Erweiterbarkeit. Der Hauptnachteil ist hingegen das dramatische Ansteigen der aktiven Objekte im System. Wie immer, zum Abschluss, möchte ich daran erinnern, dass auch wenn die jeweiligen Implementierungsdetails variieren können, sich das Muster nicht verändern wird.
Quellennachweis:
Entwurfsmuster - Elemente wiederverwendbarer objektorientierter Software ADDISON-WESLEY ISBN 3-89319-950-0
Keine Kommentare:
Kommentar veröffentlichen