Freitag, 11. Dezember 2009

Datumsspielereien (Teil 5) / Date gadgets (Part 5)

Heute gibt es eigentlich nur einen kleinen Nachtrag.

Im 4. Teil der Datumsspielereien drehte es sich u. a. darum, wie wir die korrekte Kalenderwoche berechnen. Der umgekehrte Weg, aus einer Kalenderwoche wieder ein Datum zu generieren fehlte allerdings.

Wenn wir ein solches Datum berechnen sollen, dann müssen wir uns auf einen von sieben Tagen der Woche einigen. Der Einfachheit halber habe ich in der folgenden Beispielfunktion den Wochenbegin (=Montag) gewählt.

Die Funktion GetMondayFromCalWeek() erwartet maximal 2 Parameter. Wird kein zweiter Parameter (=Jahr) übergeben, so erfolgt die Datumsberechnung auf dem aktuellen Kalenderjahr sowie der als Parameter 1 übergebenen Kalenderwoche.

CLEAR
?GetMondayFromCalWeek(51)        && Für 2009: 14.12.2009
?GetMondayFromCalWeek(4,2010)    &&           25.01.2010

FUNCTION GetMondayFromCalWeek as Date
LPARAMETERS vWeek as Integer, vYear as Integer
   
    LOCAL ldDate as Date
   
    * // Wenn kein Jahr übergeben wurde, dann arbeiten wir mit dem aktuellen
    m.vYear    = IIF ( VARTYPE ( m.vYear ) <> [N],YEAR ( DATE () ) ,m.vYear )
    * // der 1.1. des gewählten Jahres stellt die Basis für die Berechnung   
    ldDate    = CTOD ( [01.01.] + ALLTRIM ( STR ( m.vYear ) ) )
    * // Wenn der 01.01. in der 1. Kalenderwoche liegt, dann ist alles gut,   
    * // andernfalls müssen wir 7 Tage hinzuaddieren.                       
    ldDate    = IIF ( WEEK ( ldDate,2,2 ) <> 1,ldDate + 7,ldDate )
    * // Den Montag der Woche des Datums berechnen und die Anzahl der Wochen
    * // hinzuaddieren. Dies ergibt den Montag der übergebenen Kalenderwoche
    RETURN ldDate - ( DOW ( ldDate,2 ) - 1 ) + ( ( m.vWeek - 1 ) * 7 )

ENDFUNC

Donnerstag, 3. Dezember 2009

Druckerstatus abfragen / Query printer status

Die Anzeige des aktuellen Zustands der verfügbaren Drucker kann für so manchen Anwender von essentieller Bedeutung sein, wenn es darum geht einen Ausdruck möglichst schnell in den Händen zu halten. Andererseits kann es nur von Vorteil sein, wenn bei einer Druckausgabe bereits im Vorfeld erkennbar ist, daß ein spezieller Drucker bspw. einen Papierstau hat, oder nur wenig Papier im Einzugschacht verfügbar ist.

Die Funktion APRINTER(myPrinterArray[,1]) liefert uns in einem Array den Druckernamen, den Anschluss, den Treiber, den Kommentar und den Standort.

APRINTER(laPrinters,1)
CLEAR
DISPLAY MEMORY LIKE laPrinters

Was fehlt ist die Information, ob der oder die Drucker auch tatsächlich verfügbar sind, oder ob gerade ein Druckjob abgearbeitet wird, oder ob der Drucker überhaupt verfügbar ist. Solche Informationen können wir jedoch über den Windows Management Service recht einfach abrufen.Detaillierte Infos zur Win32_Printer Klasse (Bestandteil der Windows Management Instrumentation) gibt es auf MSDN:

http://msdn.microsoft.com/en-us/library/aa394363(VS.85).aspx

Dort finden wir u.a. auch die Aufschlüsselung der Fehlernummern auf die wir gezielt reagieren können, wenn der Druckerstatus bspw. eine 1 (=Other) oder 9 (=Error) liefert. Im folgenden ein wenig Mustercode der über den Zugriff auf die WMI auskunft gibt:

LOCAL lcComputer as String, loWMIService as Object, ;
      loInstalledPrinters as Object, lcStatus as String, ;
      lcFont as String

* // Arbeitsvariablen initialisieren und WMI Objekt erzeugen
lcComputer          = [.]
loWMIService        = GETOBJECT([winmgmts:] + [{impersonationLevel=impersonate}!\\] + lcComputer + [\root\cimv2])
loInstalledPrinters = loWMIService.ExecQuery([SELECT * FROM Win32_Printer])
lcFont              = _screen.FontName
* // VFP Screen vorbereiten            
_screen.FontName    = [Courier New]
CLEAR 
?[Name] + SPACE(40) + [Status]
?REPLICATE([-],50)

* // Druckerobjekt auslesen            
FOR EACH loPrinter IN loInstalledPrinters

    ?PADR(loPrinter.Name,44,[ ])
    lcStatus = GetPrinterStatusAsText(loPrinter)
    ?? lcStatus

NEXT
* // VFP Screen zurücksetzen        
_screen.FontName = lcFont


FUNCTION GetPrinterStatusAsText as String
LPARAMETERS oPrinter as Object

    LOCAL lcReturn as String
    lcReturn = []

    DO CASE
    CASE oPrinter.PrinterStatus = 1
        * // Other
        lcReturn = [anderes        ]
    CASE oPrinter.PrinterStatus = 2
        * // Unknown
        lcReturn = [unbekannt      ]
    CASE oPrinter.PrinterStatus = 3
        * // Idle
        lcReturn = [bereit         ]
    CASE oPrinter.PrinterStatus = 4
        * // Printing
        lcReturn = [druckt         ]
    CASE oPrinter.PrinterStatus = 5
        * // Warming Up
        lcReturn = [aufwärmen      ]
    CASE oPrinter.PrinterStatus = 6
        * // Stopped Printing
        lcReturn = [gestoppt       ]
    CASE oPrinter.PrinterStatus = 7
        * // Offline
        lcReturn = [Offline        ]
    CASE oPrinter.PrinterStatus = 8
        * // Paused
        lcReturn = [pausierend     ]
    CASE oPrinter.PrinterStatus = 9
        * // Error
        lcReturn = [Fehler         ]
    CASE oPrinter.PrinterStatus = 10
        * // Busy
        lcReturn = [beschäftigt    ]
    CASE oPrinter.PrinterStatus = 11
        * // Not Available
        lcReturn = [nicht verfügbar]
    CASE oPrinter.PrinterStatus = 12
        * // Waiting
        lcReturn = [wartend        ]
    CASE oPrinter.PrinterStatus = 13
        * // Processing
        lcReturn = [verarbeiten    ]
    CASE oPrinter.PrinterStatus = 14
        * // Initialization
        lcReturn = [initialisieren ]
    CASE oPrinter.PrinterStatus = 15
        * // Power Save
        lcReturn = [Stromsparmodus ]
    CASE oPrinter.PrinterStatus = 16
        * // Pending Deletion
        lcReturn = [löscht Druckjob]
    CASE oPrinter.PrinterStatus = 17
        * // I/O Active
        lcReturn = [E/A aktiv      ]
    CASE oPrinter.PrinterStatus = 18
        * // Manual Feed
        lcReturn = [manuelle Zufuhr]
    ENDCASE
    * // Die Liste ggf. nach Bedarf erweitern
    IF INLIST(oPrinter.PrinterStatus,1,9)
        lcReturn = lcReturn + GetDetectedErrorStateAsText(oPrinter.DetectedErrorState)
    ENDIF
    RETURN lcReturn

ENDFUNC


FUNCTION GetDetectedErrorStateAsText as String
LPARAMETERS vErrorstate as Integer

    LOCAL lcReturn as String

    DO CASE
    CASE m.vErrorState = 0
        * // Unknown
        lcReturn = [Unbekannter Fehler]
    CASE m.vErrorState = 1
        * // Other
        lcReturn = [Anderer Fehler]
    CASE m.vErrorState = 2
        * // No Error
        lcReturn = [kein Fehler]
    CASE m.vErrorState = 3
        * // Low Paper
        lcReturn = [zu wenig Papier]
    CASE m.vErrorState = 4
        * // No Paper
        lcReturn = [kein Papier]
    CASE m.vErrorState = 5
        * // Low Toner
        lcReturn = [zu wenig Toner]
    CASE m.vErrorState = 6
        * // No Toner
        lcReturn = [kein Toner]
    CASE m.vErrorState = 7
        * // Door Open
        lcReturn = [Gehäuse geöffnet]
    CASE m.vErrorState = 8
        * // Jammed
        lcReturn = [Papierstau]
    CASE m.vErrorState = 9
        * // Service Requested
        lcReturn = [Kundendienst erforderlich]
    CASE m.vErrorState = 10
        * // Output Bin Full
        lcReturn = [Ausgabeschacht ist voll]
    CASE m.vErrorState = 11
        * // Paper Problem
        lcReturn = [Papier Problem]
    CASE m.vErrorState = 12
        * // Cannot Print Page
        lcReturn = [Seite kann nicht gedruckt werden]
    CASE m.vErrorState = 13
        * // User Intervention Required
        lcReturn = [Benutzereingriff notwendig]
    CASE m.vErrorState = 14
        * // Out Of Memory
        lcReturn = [Arbeitsspeicher voll]
    CASE m.vErrorState = 15
        * // Server Unknown
        lcReturn = [unbekannter Server]
    OTHERWISE
        lcReturn = [Unbekannt]
    ENDCASE
    RETURN     [(] + ALLTRIM(STR(m.vErrorState)) + [) ] + lcReturn

ENDFUNC

Freitag, 20. November 2009

Neue extrem parametrisierbare Messagebox für VFP8/9 / New extreme parametric messagebox for VFP8/9

Anatoly Mogylevets hat unter news2news eine FLL veröffentlicht, mit der komplett parametrisierbare Messagebox() Designs ermöglicht werden. Unbedingt mal anschauen!

Hier der Link: http://www.news2news.com/vfp/?solution=3

Freitag, 24. Juli 2009

Datumsspielereien (Teil 4) / Date gadgets (Part 4)

Bereits in einigen früheren Postings (Datumsspielereien Teil 1-3) waren Datumswerte ein Thema. In diesem Posting dreht es sich jedoch nicht um Feiertage sondern um die Berechnung spezieller Tage. Insbesondere die aktuelle Woche des Monats, die Kalenderwoche sowie der Montag der Woche. Zur Berechnung des Monatswoche benötigen wir drei Funktionen: CEILING(), DOW() und DAY(). CEILING gibt die nächst höhere Ganzzahl zurück.
STORE 10.1 TO num1
STORE -10.9 TO num2
? CEILING(num1)  && zeigt 11
? CEILING(num2)  && zeigt -10
? CEILING(10.0)  && zeigt 10
? CEILING(-10.0) && zeigt -10
DOW liefert einen numerischen Tag-der-Woche Wert auf Basis eines Datums. Hierbei ist jedoch der 2. Parameter zu beachten, in dem der Erste Tag der Woche definiert wird. Da unsere Woche üblicherweise mit einem Montag beginnt, ist als Wert für den 2. Parameter zwingend eine 2 (-> Montag = Wochenbeginn) zu setzen.
? DOW(DATE(),2)    && zeigt beim 24.07.2009 den Wert 5
DAY wiederum extrahiert von einem Datumwert den Tagesblock.
? DAY(DATE())    && zeigt beim 24.07.2009 den Wert 24
FUNCTION WeekOfMonth as Integer
LPARAMETERS vDate as Date

    * // Woche des Monats bestimmen
    RETURN ( CEILING( ( DAY( m.vDate ) + DOW( ( m.vDate ) - DAY( m.vDate ) + 1, 2 )  - 1 ) / 7 ) )

ENDFUNC
Die Berechnung der Kalenderwoche gestaltet sich da schon um einiges einfacher. Die Wochenbestimmung hängt von zwei Grundeinstellungen ab, die der WEEK()-Funktion als Parameter 2 und 3 übergeben werden. - Parameter 2 definiert die Berechnung der ersten Woche des Jahres - Parameter 3 definiert den Wochenanfang und entspricht dem 2. Parameter von DOW()
FUNCTION WeekOfYear as Integer
LPARAMETERS vDate as Date

    * // Woche des Jahres bestimmen
    * // Parameter 2: Wert = 2
    * //              Die längere Hälfte der ersten Woche
    * //              (4 Tage) definiert die Jahreszugehörigkeit
    * // Parameter 3: Wert = 2
    * //              Montag gilt als der erste Tage der Woche
    RETURN ( WEEK( m.vDate, 2, 2 ) )
  
ENDFUNC
Zuletzt kommen wir zur Funktion zur Berechnung des Montags einer Woche. Auch in dieser Funktion wird mit DOW() gearbeitet, und wie bei den vorherigen Einsätzen wird der 2. Parameter wieder auf den Wert 2 gesetzt wird.
FUNCTION MondayOfWeek as Date
LPARAMETERS vDate as Date

    * // Montag der Woche des übergebenen Datums                          
    RETURN ( m.vDate - ( DOW( m.vDate, 2 ) - 1) )

ENDFUNC

Mittwoch, 15. Juli 2009

Arbeiten mit Arrays / Working with arrays

Wer mit VFP arbeitet wird irgendwann feststellen, dass nicht immer alle Informationen als Cursor zur Verfügung stehen. Viele Funktionen des Fuxes die u.a. auch auf Objekte und Verzeichnisse zugreifen, stellen ihre gesammelten Informationen in Form eines Arrays bereit.

Unter anderem gibt die Funktion ADIR() bei gezielten Verzeichnisabfragen (bspw. mit ADIR(myArray,[*],[D],1) ) grundsätzlich den aktuellen (.) sowie den übergeordneten (..) relativen Pfadnamen aus. Zu finden im Array an Position 1,x und 2,x. Sollen diese beiden Verzeichnisse in der weiteren Verarbeitung keine Berücksichtigung finden, kann es bspw. sinnvoll sein, diese beiden Arrayblöcke zu entsorgen.

Für die Arbeit mit Arrays stellt uns VFP diverse Funktionen und Befehle zur Verfügung:

DECLARE DIMENSION LOCAL PUBLIC
Deklarieren oder Redimensioneren eine Variable als 1- oder 2-dimensionalen Array

AINS()
Einfügen weiterer Arrayblöcke

ACOPY()
Kopieren von Arrayblöcken an eine andere Position

ADEL()
Löschen von Arrayblöcken

ALEN()
Abprüfen der Dimensionierung eines Arrays

Zunächst deklarieren wir uns einen leeren Array namens laArrayDemo der nur eine einzelne Zelle beinhaltet:

DECLARE laArrayDemo(1)

Standardmäßig enthält die Zelle 1 den boolschen Wert FALSE. Die Ergänzung der Deklaration um einen Datentyp zeigt nur Auswirkung im Bereich von COM-Objekten. Zudem besteht keine Verpflichtung, jeder Zelle Werte identischer Datentypen zuzuordnen. Letztlich ist ein Array nur einen Art Collection.

Wollen wir den o.a. Array um einige Zellen erweitern, so geschieht dies durch eine erneute Deklaration des Arrays mit der entsprechend erhöhten (oder auch reduzierten) Anzahl an Zellen.

DECLARE laArrayDemo(5)

Wissen wir nicht genau wie die aktuelle Dimensionierung aussieht und wollen nur sicherstellen, dass die bestehende um x Zellen erweitert wird, dann müssen wir die Funktion ALEN() zu Hilfe nehmen.

Bei unserem ein-dimensionalen Array stellen wir die Anzahl der Zellen fest und erhöhen das gelieferte Ergebnis um den gewünschten Wert.

DECLARE laArrayDemo(ALEN(laArrayDemo,1) + 4)

Etwas 'leserlicher' sähe das so aus:

* // Arbeitsvariablen definieren
LOCAL liDim1 as Integer, liZuwachs as Integer
* // aktuelle Dimensionierung feststellen
liDim1 = ALEN(laArrayDemo,1)
* // Anzahl der neuen Zellen definieren
liZuwachs = 4
* // Redimensionierung durchführen
DECLARE laArrayDemo(liDim1 + liZuwachs)
* // Arbeitsvariablen freigeben
RELEASE liDim1, liZuwachs

Sind nun alle fünf Positionen mit Werten gefüllt ...

laArrayDemo(1) = [Wert in Zelle 1]
laArrayDemo(2) = [Wert in Zelle 2]
laArrayDemo(3) = [Wert in Zelle 3]
laArrayDemo(4) = [Wert in Zelle 4]
laArrayDemo(5) = [Wert in Zelle 5]

... und wir benötigen an Stelle 2 einen zusätzlichen Eintrag (weitere Zelle), dann nehmen wir die Funktion AINS() zu Hilfe. Wichtig bei dieser Funktion ist, dass sie keine Redimensionierung des Array vornimmt. Die verschiebt ab der definierten Position alle Einträge um eine Position nach hinten. Hierbei fällt u.U. die letzte Zelle raus.

AINS(laArrayDemo,2)

Wichtig ist also, dass wir zuvor eine Redimensionierung durchführen müssen, damit der Inhalt von Zelle 5 nicht verloren geht. Korrekterweise müsste der Code folglich so lauten:

* // Redimensionierung +1 durchführen
DECLARE laArrayDemo(ALEN(laArrayDemo,1) + 1)
* // ab (inklusive) Zelle 2 alle Zellen um
* // eine Position nach hinten verschieben
AINS(laArrayDemo,2)

Wollen wir den Inhalt der Zellen 1 und 2 ans Ende des Arrays verschieben und die Zellen 3-5 auf die Positionen 1-3, dann könnte dies bspw. wie folgt durchgeführt werden:

* / Deklarieren der Arbeitsvariablen
LOCAL liAnzahl as Integer, liQuellPosition as Integer, liZielPosition as Integer
liAnzahl        = 2
liQuellPosition = 1
liZielPosition  = ALEN(laArrayDemo,1) + 1
* // Redimensionierung des Array um die
* // Anzahl der zu verschiebenden Zellen
DECLARE laArrayDemo(ALEN(laArrayDemo,1) + liAnzahl)
* // Kopieren der Zelleninhalte in die
* // neuen angehängten leeren Zellen
ACOPY(laArrayDemo,laArrayDemo,liQuellPosition,liAnzahl,liZielPosition)
* // Löschen der nun nicht mehr benötigten
* // ersten beiden Zellen des Arrays
ADEL(laArrayDemo,1)
ADEL(laArrayDemo,1)
* // Redimensionieren des Arrays auf die
* // ursprüngliche Größe vor dem Kopieren
DECLARE gaArryDemo(ALEN(laArrayDemo,1) - liAnzahl)
* // Freigeben der Arbeitsvariablen
RELEASE liAnzahl, liQuellPosition, liZielPosition

Eine einfachere Vorgehensweise wäre der Einsatz eines 'on the fly' generierten temporären Arrays, wie es von ACOPY() automatisch erzeugt werden kann:

* // Deklarieren der Arbeitsvariablen
LOCAL liAnzahl as Integer, liQuellPos as Integer, liZielPos as Integer
liAnzahl    = 2
liQuellPos    = 1
liZielPos    = (ALEN(laArrayDemo,1) + 1) - liAnzahl
* // Kopieren der Zellen in den temporären Array
* // Der neue Array wird automatisch erzeugt und
* // verfügt über die selbe Struktur wie der
* // Quellarray. Allerdings sind nur die kopierten
* // Zellen gefüllt. Alle anderen stehen auf .F.
ACOPY(laArrayDemo,laArrayTemp,liQuellPos,liAnzahl,liQuellPos)
* // Nun werden die nachfolgenden Zellen
* // nach oben kopiert
ACOPY(laArrayDemo,laArrayDemo,liQuellPos + liAnzahl,liZielPos - liAnzahl,liQuellPos)
* // Jetzt kommen die Zellen des TEMP-Arrays
* // an ihre neue Position im Basis Array
ACOPY(laArrayTemp,laArrayDemo,liQuellPos,liAnzahl,liZielPos)
* // Arbeitsvariablen freigeben und hierbei
* // den temporären Array nicht vergessen!!
RELEASE liAnzahl, liQuellPos, liZielPos, laArrayTemp

Dieser Code lässt sich als Funktion wie folgt umsetzen:

AMOVE(@pSourceArrayName, nErstesQuellElement, nAnzahlElemente, nErstesZielElement)

Wichtig hierbei ist, dass der Array als Referenz übergeben wird. Hierdurch sparen wir es uns, diesen als Rückgabewert zu liefern und die Funktion kann direkt mit den 'echten' Werten arbeiten. Aufruf:

AMOVE(@laArrayDemo,1,2,4)

Funktion:

FUNCTION AMOVE
LPARAMETERS paArrayRef, piQuellPos as Integer, piAnz as Integer, piZielPos as Integer
  LOCAL llReturn as Boolean
  llReturn = .T.
  TRY
    * // Funktion für 1-dimensionale Arrays
    ACOPY( pbArrayRef  , laArrayTemp , piQuellPos , piAnz , piQuellPos )
    ACOPY( paArrayRef  , paArrayRef  , piQuellPos + piAnz , piZielPos - piAnz,piQuellPos )
    ACOPY( laArrayTemp , paArrayRef  , piQuellPos , piAnz , piZielPos )
    RELEASE laArrayTemp
  CATCH
    llReturn = .F.
  ENDTRY
  RETURN llReturn
ENDFUNC

Nachteil dieser Funktion ist, dass nur eindimensionale Arrays verarbeitet werden können. Also ergänzen wir diese Funktion zum Einen um das automatische Erkennen der Dimensionalität sowie einer Trennung in eine 1- bzw. 2-Dimensionale Verarbeitung. Anschliessend sieht die Funktion wie folgt aus:

FUNCTION AMOVE
LPARAMETERS paArrayRef, piQuellPos as Integer, piAnz as Integer, piZielPos as Integer
  * // Parameterwerte:                                               
  * // paArrayRef    = Referenz auf Array                               
  * // piQuellPos    = Startposition (Row) ab der verschoben werden soll   
  * // piAnz        = Anzahl der Rows die verschoben werden sollen       
  * // piZielPos    = Zielposition (Row) an die verschoben werden soll   
   
  LOCAL llReturn as Boolean
  llReturn = .T.

  IF ALEN(paArrayRef,2) = 0
    TRY
      * // Block für 1-dimensionale Arrays
      ACOPY( paArrayRef  , laArrayTemp , piQuellPos , piAnz , piQuellPos )
      ACOPY( paArrayRef  , paArrayRef  , piQuellPos + piAnz , piZielPos - piAnz, piQuellPos )
      ACOPY( laArrayTemp , paArrayRef  , piQuellPos , piAnz , piZielPos  )
      RELEASE laArrayTemp
    CATCH
      * // Irgendetwas ging schief
      llReturn = .F.
    ENDTRY
  ELSE
    TRY
      * // Block für 2-dimensionale Arrays
      LOCAL liColumns as Integer, liRows as Integer
      * // Zur besseren Übersicht die Array Dimensionen zwischenspeichern
      liColumns  = ALEN(paArrayRef,2)
      liRows     = ALEN(paArrayRef,1)
      * // Berechnen der tatsächlichen Offsets auf Basis der vorhandenen Columns
      piQuellPos = IIF(piQuellPos > 1, piQuellPos * liColumns - liColumns + 1, piQuellPos)
      piZielPos  = IIF(piZielPos > 1,  piZielPos  * liColumns - liColumns + 1, piZielPos )
      piAnz      = piAnz * liColumns
      * // Jetzt kann's los gehen
      ACOPY( paArrayRef  , laArrayTemp , piQuellPos , piAnz , piQuellPos )
      ACOPY( paArrayRef  , paArrayRef  , piQuellPos + piAnz , liRows - piAnz,piQuellPos )
      ACOPY( laArrayTemp , paArrayRef  , piQuellPos, piAnz ,  piZielpos )
      RELEASE laArrayTemp, liColumns, liRows
    CATCH
      * // Irgendetwas ging schief
      llReturn = .F.
    ENDTRY
  ENDIF
  RETURN llReturn
ENDFUNC

Donnerstag, 5. März 2009

Neuigkeiten über VFPx / VFPx news

In den letzten vier Wochen gab es wieder jede Menge neues im Bereich VFPx. Bereich Abgeschlossene Projekte
  • Seit dem 22. Februar steht die Version 3.01 des PEM Editors zur Verfügung.
Bereich Release Candidates
  • Seit dem 18. Februar die von Rick Schummer angekündigte korrigierte und erweiterte VFP9 SP2 Hilfedatei in der Version 1.05 bereit.
  • Seit dem 9. Februar gibt es die Version 3.5.0 der Themed Controls.
Bereich Beta Releases
  • Seit dem 28. Februar gibt es die neue Version 00.09 der FoxTabs.
In meinem Blogeintrag vom 26. Februar 2008 -VFPx auf Codeplex aktualisiert- pflege ich die Neuerungen kontinuierlich nach. Die eigentlichen Projekte können von dort direkt aufgerufen werden.
Auch wenn wir uns derzeit in der Fastenzeit befinden kann ich nur sagen: "Ran an den Speck!"

Donnerstag, 12. Februar 2009

Nostalgie als VFP-Startprogramm / Nostalgia as VFP start program

Vor ein paar Tagen fiel mir beim Durchstöbern eines alten VFP-Verzeichnisses ein kleines Programm auf, dass den C64 Bootscreen simuliert.

Nun war der alte Brotkasten nicht wirklich mein erster Computer (mein erster Commodore, ein C116, war eher ein überdimensionierter Taschenrechner dessen Abmessungen die der zum laden und speichern der Programme benötigten Datasette nur knapp überstieg und bald durch einem C128 ersetzt wurde).

Das hinderte mich jedoch nicht, das kleine prg ein klein wenig umzustellen und als Startprogramm innerhalb von VFP/Extras/Optionen/Dateiablage/Startprogramm zu hinterlegen. Wenn ich nun VFP starte werde ich wieder vom blauen Bootscreen begrüßt...

WITH _screen
    .BackColor = RGB(1,0,162)
    .ForeColor = RGB(83,81,240)
    .FontSize = 24

    CLEAR
    DECLARE Sleep IN WIN32API INTEGER
    LOCAL liWait as Integer
    liWait = 500

    Sleep(liWait)
    ? [    **** COMMODORE 64 ROM V1.1 ***]
    Sleep(liWait)
    ?
    Sleep(liWait)
    ? [ 64K RAM SYSTEM  38911 BYTES FREE]
    ?
    Sleep(liWait)
    ? [READY.]
    Sleep(liWait)
    ? [LOAD"VISUAL FOXPRO 9",8,1]
    ?
    Sleep(liWait)
    ? [SEARCHING FOR VISUAL FOXPRO 9]
    Sleep(liWait)
    ? [LOADING ]
    FOR i = 1 TO 5
        Sleep(liWait)
        ??[*]
    ENDFOR
    Sleep(liWait)

    .BackColor = RGB(255,255,255)
    .ForeColor = RGB(0,0,0)
    CLEAR
    ?? [VISUAL FOXPRO 9 ...READY AND WAITING FOR INPUT]

    .FontSize = 9
ENDWITH

Donnerstag, 5. Februar 2009

Einführung in Entwurfsmuster (Teil 5) / Introduction to design patterns (Part 5)

Im Dezember 2006/Januar 2007 schrieb Andy Kramek in seinem Blog über die Implementierung von Entwurfsmustern in Visual Foxpro. Andy hat mir erlaubt, meine deutsche Übersetzung seiner Artikelserie hier zu veröffentlichen. An dieser Stelle für seine Erlaubnis nochmals herzlichen Dank.

Heute nun Teil 5, der sich mit dem Dekorierer 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

Der Dekorierer (The Decorator)

Der Dekorierer beschreibt eine Lösung zu dem Problem, ein Objekt in seiner Funktionalität zu erweitern, ohne den eigentlichen Code des Objektes zu verändern.

Wie erkenne ich einen Dekorierer?

Die formale Definition des Dekorierers gemäß „GoF“ lautet:
Erweitere ein Objekt dynamisch um Zuständigkeiten.
Der Bedarf, Funktionalitäten (oder Verhalten) eines Objektes dynamisch zu ändern tritt in zwei Situationen auf. Erstens, wenn der Quellcode eines Objektes schlicht nicht verfügbar ist, vielleicht handelt es sich um ein ActiveX-Control oder es kommt eine Klassenbibliothek eines Drittanbieters zum Einsatz. Zweitens, wenn die Klasse intensiv genutzt wird, die spezifische Verantwortlichkeit jedoch nur in ein oder zwei bestimmten Situationen benötigt wird und es nicht angebracht wäre, den Code der Klasse einfach hinzuzufügen.

In den vorangegangenen Beiträgen haben wir verschiedene Wege zu Feststellung der Steueraufschläge in Abhängigkeit der Lokation betrachtet. Das grundsätzliche Steuerberechnungsobjekt arbeitet in jedem Fall mit einer sichtbaren Methode namens CalcTax() welche zwei Parameter erwartet (einen Wert und eine Rate), und gibt den fälligen Steuerbetrag zurück. Wir bekamen das Grundproblem bezüglich des Umgangs mit den Steuern mit Hilfe des Brückenmusters in den Griff, indem wir die Implementierung von der Schnittstelle separierten. Obwohl es flexibler war als eine einfache direkte Codierung, stellten wir jedoch sehr schnell fest, dass es nicht flexibel genug war, um mit unterschiedlichen Steuerraten in unterschiedlichen Lokationen zurechtzukommen.

Sowohl die Strategie als auch die Zuständigkeitskette kommen mit dieser Anforderung zurecht, allerdings bedeuten beide Lösungswege das Klassenableitungen eingesetzt werden müssen. Das Dekorierer-Muster erlaubt es uns, dieses Problem ohne Ableitungen der Originalklasse zu lösen.

Stattdessen definieren wir ein neues Objekt mit gleichem Interface wie der Steuerrechner, welches jedoch den notwendigen Code beinhaltet, um die passende Steuerrate der gegebenen Lokation festzustellen. Dieses Objekt sieht genauso aus wie der Steuerrechner, es kann jedoch, weil es über eine Referenz auf den echten Steuerrechner verfügt, jedwede Berechnungsanfrage vorverarbeiten und anschließend einfach die echte Implementierung aufrufen.

Was sind die Komponenten eines Dekorierer?

Ein Dekorierer stellt zwei grundsätzlich Anforderungen. Zunächst einmal benötigen wir die Implementierungsklasse, welche die Schnittstelle und die grundlegende Funktionalität definiert. Desweiteren benötigen wir einen Dekorierer der die Schnittstelle der Implementierung reproduziert und über eine Referenz auf diese verfügt. Das Klient-Objekt leitet nun Aufrufe die eigentlich direkt an die Implementierung gegangen wären, an das Dekorierer Objekt. Die Basisstruktur für das Dekorierer-Muster sieht wie folgt aus:

 
Eigentlich ist dieses Muster eine erweiterte Brücke. Soweit es den Klienten betrifft kann er den Dekorierer aufrufen, als ob es sich um die Implementierung am Ende einer Standardbrücke handelt, da beide Schnittstellen identisch sind. Soweit es die Implementierung betrifft sieht der Aufruf genauso aus, wie wenn er direkt vom Klienten gekommen wäre. Letztlich muss sie nicht einmal wissen, dass es den Dekorierer überhaupt gibt.

Wie implementiere ich einen Dekorierer?

Eine neue Klasse mit dem Namen ‚cntDekorator‘ wird definiert um das Dekorierer-Beispiel zu implementieren. Es hat zwei spezielle Eigenschaften und eine spezielle Methode wie im folgenden beschrieben:

Name          Beschreibung
cImpClass     Name der für diesen Dekorierer zu instanziierenden Klasse
cImpLib       Klassenbibliothek der zu instanziierenden Implementierungsklasse
oCalculator   Objektreferenz auf eine Instanz der real implementierten Klasse
              der CalcTax() Methode
              Dieses Objekt wird innerhalb der INIT() Methode des Dekorierers
              instanziiert.
CalcTax          Die Implementierung des Dekorierers für die äquivalente Methode
              der realen Implementierung.


Eigentlich ist die Dekorierer-Klasse sehr einfach aufgebaut. Wenn sie erzeugt wird instanziiert sie die reale Implementierungsklasse, welche über die Namens- und Bibliotheks-Eigenschaften definiert ist. Zusätzlich implementiert sie eine operationale Methode (in diesem Fall ‚CalcTax‘), welche genauso benahmt ist und über dieselbe Signatur wie die reale Methode verfügt.

Der Code in der CalcTax()-Methode des Dekorierer ist ziemlich gradlinig. Er erwartet zwei Parameter, die LokationsID als String und den Preis für den die Steuer berechnet werden soll. Beachten Sie, dass dies nicht dieselben Parameter sind, die von der CalcTax() Methode der realen Klasse benötigt werden (dort übergeben wir Preis und Steuerrate). Die CalcTax() Methode des Dekorierers stellt die passende Rate basierend auf der LokationsID fest und ruft dann die CalcTax() Methode des Implementierungsobjekts mit den realen Parameters auf. Der Rückgabewert wird unverändert einfach nur an den Klienten durchgereicht.

LPARAMETERS tcLocation, tnPrice

LOCAL lnRate, lnTax
STORE 0 TO lnRate, lnTax
*** Korrekte Steuerrate feststellen
DO CASE
CASE tcLocation = [01]
    lnRate = 5.75
CASE tcLocation = [02]
    lnRate = 5.25
CASE tcLocation = [03]
    lnRate = 0.00
OTHERWISE
    lnRate = 0
ENDCASE

*** Nun die reale Implementierung aufrufen
lnTax = This.oCalculator.CalcTax( tnPrice, lnRate )

*** Und das Ergebnis zurückgeben
RETURN lnTax

Um den Dekorierer nutzen zu können müssen wir nur eine kleine Änderung im Klienten durchführen. Anstatt die reale Implementierung zu instanziieren muss nun das Dekorierer Objekt instanziiert werden. Denken Sie daran, dass die Aufrufsignatur des Dekorierers mit der realen Methode identisch ist. Somit müssen keine weiteren Änderungen durchgeführt werden.

LPARAMETERS tcContext, tnPrice
LOCAL lnTax
WITH ThisForm
    *** Pruefen, ob der Dekorierer verfuegbar ist
    IF VARTYPE( This.oCalc ) # [O]
        *** Ist er nicht, also erzeugen wir ihn
        .oCalc = NEWOBJECT( [cntDecorator], [calclass.vcx] )
    ENDIF
    *** Nun einfach die CalcTax() Methode aufrufen und Lokation und Preis uebergeben
    lnTax = .oCalc.CalcTax( tcContext, tnPrice )
    IF ISNULL( lnTax )
        *** Die Daten konnten nicht verarbeitet werden
        MESSAGEBOX( [Unable to Process this Location], 16, [Failed] )
        lnTax = 0
    ENDIF
    RETURN lnTax
ENDWITH

Obwohl es sich hier um ein sehr einfaches Beispiel handelt, können Sie sehen, wie leicht es ist, die Funktionalität mit Hilfe eines Dekorierers zu erweitern. Eine typisches ‚real life‘ Problem, das mit Hilfe dieses Musters angegangen werden kann ist, wie man implementierungsspezifische Validierungen bei generischen Speicherroutinen unterstützt.

Dekorierer Zusammenfassung

Das Dekorierer Muster kommt zum Einsatz, wenn wir das grundsätzliche Verhalten einer spezifischen Instanz modifizieren möchten, ohne dabei entweder eine Klassenableitung zu erzeugen, oder den Originalcode zu ändern. Letztlich agiert der Dekorierer als Prä-Prozessor der Implementierung wobei er sich zwischen Klient und Implementierung schaltet.

Quellennachweis:

Entwurfsmuster - Elemente wiederverwendbarer objektorientierter Software ADDISON-WESLEY ISBN 3-89319-950-0

Freitag, 23. Januar 2009

Reparierte VFP9 SP2 Hilfedatei wird unter VFPX bereitgestellt / Fixed VFP9 SP2 help file is provided under VFPX

Gute Nachricht von Rick Schummer zur im letzten Jahr im April erschienenen defekten SP2 Hilfedatei.

Nachdem diverse (noch nicht genannte) Entwickler in den letzten Monaten die Hilfedatei durch Decompilieren, Reparieren und Neukompilieren von ihren ursprünglichen Problemen befreit haben, hat YAG von Microsoft Seite nun die Genehmigung erteilt, die Hilfedatei auf VFPX unter der Creative Common License zu veröffentlichen. Diese ermächtigt die VFP Community Verbesserungen an der Hilfedatei vorzunehmen.

Rick kündigt an, dass sobald die letzten Fehler bereinigt sind, die Datei auf VFPX bereitsteht.

Donnerstag, 15. Januar 2009

WAV-Sounds mit VFP abspielen / Playing WAV-sounds with VFP

Vor einiger Zeit hatte ich mir einen kleinen TeaTimer gebaut, damit mein schwarzer Tee nicht dauernd 10 bis 20 Minuten zieht.

Das Ganze passte auch prima, solange ich die Timer-Form im Auge behielt. Dummerweise schaue ich während der Arbeit mit Foxpro aber auf mein Codefenster und so passierte es recht häufig, dass ich doch wieder einen 20 Minuten Tee da stehen hatte.

Lange Rede kurzer Sinn, es musste etwas 'ohrenfälligeres' her, denn auch das Flackern des Timers bei abgelaufener Zeit half nicht wirklich.

Nachdem also der Entschluss zum Abspielen einer WAV-Datei gefallen war, musste ich feststellen, dass mir das Beispiel in den Solution Samples nicht auf Anhieb die Informationen preisgab, die mich interessierten. Irgendwo im Web fand ich dann ein Stück Mustercode zum Aufrufen der MCI API-DLL, welches ich als Grundlage für den u.a. Beispielcode verwendet habe. Seitdem funktionieren auch meine 3 und 4 Minuten Tees... ;-)

Die notwendige API befindet sich im Windows-System32 Verzeichnis und heisst WinMM.dll. Sie beinhaltet das Media Control Interface aus dem letztlich nur die Funktion PlaySound deklariert werden muss.

Die u.a. Funktion PlayWAV kann mit oder ohne Parameter aufgerufen werden. Wird der Name einer WAV-Datei übergeben, so wird versucht, diese abzuspielen. Andernfalls wird eine Standarddatei aus dem Windows Media Verzeichnis abgespielt. Die möglichen Werte des zweiten Parameters können dem u.a. DEFINE-Block entnommen werden.

?PlayWAV()

FUNCTION PlayWAV

    LPARAMETERS vWavFile as String, vFlags as Integer

    LOCAL llReturn as Boolean
    llReturn = .T.

    * // -----------------------------------------------------------
    * // vWavFile = (optional)                                      
    * //             Pfad und Dateiname der abzuspielen WAV-Datei  
    * //                                                          
    * // vFlag    = (optional)                                      
    * //            gezielte Abspielvariante gem. u.a. DEFINEs      
    * // -----------------------------------------------------------

    #DEFINE SND_SYNC         0x00000000    && synchron abspielen (default)                  
    #DEFINE SND_ASYNC        0x00000001    && asynchron abspielen                          
    #DEFINE SND_NODEFAULT    0x00000002    && silence (!default) wenn WAV nicht gefunden  
    #DEFINE SND_MEMORY       0x00000004    && zeigt auf eine im RAM befindliche Datei      
    #DEFINE SND_LOOP         0x00000008    && solange abspielen bis neue WAV übergeben wird
    #DEFINE SND_NOSTOP       0x00000010    && derzeit ablaufende Sounds nicht unterbrechen  
    #DEFINE SND_NOWAIT       0x00002000    && nicht warten wenn der Treiber 'busy' ist      
    #DEFINE SND_ALIAS        0x00010000    && Name ist ein registrierter ALIAS              
    #DEFINE SND_ALIAS_ID     0x00110000    && Alias ist eine vordefinierte ID              
    #DEFINE SND_FILENAME     0x00020000    && Name ist ein Dateiname                      
    #DEFINE SND_RESOURCE     0x00040004    && Name ist eine Ressource oder Atom          
    #DEFINE SND_PURGE        0x00000040    && entferne zum Abspielen nicht-statische Tasks  
    #DEFINE SND_APPLICATION  0x00000080    && Suche nach assoziierter Applikation          

    * // WinMM.dll -> MCI API-DLL -> MCI = Media Control Interface  
    TRY
        DECLARE INTEGER PlaySound IN WinMM.dll AS PLAYSOUNDOVERAPI ;
                STRING @ pszSound, INTEGER HMODULE_hmod, LONG DWORD_fdwSound

        m.vWavFile    = IIF( ;
                          VARTYPE(m.vWavFile) <> [C] OR !FILE(m.vWavFile), ;
                          ADDBS(GETENV([windir])) + [media\tada.wav], ;
                          m.vWavFile ;
                         )
        m.vFlags    = IIF( ;
                          VARTYPE(m.vFlags)=[N], ;
                          m.vFlags, ;
                          SND_ASYNC+SND_FILENAME ;
                         )
        PLAYSOUNDOVERAPI(m.vWavFile,0,m.vFlags)
    CATCH
        * // API Aufruf schlug fehl, also nix mit TADA              
        llReturn = .F.
    ENDTRY

    RETURN llReturn

ENDFUNC

Montag, 12. Januar 2009

Einführung in Entwurfsmuster (Teil4) / Introduction to design patterns (Part4)

Im Dezember 2006/Januar 2007 schrieb Andy Kramek in seinem Blog über die Implementierung von Entwurfsmustern in Visual Foxpro. Andy hat mir erlaubt, meine deutsche Übersetzung seiner Artikelserie hier zu veröffentlichen. An dieser Stelle für seine Erlaubnis nochmals herzlichen Dank.

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