Heute denken wir uns einmal eine Strategie aus, wie wir Problemstellungen in unseren Anwendungen schneller, flexibler und einfacher lösen können. Dieser Artikel beschreibt die exemplarische Implementierung des Entwurfsmuster Strategie (Strategy) in Visual FoxPro.
Schauen wir uns zunächst einmal wieder die Definition aus der Wikipedia an:
Strategie-Objekte werden ähnlich wie Klassenbibliotheken verwendet. Im Gegensatz dazu handelt es sich jedoch nicht um externe Programmteile, die als ein Toolkit genutzt werden können, sondern um integrale Bestandteile des eigentlichen Programms, die deshalb als eigene Objekte definiert wurden, damit sie durch andere Algorithmen ausgetauscht werden können.
Meistens wird eine Strategie durch Klassen umgesetzt, die eine bestimmte Schnittstelle implementieren. In Sprachen wie Smalltalk, in denen auch der Programmcode selbst in Objekten abgelegt werden kann, kann eine Strategie aber auch durch solche Code-Objekte realisiert werden.
Die Verwendung von Strategien bieten sich an, wenn
- viele verwandte Klassen sich nur in ihrem Verhalten unterscheiden,
- unterschiedliche, austauschbare Varianten eines Algorithmus benötigt werden,
- Daten innerhalb eines Algorithmus vor Klienten verborgen werden sollen oder
- verschiedene Verhaltensweisen innerhalb einer Klasse fest integriert (meist über Mehrfachverzweigungen) sind und
- die verwendeten Algorithmen wiederverwendet werden sollen oder
- die Klasse flexibler gestaltet werden soll.
Hat jemand was verstanden? Ich hab's auf den ersten Blick ehrlich gesagt nicht kapiert und mir daher mal verschiedene Gedanken über den obigen Inhalt gemacht. Dabei bin ich von der Situation viele verwandte Klassen sich nur in ihrem Verhalten unterscheiden aus gestartet. Okay, also viele verwandte Klassen bedeutet zunächst, dass eine abstrakte Klasse und zig Ableitungen davon gibt, die alle über die gleiche Schnittstelle verfügen. Soweit ja noch klar. Damit können wir sicherstellen, dass die einzelnen Klassen austauschbar werden und wir erhalten Flexibilität und Wiederverwendbarkeit.
Jetzt brauchen wir noch irgendetwas, welches wir als Ansprechpartner innerhalb unserer Anwendung nutzen können. Diese Komponente trägt dann auch dafür Sorge, dass die jeweilige Instanz des gewünschten Verhaltens verwendet wird. Wir müssen uns also nicht mehr drum kümmern, sondern unterhalten uns nur mit dieser Komponente. Somit kapseln wir die einzelnen Strategien vom Klienten.
Gut, damit ergeben sich für uns folgende Erkenntnisse:
- abstrakte Klasse für die Schnittstelle (abstrakte Strategie)
- beliebige Anzahl konkreter Klassen mit dem Algorhythmus (konkrete Strategie)
- einen Ansprechpartner für unsere Aufrufe (Kontext)
- und unsere Anwendung (Klient)
**Unsere Anforderung**
Der nachfolgende Beispielcode geht davon aus, dass wir ein Exportmodul für unsere Anwendung erstellen wollen. Wir haben einen Cursor und wollen diesen in unterschiedliche Formate exportieren. Das heißt, wir haben unterschiedliche Ausgabeformate für den gleichen Input und wollen dies auf flexible Art und Weise lösen. Das Beispiel selbst ist sehr einfach gehalten und steht in Analogie zum Befehl Copy To in VFP, aber auch weiteren Exportmöglichkeiten.
**Abstrakte Strategie**
Als erstes erstellen wir uns unsere Klassendefinition für alle Exportvarianten:
[code]*//- Abtrakte Basisklasse für alle Exporttypen (abstrakte Strategie)
Define Class AbstraktExport As Custom && Abstract Strategy
cDateierweiterung = ""
cVerzeichnis = ""
cDatei = ""
cBeschriftung = ""
Function Schreiben() As Boolean
EndFunc
EndDefine[/code]
Ganz einfach gehalten. Es gibt ein Informationen zur Ausgabedatei und die eigentliche Methode zum Erzeugen der Ausgabe.
**Konkrete Strategie(n)**
Wir leiten uns hier für jeden Exporttypen, den wir anbieten wollen, eine eigene Klasse von unserer abstrakten Strategie ab und implementieren den Code in der Methode schreiben. Wir wollen folgende Formate anbieten: CSV, XML und Excel. Dazu definieren wir uns drei Klassen:
[code]*--- Export als kommaseparierte Liste (CSV)
Define Class ExportCsv As AbstraktExport && (konkrete Strategie)
cDateierweiterung = ".txt"
cBeschriftung = "Export als kommaseparierte Liste"
Function Schreiben() As Boolean
Local llOk, lcAlias
llOk = .T.
lcAlias = Alias()
Select(m.lcAlias)
Try
Copy To (This.cDatei + This.cDateierweiterung) Type Csv
Catch
llOk = .F.
EndTry
Return m.llOk
EndFunc
EndDefine
*--- Export nach XML
Define Class ExportXml As AbstraktExport
cDateierweiterung = ".xml"
cBeschriftung = "XML-Datei (*.xml)"
Function Schreiben() As Boolean
Local llOk, lcAlias
llOk = .T.
lcAlias = Alias()
Select(m.lcAlias)
Try
CursorToXML(m.lcAlias, ;
This.cDatei + This.cDateierweiterung, ;
1, 4+8+48+512, 0, "1")
Catch
llOk = .F.
EndTry
Return m.llOk
EndFunc
EndDefine
*--- Export nach Excel (einfach)
Define Class ExportExcel As AbstraktExport && (konkrete Strategie)
cDateierweiterung = ".xls"
cBeschriftung = "Export nach Excel"
Function Schreiben() As Boolean
Local llOk, lcAlias
llOk = .T.
lcAlias = Alias()
Select(m.lcAlias)
Try
Copy To (This.cDatei + This.cDateierweiterung) Type Xl5
Catch
llOk = .F.
EndTry
Return m.llOk
EndFunc
EndDefine
[/code]
Insgesamt gewohnter Anblick und vertraute VFP-Befehle. Dennoch sehen wir, dass der Export nach Excel und CSV über Copy To erfolgt, während die Ausgabe nach XML über CursorToXML() realisiert wird. Und hier zeichen sich bereits die ersten Vorteile unserer Strategie ab. Egal, welches Ausgabeformat wir haben wollen, würden wir aktuell immer nur die Methode Schreiben() ausführen.
**Die Kapselung (oder der Kontext)**
Gut, zum gegenwärtigen Zeitpunkt haben wir nur ein paar lose Klassendefinitionen rumfliegen. Natürlich wollen wir in unserer Anwendung nur einen zentralen Punkt haben, über den wir den Export veranlassen können. Weiterhin wollen wir ebenfalls nicht, dass an beliebigen Stellen einfach mal zig Instanzen unserer konkreten Strategien produziert werden. Daher erstellen wir uns eine weitere Klassen, die zur Verwaltung unserer unterschiedlichen Ausgabeformate genutzt wird und als Kommunikationspartner mit unserer Anwendung zur Verfügung steht.
[code]*--- Verwaltungsklasse unserer Ausgabeformate
Define Class ExportHandler As Custom && (Kontext)
Protected oExportTyp
oExportTyp = .Null.
Function Init() As Boolean
*--- Initialen Exporttyp laden (Default)
This.oExportTyp = CreateObject("ExportXml")
EndFunc
Function Destroy() As Boolean
This.oExportTyp = .Null.
EndFunc
Function SetzeExportTyp(tcExportTyp As String) As Boolean
This.oExportTyp = .Null.
Try
This.oExportTyp = CreateObject("Export" + m.tcExportTyp)
Catch
This.oExportTyp = .Null.
Finally
Return (Vartype(This.oExportTyp) == "O")
EndTry
EndFunc
Function GebeExportTyp() AS String
Return This.oExportTyp.cBeschriftung
EndFunc
Function Schreiben(tcDateiname As String) As Boolean
This.oExportTyp.cDatei = Juststem(tcDateiname)
This.oExportTyp.cVerzeichnis = Evl(JustPath(tcDateiname),FullPath(""))
Return This.oExportTyp.Schreiben()
EndFunc
Function ToCsv(tcDateiname As String) As Boolean
This.SetzeExportTyp("Csv")
Return This.Schreiben(m.tcDateiname)
EndFunc
Function ToXml(tcDateiname As String) As Boolean
This.SetzeExportTyp("Xml")
Return This.Schreiben(m.tcDateiname)
EndFunc
EndDefine
[/code]
Ich habe hier zwei Ansätze in der Klasse implementiert:
- Allgemeiner Ansatz, wir haben die Kontrolle über das gewünschte Ausgabeformat
- Vereinfachte Methoden (ToCvs, ToXml, etc.)
Und so langsam dürfte erkennbar sein, warum das Entwurfsmuster Strategie sehr nützlich ist. Wir müssen nun in unserer Anwendung nur noch den Kontext instanziieren und alles weitere geht dann automatisch.
[code]oExport = CreateObject("ExportHandler")
oExport.SetzeExportTyp("Excel")
? oExport.Schreiben("C:TempMeinExportExcel")
? oExport.ToXml("C:TempMeinExportXML")
[/code]
**Ausbaustufen**
Gut mit diesem Grundgerüst können wir bereits erste Ergebnisse produzieren und fleissig arbeiten. Sollten wir die Anforderung erhalten, weitere Ausgabeformate in unserer Anwendung anzubieten, dann ergeben sich uns hier maximal zwei Schritte, die wir durchführen müssen. Als erstes erstellen wir eine neue Klasse für das geforderte Ausgabeformat (notwendig) und als zweites können wir im Kontext eine neue Methode anbeiten (optional), die das neue Format nach außen anbietet. Und das war's bereits.
Die Methode im Kontext zum Einstellen des gewünschten Exporttyps kann man übrigens noch problemlos aufbohren. Wir haben in unseren Anwendungen eine Verwaltungstabelle für verschiedene Import- und Exportformate. Dabei nutzen wir innerhalb der Anwendung, so wie hier im Code Tokens und setzen eine Abfrage auf die Tabelle ab, um die Informationen zu Klassenname, Klassenbibliothek, Beschriftung, Dateierweiterung, etc. in Erfahrung zu bringen.
Das gezeigte Beispiel behandelt den Export von Informationen. Natürlich lässt sich das Konzept auch auf andere Szenarien anwenden, hier mal ein paar Möglichkeiten: Import *g*, Verschlüsselung, Konvertierung, und so weiter...
Übrigens, wenn man den Druck ebenfalls als Datenexport betrachtet, braucht man lediglich eine neue Klasse zu schreiben und anzubinden. Was potentiell für einfache Reports sehr praktisch sein kann.
[code]*--- Export als Druckdokument
Define Class ExportReport As AbstraktExport
Protected cReportDatei
cReportDatei = "Schnellausgabe.frx"
Function Schreiben() As Boolean
Local llOk, lcAlias
llOk = .T.
lcAlias = Alias()
Select(m.lcAlias)
Try
Report Form (This.cReportDatei) To Printer Prompt
Catch
llOk = .F.
EndTry
Return m.llOk
EndFunc
EndDefine
[/code]
Okay, jetzt aber genug... Ich hoffe, dass dieser Artikel das grundsätzliche Konzept des Strategie-Muster ausreichend erklärt, und dass der gezeigte Quellcode leicht verständlich sowie nachvollziehbar ist.
Bis denne, JoKi