Heute nun Teil 3, der sich mit dem Strategiemuster beschäftigt.
Hinweis: Die Überschriften der jeweiligen Kapitel sind direkt mit seinem Originalbeitrag verknüpft.
Einführung in Entwurfsmuster (Design Patterns)
Von Andy Kramek
Die Strategie (Strategy)
Was ist eine Strategie und wie setze ich sie ein?
Die Strategie beschreibt eine Lösung zum Hauptproblem beim Erstellen von generischem Code – Wie gehe ich mit nicht vorhergesehenen Anforderungen und Änderungen bei der Implementierung um.
Woran erkenne ich, wo ich eine Strategie benötige?
Die formelle Definition einer Strategie, gemäß „Entwurfsmuster – Elemente wieder verwendbarer objektorientierter Software“ von Gamma, Helm, Johnson und Vlissides lautet:
Definiere eine Familie von Algorithmen, kapsele jeden einzelnen und mache sie austauschbar. Das Strategiemuster ermöglicht es, den Algorithmus unabhängig von ihn nutzenden Klienten zu variieren.Beim ersten Lesen hört sich das ein klein wenig obskur an. Wir sollten uns ein einfaches Beispiel ansehen, bei dem ein Strategiemuster helfen könnte. Nehmen wir das Problem, die Mehrwertsteuer für einen Einkauf zu berechnen. Wir können davon ausgehen, dass unsere Applikation weiß, welche Gegenstände verkauft wurden und die entsprechende Menge (auf Basis der Preisinformationen) und so feststellen kann, wie hoch der Einkaufsbetrag ist. Nun müssen wir die Mehrwertsteuer ausweisen bzw. berechnen.
Eigentlich gibt es da kein Problem, solange wir in einer Region leben, die nur einen Mehrwertsteuersatz kennt. Worauf zu achten ist: Wird der Gegenstand überhaupt versteuert oder nicht, und wenn ja wie ist sein augenblicklicher Mehrwertsteuersatz. Allerdings sind in den USA Mehrwertsteuersätze länder- oder sogar ortsspezifisch. Der Prozentsatz hängt davon ab, wo ich etwas kaufe (oder sogar, in einigen Fällen, wo der Sitz des Verkäufers liegt).
Beispielsweise beläuft sich in unserem Staat die Mehrwertsteuer für Kleidung auf 5.75%, aber wenn wie 20 Meilen nach Süden fahren beträgt der Steuersatz nur 5.25% und wenn wir in den nächsten Staat fahren (nur 50 Meilen entfernt) dann gibt es dort gar keine Mehrwertsteuer auf Kleidung. Ein und derselbe Gegenstand der mit $29.95 ausgeschildert ist kann uns deswegen $31.67, $31.52 oder $29.95 kosten, je nach dem, wo wir ihn kaufen.
Um das ganze etwas abstrakter zu Beschreiben, die ausgewiesene Mehrwertsteuer der Transaktion hängt vom Kontext ab, in dem die Transaktion stattfindet. Würden wir nun den entsprechenden Code in der Applikation hinterlegen könnte das wie folgt aussehen:
DO CASE
CASE lcLocale = [Akron]
IF lcItemType = [Kleidung]
lnTaxRate = 5.75
ELSE
* Weitere Einträge hier
ENDIF
CASE lcLocale = [Canton]
IF lcItemType = [Kleidung]
lnTaxRate = 5.25
ELSE
* Weitere Einträge hier
ENDIF
CASE lcLocale = [Grove City]
IF lcItemType = [Kleidung]
lnTaxRate = 0.00
ELSE
* Weitere Einträge hier
ENDIF
OTHERWISE
* Standardwert zuweisen
lnTaxRate = 5.50
ENDCASE
Wir können sofort erkennen, was das Problem sein wird!
Was passiert, wenn wir Cleveland zu unserer Ortsliste hinzufügen, oder wenn Akron beschließt, seine Steuer zu senken, um Canton Verkaufsanteile streitig zu machen. Wir müssen unseren Code mit allen damit verbundenen Risiken ändern. Je mehr Orte wir der Liste hinzufügen umso unübersichtlicher wird der Code.
Natürlich könnten wir die Informationen innerhalb einer Tabelle hinterlegen (vielleicht eine Spalte für Orte, eine Spalte für jeden Artikel) und lesen diese Daten jedes Mal nach wenn wir sie benötigen. Auf diese Weise müssten wir nur noch einzelne Datensätze anpassen wenn sich Werte ändern, allerdings könnte das ziemlich schnell eine ganz schön große Tabelle werden und es gäbe jede Menge Duplikate und Redundanzen innerhalb dieser Datenmenge. Also auch dies wäre nicht die optimale Lösung.
Was wir doch eigentlich in unserer Applikation tun wollen ist, einfach den Artikelpreis und Kontextinformationen zu übergeben, und zwar an etwas, das uns einfach nur meldet wie hoch der Mehrwertsteuerbetrag ist. Mit anderen Worten wollen wir die Abstraktion (berechne die fällige Mehrwertsteuer) von der Implementierung (basierend auf dem Ort) trennen. Solange wir nur den Code verschieben, wird nur das Problem verlagert aber nicht gelöst.
Eine einfache Brücke ist nicht die Lösung, denn sie ermöglich nur eine einzelne Implementierung und wir benötigen multiple Implementierungen. Was wir brauchen ist die Möglichkeit zur Laufzeit zu entscheiden, welche Implementierung zum Einsatz kommen soll. Das Strategiemuster bietet uns die Möglichkeit, Klassen für jede Situation zu definieren mit der wir konfrontiert werden und dann die passende zur Laufzeit zu instanzieren.
Was sind die Komponenten einer Strategie?
Eine Strategie besteht aus drei essentiellen Komponenten. Die „abstrakte Strategie“ definiert das Interface (Schnittstelle) sowie generelle Funktionalitäten. Die „konkreten Strategien“ sind die Unterklassen welche die verschiedenen möglichen Implementierungen definieren. Die dritte Komponente, der „Kontext“, ist verantwortlich für die Referenzierung auf die aktuelle Implementierung. Die Strategie wird über eine Anfrage zur Aktion durch den Klienten initiiert.
Wir können uns eine Strategie als „dynamische Brücke“ vorstellen, in welcher das eine Ende (der Kontext) statisch ist, das andere (die Strategie) jedoch aus einer Auswahl von möglichen Varianten eine Implementierung auswählt, die für den Augenblick passend ist. Die Essenz des Musters ist, das die Entscheidung, welche Unterklasse instanziert werden soll, zu jeder Zeit davon abhängt, welche Informationen vom Klienten (bspw. der Applikation) benötigt werden.
Typischerweise weist das Muster die Verantwortung zur Instanzierung der konkreten Strategie dem Klienten zu, welcher dann eine Referenz an den Kontext übergibt. Um nun ein Strategiemuster zu implementieren müssen Sie eine abstrakte Strategieklasse definieren, und so viele unterschiedliche spezielle Unterklassen wie Sie benötigen. Das Objekt, welches üblicherweise die Rolle des Kontext übernimmt (in VFP ist dies üblicherweise die Form oder der Parent Container) muss dem potentiellen Klient ein passendes Interface bereitstellen. Ebenso benötigt es eine Eigenschaft die genutzt wird, um die Referenz auf das aktuelle Implementierungsobjekt aufzunehmen.
Wie implementiere ich eine Strategie?
Das folgende Beispiel zeigt auf, wie wir ein Strategiemuster als Lösung für das zuvor aufgeführte Mehrwertsteuerproblem einsetzen könnten.
Das Erste was wir tun müssen ist, dass wir eine abstrakte Klasse definieren, die über eine Eigenschaft verfügt, in der die zu verarbeitende Steuerrate hinterlegt wird. Sie verfügt zudem über eine Methode zum Berechnen der Steuer auf Basis des übergebenen Betrages und der Steuerrateneigenschaft. Nun erzeugen wir so viele Unterklassen dieser Definition wie wir Steuerratenvarianten haben – jede Variante eine Unterklasse.
Jetzt müssen wir noch herausfinden, wie wir feststellen können, wann welche der diversen Unterklassen zum Einsatz kommen soll. Um dies zu können müssen wir die verschiedenen Lokationen und verfügbaren Steuerraten mit der passenden Unterklasse in Relation setzen.
Natürlich gibt es viele Möglichkeiten dies zu tun und die einfachste wäre, einfach eine Liste der Lokationen (bspw. Städte) und die dort geltende Steuerrate zu erstellen. Da wir eine Unterklasse für jede Steuerrate haben können wir diese auf Basis der zur Lokation gehörenden Steuerrate herausfinden. Da wir jedoch immer noch eine Tabelle mit einem Datensatz je Stadt benötigen, können wir den Aufwand dadurch reduzieren, dass wir unsere Unterklasse analog zur Steuerrate benamen. Dies würde in etwa wie folgt aussehen:
Stadt Steuerrate Unterklasse
Akron 5,75 Tax575
Canton 5,25 Tax525
Grove City 0,00 Tax000
Kent 5,75 Tax575
Mit anderen Worten nutzen wir die zugehörige Steuerrate zur Identifikation der Unterklasse. Dies hat zur Konsequenz, dass wir die dritte Spalte der Tabelle nicht mehr benötigen. Wir können die Unterklasse direkt auf Basis der Steuerrate benennen.
Wie codieren wir dies nun in einem Formular? Nun, das ist ziemlich einfach. Alles was wir brauchen ist eine DropDownListe mit den Lokationen aus unserer Stadt/Steuerraten-Tabellen. Wenn die Stadt ausgewählt wurde, erhalten wir die zugehörige Steuerrate. Hierüber wiederum generieren wir den Namen der zu instanzierenden Unterklasse, und da alle Unterklassen aus einer zentralen Basisklasse abgeleitet sind, wissen wir, dass alles was wir nur die passende Methode im Objekt mit den richtigen Parametern - in diesem Fall zwei Parameter – den Kontextschlüssel (in diesem Fall die zugeordnete Steuerrate) und den Preis zu dem die Steuern berechnet werden sollen. Der Kontextschlüssel wird genutzt um den Namen der benötigten Unterklasse zu generieren. Wenn die aktuelle Klasse nicht die richtige ist, instanzieren wir einfach die korrekte Klasse.
Hier nun der hinterlegte Code:
LPARAMETERS tcContext, tnPrice
WITH This
* Definiere den Namen des Strategieobjektes basierend auf der Basisklasse und Steuerrate
lcStrategy = [CNTSTRAT] + PADR( tcContext, 3, [0] )
* Gibt es die Klasse schon?
IF ISNULL( .oStrategy ) OR NOT UPPER( .oStrategy.Name ) == lcStrategy
* Diese Klasse muss noch erzeugt werden
.oStrategy = NEWOBJECT( lcStrategy, [Ch15.vcx] )
ENDIF
* Jetzt wird die Berechnungsmethode aufgerufen
lnTax = .oStrategy.CalcTax( tnPrice )
RETURN lnTax
ENDWITH
Dies ist nur eine Möglichkeit wie eine Strategie implementiert werden kann. Sie macht im gegebenen Beispiel durchaus Sinn, in anderen Szenarien können andere Implementierungen notwendig sein. Stellen Sie sich beispielsweise einmal die Zuweisung von Rabatten an Aufträge vor. Die Maske zur Erfassung (Der Kontext) könnte diverse unterschiedliche Rabatte innerhalb eines Auftrags benötigen:
- Mengenrabatte, zu berechnen für einen einzelnen Artikel
- Auftragswertrabatt, zu berechnen auf den kompletten Auftrag
- Spezialrabatte oder Werberabatte auf einzelne Artikel
- Kundenbezogene Rabatte auf den kompletten Auftrag
Strategiemuster Zusammenfassung
Wir haben eine mögliche Implementierung für ein Strategiemuster gesehen und ein anderes kurz angerissen. Der Vorteil einer Strategie ist, dass sie es umgeht, alternative Implementierungen innerhalb des Codes hinterlegen zu müssen. Sie erlaubt es uns, separate Objekte die über ein gemeinsames Interface verfügen zu erzeugen. Für jede Option erfolgt eine gezielte Instanzierung des benötigten Objekts. Wie bei anderen Mustern auch gilt, dass die jeweiligen Implementierungen verschieden sein können, das Muster sich jedoch nicht ändert.
Quellennachweis:
Entwurfsmuster - Elemente wiederverwendbarer objektorientierter Software ADDISON-WESLEY ISBN 3-89319-950-0
Keine Kommentare:
Kommentar veröffentlichen