Dienstag, 21. Dezember 2010

Ein JSON-Parser in ABAP

Wenn es darum geht, strukturierte Daten zwischen verschiedenen Systemen auszutauschen, bietet sich das JSON-Datenformat an. Es unterstützt Arrays, Hashs, einige elementare Datentypen für Zahlen, Strings und Wahrheitswerte und erlaubt es, Datenobjekte beliebig tief ineinander zu schachteln.

Die Notation ist einfach und Programmierern der verschiedensten Sprachen (C, C++, C#, Java, JavaScript) in dieser oder ähnlicher Form geläufig. Im Vergleich zu XML gibt es weniger Overhead zur Definition der Struktur, so dass das Wesentliche, der eigentliche Dateninhalt, besser ins Auge fällt.

Hier einige einfache Beispiele:

[ "rot", "grün", "blau" ]

ist ein Array von Strings. Der folgende Hash definiert beispielhaft einige Name/Wert-Paare:

{ 
"Alter":42,
"Beruf":"Stenotypistin",
"Raucher":true
}

Natürlich lassen sich Arrays und Hashs beliebig kombinieren, zum Beispiel zu einem Hash of Arrays (HoA):

{
"Adam" : ["Kain", "Abel", "Seth"],
"Abraham" : ["Ismael", "Isaak"],
"Isaak" : ["Jakob", "Esau"]
}

Da JSON im wesentlichen der Notation von Daten in JavaScript entspricht, wird es häufig in Webanwendungen für die Kommunikation mit dem Server verwendet. Das Parsen im JavaScript-Layer einer Webanwendung kann einfach mit der Anweisung eval erfolgen.[1]

Um die Benutzeroberfläche portabel entwickeln und ohne grossen Aufwand zwischen verschiedenen Backends wählen zu können, empfiehlt sich ein standardisiertes Datenformat wie JSON für beide Richtungen des Datentransports, für das Senden wie das Empfangen der Daten.

Da ich seit kurzem an einer Webanwendung arbeite, die Ajax-Requests mit JSON-Daten an ein SAP-System sendet und von diesem empfängt, benötigte ich im ABAP-Stack einen JSON-Parser. Da es einen solchen anscheinend im SAP-System bisher nicht gibt, habe ich ihn selbst programmiert: Die Klasse ZCL_JSON_PARSER kann eine JSON-Eingabe in ein allgemein typisiertes ABAP-Datenobjekt umwandeln, das dann im Client Code durchsucht oder auf die anwendungsspezifischen Datenstrukturen abgebildet werden kann.

Bei Mappingaufgaben wie dieser ist das testgetriebene Entwickeln besonders effizient: Das liegt daran, dass wir nicht wie sonst mit Seiteneffekten zu tun haben - es gibt praktisch keine API-Aufrufe, die so oder so ausgehen können, und keine Abhängigkeit von den Inhalten irgendwelcher Datenbanktabellen. Wir haben ein Input und ein nur von diesem abhängendes Ouput. Das ruft geradezu danach, die Funktionalitäten Schritt für Schritt als Erfüllung von Testerwartungen zu implementieren. [3]

Bevor wir aber auch nur einen syntaktisch korrekten Test hinschreiben können, muss wenigstens die Schnittstelle der zu testenden Methode festgelegt sein. In unserem Fall werden wir eine öffentliche Methode parse haben, die sicher folgendermassen aussieht:

methods parse
importing
iv_json type string
returning
value(es_data) type zut_data
raising
zcx_parse_error.

Eingabe ist sicher ein String. Ausgabe ist ein Datenobjekt, das mit ABAP-Mitteln ausgewertet werden kann und beliebig tiefe Verschachtelungen zulässt. Hierzu benötigen wir einen generischen Datentyp, der beliebige Datenobjekte fassen kann. Der Datentyp any kann leider nicht verwendet werden, da er nur eine Abstraktion aller bestehenden Datentypen darstellt und dem Compiler nicht sagen kann, wieviele Bytes für diesen Typ alloziert werden sollen. Der Typ ref to data hat dieses Problem nicht. Er ist "ein Zeiger auf irgendetwas" und benötigt eine bekannte Anzahl von Bytes: Die Länge einer Speicheradresse, üblicherweise vier Bytes.

Der obige Rückgabetyp ist im Data Dictionary definiert. In ABAP würde man ihn äquivalent wie folgt notieren:
types: begin of zut_data,
type type c length 1,
data type ref to data,
end of zut_data.

Der Typschlüssel type spezifiert den Typ des Datenobjekts, auf das der Zeiger data zeigt. Das können elementare Datentypen wie String (S), Number (N), Boolean (B) sein, aber auch zusammengesetzte Typen wie Hash (h) oder Array (a).

Man beachte, dass die so typisierte Schnittstelle schon eine wichtige Designentscheidung enthält: Das Ziel unseres Mappings wird ein Datenobjekt, nicht die Instanz einer Klasse. In der Hierarchie der ABAP-Objekte haben Datenobjekte und Instanzen von Klassen keinen gemeinsamen Oberbegriff.

Man hätte alternativ eine Klasse zur Definition des Zieltyps in ABAP verwenden können. Das hätte gewisse Vorteile gebracht, zum Beispiel hätte der Typschlüssel eliminiert werden können [2] – aber um den Preis beträchtlicher Performanceeinbussen: Der create data Befehl hat wesentlich bessere Ausführungszeiten als create object.

Für die einfachen Datentypen können wir nun schon einige Tests hinschreiben:

* --- Zahlen erkennen
method test_42.

data: ls_data type zut_data.
ls_data = go_ref->parse( '42' ).

* Parser erkennt '42' als Zahl:
assert_equals( act = ls_data-type
exp = 'N' ).

* Wert 42 wurde korrekt ermittelt
_assert_equals( act = ls_data-data
exp = 42 ).

endmethod.

* --- Einen String erkennen
method test_string.
data: ls_data type zut_data.
ls_data = go_ref->parse( '"Abc"' ).

* Typ String wird erkannt
assert_equals( act = ls_data-type
exp = 'S' ).

* String wird richtig gelesen
_assert_equals( act = ls_data-data
exp = 'Abc' ).

endmethod. "test_string

Hierbei ist _assert_equals eine Hilfsmethode, die im wesentlichen wie assert_equals funktioniert; wenn aber act ein Zeiger ist, wird er vor dem Vergleich mit exp dereferenziert.

Wenn ein leerer Input hereingereicht wird, soll auch eine leere Struktur zurückgegeben werden:
* --- Keine Eingabe -> Keine Ausgabe
method test_nothing.
data: ls_data type zut_data.

ls_data = go_ref->parse( '' ).

* Leerer String ergibt leeres Ergebnis (keine Ausnahme)
assert_initial( ls_data ).

endmethod. "test_nothing

Wie ist es nun mit zusammengesetzten Typen - hier Hashs und Arrays? Es liegt nahe, Hashs und Arrays in ABAP durch interne Tabellen mit Hash- bzw. Standardzugriff abzubilden. Für Arrays ist dies relativ einfach:
types: zut_array_tab 
type standard table of zut_data.

leistet das Gewünschte: Denn jedes Arrayelement kann ja wieder ein beliebiges Datenobjekt enthalten. Also ist der Zeilentyp des Arrays wieder zut_data.

Hashs sind Mengen von Schlüssel/Wert-Paaren. Der Schlüssel ist ein String, der Wert ist wieder ein beliebiges Datenobjekt. Ein Hash-Element sieht also folgendermassen aus:
types: begin of zut_hash_element,
key type string.
include type zut_data as value.
types: end of zut_hash_element.

Der Hash selbst ist nun im ABAP-Sinne eine Hash-Tabelle mit Zeilentyp zut_hash_element und Schlüsselspalte key:
types: zut_hash_tab type hashed table of zut_hash_element 
with unique key key.


Nun können wir auch Tests für Hashs und Arrays formulieren, mit dem einfachsten Fall beginnend:

Es soll sicher der leere Array erkannt werden (go_ref ist das Object Under Test, also die zu testende Instanz von ZCL_JSON_PARSER):

* --- Der leere Array []
method test_empty_array.

data: ls_data type zut_data.

field-symbols: <lt_array> type zut_array_tab.

ls_data = go_ref->parse( ' [ ] ' ).

* Typ array wird erkannt
assert_equals( act = ls_data-type
exp = 'a' ).

assert_bound( ls_data-data ).

assign ls_data-data->* to <lt_array>.
assert_subrc( act = sy-subrc
msg = 'Datenobjekt hat nicht den korrekten Typ' ).

* Es wurde der leere array ermittelt
assert_initial( <lt_array> ).

endmethod. "test_empty_array

Der letzte und komplizierte Testfall, das Parsen einer verschachtelten komplexen Struktur soll zugleich zeigen, wie die resultierende Struktur in ABAP ausgewertet werden kann, um die in der Anwendung gewünschten Daten zu extrahieren:
* --- Verschachtelte Struktur
method test_complex_nested.

data: ls_data type zut_data,
lv_array_length type i.

field-symbols: <lt_array> type zut_array_tab,
<lt_hash> type zut_hash_tab,
<lt_hash2> type zut_hash_tab,
<ls_line> type zut_data,
<ls_element> type zut_hash_element,
<ls_element2> type zut_hash_element.


ls_data = go_ref->parse(
' [ 1, { x:[1,2,3],y:{"c":1} }, "a" , true ] '
).

* Basistyp Array wird erkannt
assert_equals( act = ls_data-type
exp = 'a' ).

* Äusseren Array anschauen
assign ls_data-data->* to <lt_array>.

* Er hat vier Elemente
describe table <lt_array> lines lv_array_length.
assert_equals( act = lv_array_length
exp = 4 ).

* Auf zweites Element positionieren
read table <lt_array> assigning <ls_line>
index 2.
assert_equals( act = <ls_line>-type exp = 'h' ).
assign <ls_line>-data->* to <lt_hash>.

* Den Hash (das zweite Element des äusseren Arrays) anschauen
* Auf Element zum Schlüssel 'y' positionieren
read table <lt_hash> assigning <ls_element>
with table key key = 'y'.
assert_subrc( sy-subrc ).

* Das Element zum Schlüssel 'y' ist wieder ein Hash
assert_equals( act = <ls_element>-type exp = 'h' ).
assign <ls_element>-data->* to <lt_hash2>.

* Dieser innerste Hash enthält zum Schlüssel 'c' die Zahl 1:
read table <lt_hash2> assigning <ls_element2>
with table key key = 'c'.
assert_subrc( sy-subrc ).
assert_equals( act = <ls_element2>-type exp = 'N' ).
_assert_equals( act = <ls_element2>-data exp = 1 ).

endmethod.

Zum Entwurf der Klasse ZCL_JSON_PARSER noch einige Anmerkungen:

Kernstück ist die Methode get_any, die ein beliebiges JavaScript-Datenobjekt einliest. Sie wird nicht nur von der einzigen öffentlichen Methode parse für das Top-Datenobjekt aufgerufen, sondern auch an den dafür vorgesehenen Plätzen in Arrays und Hashs, was zu rekursiven Aufrufen führt.

Es gefiel mir, die Klasse vollständig von globalen Variablen freizuhalten: Sie besitzt überhaupt keine Attribute. Der Preis dafür war lediglich, alle Teilmethoden mit einem Parameter cv_pos zu versehen, der die aktuelle Position im String angibt und von den Teilmethoden bei erfolgreichem Lesen eines Ausdrucks erhöht werden kann. Die Aufrufe der Teilmethoden werden dadurch etwas unhandlicher als sie sein könnten: Jede Methode übergibt per Referenz den vollständigen zu parsenden String und die Position im String als Changing-Parameter. Dazu gibt es meist den Exportparameter ev_found, ein Flag, das angibt, ob das gesuchte Teilobjekt gefunden wurde.

Die Klasse kann ein bisschen mehr als das JSON-Format: Schlüssel von Hashes können wie in JavaScript ohne Anführungszeichen hingeschrieben werden, wenn sie einen gültigen Variablennamen darstellen. Und Strings können auch durch einfache Hochkommata begrenzt werden. Einzige Einschränkung für Strings: Unicode-Zeichen in der Notation \uXXXX sind nicht unterstützt.


[1] Wegen der theoretischen Möglichkeit von Code Injection-Angriffen wird für Anwendungen, die im Public Internet laufen, allerdings empfohlen, auch in JavaScript einen ausprogrammierten Parser zu verwenden, der wirklich nur Datenobjekte evaluiert und keine JavaScript-Anweisungen ausführt. Die bekannteren JavaScript-Frameworks wie Prototype oder jQuery enthalten einen JSON-Parser.
[2] Das ist Martin Fowlers Refactoringmuster: "Typschlüssel durch Unterklassen ersetzen".
[3] Das ist auch der Grund, warum in TDD-Einführungen häufig Mappingaufgaben als Beispiel verwendet werden, etwa ein Konverter von Ganzzahlen in römische Zahlwerte.

Nachtrag am 12.3.2013

Durch die Integration von JSON in den ABAP Kernel gibt es sowohl zum hier vorgestellten JSON-Parser als auch zum JSON-Builder ab SAP-Basisrelease 7.02 eine meist einfachere Alternative.

Im einfachsten Fall - wenn man das aus den ABAP-Daten abgeleitete "kanonische JSON-Format" akzeptieren kann - kommt man mit einem Zwei- bis Dreizeiler zur Transformation von ABAP-Daten in JSON-Daten und umgekehrt aus. Hierbei liegt die Anweisung call transformation id zugrunde. Um ein abweichendes JSON-Format herzustellen, kann man statt id eine selbstdefinierte XSLT-Transformation verwenden, wie in meinem Blog Developing a REST API in ABAP beschrieben.

Donnerstag, 25. November 2010

Keine Message-Befehle in Geschäftslogik!

Vor kurzem erhielt ich per Mail die Anfrage eines Entwicklers, wie er denn mit ABAP Units testen könne, ob eine bestimmte Fehlermeldung, die er in seiner Geschäftslogik durchläuft, gesendet oder nicht gesendet wird. Er hatte das schon auf verschiedene Weisen probiert, kam an dieser Stelle aber nicht weiter.

Wenn etwas sich als kompliziert testbar erweist, liegt es oft nicht am Unit Test oder gar, wie hier indirekt geargwöhnt wurde, am Unit Test Framework (zwischen den Zeilen lese ich "Pah! Mit ABAP Unit kann man nicht mal eine Errormeldung abfangen bzw. abfragen"), sondern meistens liegt das Problem im zu testenden Code selbst. So auch hier.

Nehmen wir an, es sei eine Plausibilitätsprüfung gefordert, dass ein vom Benutzer einzugebender Preis einen bestimmten Maximalbetrag nicht überschreiten darf. In der an die Transaktion gebundenen Klasse, die die Eingabewerte überprüft, finden wir vielleicht den folgenden Code:

if iv_input_price > gv_max_price.
* Der eingegebene Preis &1 ist zu hoch (Maximum: &2)
message e100 with iv_input_price gv_max_price.
endif.


Das funktioniert im GUI alles ganz wunderbar. Aber beim automatischen Testen des Codes ergeben sich Schwierigkeiten. Der Unit Test, der eine Überschreitung des Maximalbetrags simuliert, lässt sich nicht bis zu Ende ausführen. Wenn er auf die Fehlermeldung stösst, übergibt der Unit Test die Kontrolle zwangsläufig an den GUI zur Anzeige des Fehlers. Das bedeutet aber: Die Ausführung des Unit Tests stoppt an dieser Stelle. Beides - die Anzeige des Fehlers und die Unmöglichkeit, den Unit Test korrekt zu Ende zu führen - ist natürlich unerwünscht.

Wo liegt hier das Problem?

Auch wenn der produktive Code anscheinend ja korrekt funktioniert und das Problem nur beim Unit Test auftritt, liegt das Problem nicht beim Unit Test und schon gar nicht beim Unit Test Framework. Das Problem besteht darin, dass obiger Code die Prüflogik an die Existenz eines GUI bindet: Der message-Befehl ist eine Anweisung, die an den GUI zur Ausführung übergeben wird. Es ist nicht möglich, die Prüfung unabhängig vom GUI auszuführen. Das ist in vieler Hinsicht schädlich:

  • Der Code kann nicht im Hintergrund ausgeführt werden, z.B. in einem Job: Eine Errormeldung führt zum sofortigen Abbruch des Jobs. Wenn aber z.B. eine ganze Reihe von Belegen im Hintergrund verbucht werden sollen, ist dies nicht das erwünschte Verhalten: Dann soll normalerweise nur die Bearbeitung des aktuellen Belegs abgebrochen und mit dem nächsten Beleg fortgefahren werden.

  • Es ist nicht möglich, die Klasse mit dieser Prüfung in einer Web-Anwendung oder mit einem ganz anderen User Interface einzusetzen: Da die message-Anweisung nicht ausgeführt werden kann, kommt es zu einem Abbruch des Prozesses mit einem Kurzdump - im Web also zu einem "internen Serverfehler" (HTTP 500).

  • Es ist nicht möglich, die Logik dieser Klasse als API-Funktion anzubieten, da der Fehlerfall zu einer Situation führt, die nicht durch die Schnittstelle kontrolliert ist.



Alle Probleme der Informatik können durch Einführung einer neuen Indirektionsebene gelöst werden, lautet ein berühmtes Zitat von Butler Lampson.[1] Das ist auch in diesem Fall so. Genau um die Behandlung eines Fehlers von dessen Erkennung zu trennen, hat man die Exception erfunden. Statt eine Fehlersituation direkt im GUI zu melden, sollte das Programm eine Ausnahme auslösen, und zwar am besten gleich eine klassenbasierte. Diese Ausnahme wird in der Schnittstelle der Prüfmethode deklariert. Der Konsument dieser Methode kann dann mit einem try... catch... Block selbst entscheiden, was er im Ausnahmefall machen möchte. Nach Einführung einer solchen Ausnahme kann der Code beispielsweise folgendermassen aussehen:

if iv_input_price > gv_max_price.
raise exception type zcx_price_exceeds_bound
exporting
price = iv_input_price
bound = gv_max_price.
endif.


Der Unit Test einer solchen Logik gestaltet sich entsprechend einfach — und einfache Unit Tests sind meist ein Zeichen, dass der produktive Code ganz gut geraten ist. Hier der Test, dass die Prüfung eine Grenzwertüberschreitung erkennt und die erwartete Ausnahme auslöst:

method test_bound_detected.
try.
go_validator->check_price( gc_very_high_price ).
* Nicht OK - Ausnahme wurde nicht ausgelöst:
fail( 'Zu hoher Preis soll Ausnahme auslösen!').
catch zcx_price_exceeds_bound into lo_ex.
* OK - optional noch die Ausnahmeparameter prüfen:
assert_equals(
act = lo_ex->price
exp = gc_very_high_price
msg = 'Ausnahmeobjekt falsch parametrisiert' ).
assert_equals(
act = lo_ex->bound
exp = go_validator->gv_max_price
msg = 'Ausnahmeobjekt falsch parametrisiert' ).
endtry.
endmethod.


Auch der umgekehrte Test ist sinnvoll: Wenn ein hinreichend kleiner Preis (z.B. 1) eingegeben wird, soll keine Ausnahme ausgelöst werden:

method test_no_ex_for_price_in_range.
go_validator->check_price( 1 ).
endmethod.


Fehlt hier nicht noch etwas? Nein! Denn das Abfangen einer Ausnahme können wir getrost dem Unit Test Framework überlassen: Wenn es zu einer Ausnahme kommt, ist das ja ein Fehler im Programm. Also reagiert das Unit Test Framework bereits korrekt, indem es den Test test_no_exc_for_price_in_range in der Übersicht als fehlerhaft markieren wird. Siehe hierzu auch meinen Blog Unbehandelte Ausnahmen als Zusicherung.

Damit könnten wir den Fall als erledigt betrachten: Error Messages in der Prüflogik sollen unbedingt vermieden werden. Ende der Durchsage.

Trotzdem bleibt der unterschwellige Vorwurf bestehen, das Unit Test Framework könnte keine Error Messages abfangen (unabhängig davon, ob dies sinnvoll ist oder nicht). Die Antwort ist: Es kann - wenn wir bereit sind, eine weitere Indirektionsebene einzuführen! Indem ich ausführe, wie das geht, will ich keineswegs zur Verwendung von Error Messages raten. Es könnte aber sein, dass das Wissen um diese Möglichkeit in irgendwelchen Fällen einmal nützlich ist.

Tatsächlich bietet ABAP eine Möglichkeit, Fehlermeldungen abzufangen: Funktionsbausteine haben stets die eingebaute Ausnahme error_message. Wenn diese Ausnahme bei Aufruf eines Funktionsbausteins explizit abgefangen wird, so wird sie beim Auftreten einer Error Message innerhalb des vom Funktionsbaustein durchlaufenen oder aufgerufenen Codes ausgelöst. Man kann also nach Aufruf des Funktionsbausteins wie üblich den sy-subrc abfragen, um zu erkennen, dass eine Error Message ausgelöst wurde.

Nun gibt es für Methoden aus gutem Grund (s.o.) keinen solchen Mechanismus. Wir können jedoch einen Funktionsbaustein einführen, der dynamisch Methoden eines zur Laufzeit gegebenen Objekts mit einer zur Laufzeit gegebenen Parameterliste aufruft:

FUNCTION Z_TEST_E_MESSAGE.
*"----------------------------------------------------------------------
*"*"Lokale Schnittstelle:
*" IMPORTING
*" REFERENCE(IO_OBJECT) TYPE REF TO OBJECT
*" REFERENCE(IV_METHOD) TYPE CSEQUENCE
*" REFERENCE(IT_PARAMETERS) TYPE ABAP_PARMBIND_TAB
*"----------------------------------------------------------------------

call method io_object->(iv_method)
parameter-table it_parameters.

ENDFUNCTION.


Das folgende Beispielprogramm zeigt dann mithilfe eines Miniobjekts lcl_testee,
das auf Wunsch Error Messages erzeugt, wie das Auftreten oder Nichtauftreten einer
Error Message im Unit Test geprüft werden kann:

* ---
report z_test_e_message.

* --- How to test for error messages within unit tests

* ---
class lcl_testee definition.
public section.
methods unwise_check
importing iv_error type flag.
endclass. "lcl_testee DEFINITION

* ---
class lcl_testee implementation.
method unwise_check.
if iv_error eq 'X'.
message e001(bl)
with 'Never issue E-message in a validator'.
endif.
endmethod. "unwise_check
endclass. "lcl_testee IMPLEMENTATION

* ---
class lcl_test definition
for testing " #AU Risk_Level Harmless
inheriting from cl_aunit_assert. " #AU Duration Short

private section.
data go_testee type ref to lcl_testee.
methods:
setup,
prepare_parameters
importing iv_error type flag
exporting et_parameters type abap_parmbind_tab,
test_error for testing,
test_no_error for testing.


endclass. "lcl_test DEFINITION

*
class lcl_test implementation.
*
method setup.
create object go_testee.
endmethod. "setup
*
method test_error.
data: lt_parameters type abap_parmbind_tab,
lv_subrc type i.
call method prepare_parameters
exporting iv_error = 'X'
importing et_parameters = lt_parameters.
call function 'Z_TEST_E_MESSAGE'
exporting
io_object = go_testee
iv_method = 'UNWISE_CHECK'
it_parameters = lt_parameters
exceptions
error_message = 1.
lv_subrc = sy-subrc.
assert_not_initial(
act = lv_subrc
msg = 'Method should issue an error message' ).
endmethod. "test
*
method test_no_error.
data: lt_parameters type abap_parmbind_tab.
call method prepare_parameters
exporting iv_error = space
importing et_parameters = lt_parameters.
call function 'Z_TEST_E_MESSAGE'
exporting
io_object = go_testee
iv_method = 'UNWISE_CHECK'
it_parameters = lt_parameters
exceptions
error_message = 1.
assert_subrc( sy-subrc ).
endmethod. "test_no_error
*
method prepare_parameters.
data: ls_parameter type abap_parmbind,
lv_error type ref to flag.

clear et_parameters.

create data lv_error.
lv_error->* = iv_error.
ls_parameter-name = 'IV_ERROR'.
ls_parameter-kind = cl_abap_objectdescr=>exporting.
ls_parameter-value = lv_error.

insert ls_parameter into table et_parameters.

endmethod.

endclass. "lcl_test IMPLEMENTATION


Dieses Programm beweist, dass es durchaus möglich ist, Error Messages in Unit Tests abzufangen. Der beträchtliche Umfang dieses Testcodes für eine so einfache Sache wie das Auslösen einer Fehlermeldung ist allerdings ein untrüglicher "Code Smell" – in diesem Fall dafür, dass der message-Befehl an einem ungeeigneten Ort verwendet wird.


[1] Zitiert aus Greg Wilson, Andy Oram [Hg.]: Beautiful Code, O'Reilly, Sebastopol (CA), Juni 2007, S.279.

Dienstag, 23. November 2010

Eine objektorientierte Metasprache

In Computerzeitschriften und Büchern über modellgetriebene Softwareentwicklung wird in jüngster Zeit häufig das Framework Xtext beschrieben und gepriesen — was vor allem dem Umstand zu verdanken ist, dass es für die Integration von Programmiersprachen in die Eclipse IDE verwendet wird.[1] Xtext (bzw. dessen zugrundeliegender Parsergenerator Antlr) ist ein leicht zu erlernendes Tool, um die Grundzüge der Entwicklung domänenspezifischer Sprachen kennenzulernen — gerade deshalb eignet es sich auch gut für den Einstieg in dieses Gebiet. Es gibt aber interessante Alternativen, die man zumindest kennen sollte.

Eine interessantere Option für zur Laufzeit pflegbare Modelle, für Codegenerierung wie auch für die Entwicklung von DSLs stellen in meinen Augen die sogenannten Parsing Expression Grammars (PEG) dar, insbesondere die von Alessandro Warth entwickelte objektorientierte Metasprache OMeta. Der Grundgedanke aller PEGs ist ein verallgemeinerter Begriff des Pattern Matching, wie es z.B. in regulären Ausdrücken verwendet wird. PEGs nutzen die Erkenntnis, dass alle üblicherweise mit dem Bau von Compilern und Interpretern verbundenen Aufgaben in gewisser Weise nur Varianten eines gemeinsamen Mechanismus sind - der Mustererkennung: [2]


  • Der Tokenizer (auch Lexer oder Scanner genannt) transformiert einen Array von Zeichen (die Eingabe, den Quelltext) in eine Folge von syntaktischen Grundeinheiten, die Tokens. Klassisch verwendet man hierzu ein Werkzeug wie lex oder dessen Weiterentwicklung Flex. Im Prinzip stellt ein Lex- oder Flexfile eine Liste von regulären Ausdrücken dar, die der Reihe nach auf die Eingabe angewendet werden, bis ein passender Ausdruck gefunden wird. Jedem regulären Ausdruck ist eine "Aktion" in Form von C-Code zugeordnet, die schliesslich mit der Rückgabe eines wohldefinierten Tokens mitsamt eines etwaigen Token-Arguments enden muss. Zur Illustration, wie ein solches Flexfile notiert wird, mag mein Flex-Definitionsfile apc_lexer.lex im Projekt astropatterns dienen.
  • Der Parser operiert auf dem Array von Tokens, um eine interne Datenstruktur zu erzeugen, die für die effiziente maschinelle Abarbeitung besonders gut geeignet ist. In der Regel ist das Ergebnis des Parsings ein abstrakter Syntaxbaum (AST). Auch das Parsing ist im Grunde Mustererkennung, nur auf einer höheren Ebene: Er arbeitet mit einer anderer Art "Alphabet", das aus terminalen und nichtterminalen Symbolen besteht, und die Muster sind die Syntaxregeln, die sogenannten Produktionsregeln. Parsergeneratoren haben eine lange Geschichte, in denen yacc (yet another compiler compiler) einen ersten Meilenstein darstellte. yacc, obwohl vor über 30 Jahren entwickelt, wird bis heute für den Entwurf von Programmiersprachen verwendet. Ich habe mit Bison, dem Nachfolger von yacc, eine astrologische DSL implementiert, die hier ebenfalls der Veranschaulichung dienen soll. Die Bison-Grammatik dieser DSL zeigt als strukturelle Ähnlichkeit, dass auch hier die Eingabe mit einer Reihe sogenannter Produktionsregeln verglichen wird. Wenn eine passende Regel gefunden wird, wird die zugeordnete semantische Aktion ausgeführt.
  • Type Checker und Optimierer sind Transformationen des AST, die bestimmte Muster erkennen und durch spezifischere oder effizientere Konstruktionen ersetzen. Da das Produkt des Parsers in der Regel ein Syntaxbaum ist, verwendet man häufig das Besucher-Entwurfsmuster, um den Baum zu traversieren und dabei je nach Anwendungsfall spezifische Operationen auszuführen.[3]
  • Das Gleiche gilt schliesslich für den Code Generator, der den AST in Programmquelltext oder Binärcode für einen Prozessor oder eine virtuelle Maschine transformiert. Auch hierfür werden an das Besuchermuster angelehnte Methoden verwendet, obwohl sich auch diese Aufgabe in Form einer Grammatik mit Produktionsregeln formulieren liesse.


Die Programmiersprache OMeta bietet einen Mechanismus, um all diese Aufgaben in Form einer PEG-Grammatik zu formulieren und zu lösen. Das Konzept ist so allgemein gehalten, dass im Prinzip beliebige Transformationen von Quelltexten und Datenstrukturen in andere Zielstrukturen möglich sind. Die OMeta-Syntax kann in verschiedene Hostsprachen eingebettet werden, es gibt z.B. OMeta-Implementierungen für Squeak, COLA (der ersten verwendeten Hostsprache – einer Mischung aus Scheme und Smalltalk), C#, JavaScript, Ruby u.v.a.m. – die Zahl der Implementierungen steigt. Besonders attraktiv finde ich dabei die Kooperation mit dynamischen Hostsprachen wie JavaScript oder Python. Denn die interpretierten Sprachen sind offenbar besonders gut gerüstet für die Vision der modellgetriebenen Softwareentwicklung, zur Laufzeit in einer domänenspezifischen Sprache das Systemverhalten steuern zu können. Die Sprache JavaScript hat darüberhinaus den zusätzlichen Vorteil, dass sie als "Standardsprache der Webbrowser" ohne Extrakosten in Webanwendungen verfügbar ist. Weil ich die Sprache OMeta so wichtig finde, habe ich auf meiner Homepage eine Oberfläche zum Experimentieren unter dem Link

http://ruediger-plantiko.net/ometa/

bereitgestellt. Sie ist ähnlich der auf http://tinlizzie.org bereitgestellten Seite, trennt jedoch klarer den Grammatik-Definitionsteil von der Ausführung des daraus generierten Parsers.

OMeta ist, wie es der Name sagt, eine objektorientierte Metasprache, die viele interessante Features aufweist:

  • Die Eingabe von OMeta muss nicht als Quelltext oder überhaupt als Zeichenfolge vorliegen, sondern kann ein Array von beliebigen Objekten der Hostsprache sein. Das ermöglicht z.B. auch die Transformation von abstrakten Syntaxbäumen. Denn jeder Syntaxbaum ist i.w. als eventuell verschachtelte Folge von Arrays, Tokens und Objekten darstellbar. Das JavaScript-Statement
    document.getElementById("btnSave").click();

    könnte z.B. durch folgenden AST dargestellt werden:
    [ APPLY, 
    [ APPLY, document, "getElementById", ["btnSave" ] ],
    "click", [] ]

    Das ist, formal gesehen, ein Array, der aus Objekten besteht - nämlich wiederum aus Arrays, aus Symbolen wie APPLY (die z.B. auch als Strings realisierbar wären), aus gewöhnlichen JavaScript-Objekten der Hostsprache, wie hier dem DOM-Objekt document, und Datentypen wie Strings, Zahlen, usw.[4] In dieser Form könnte der AST wiederum einem OMeta-Programm zur Verarbeitung vorgelegt werden.

  • Objektorientierung erlaubt die Trennung von Produktionsregeln in verschiedenen Namensräumen. So ist jede Grammatik frei in ihrer Namenswahl für die verwendeten Symbole, unbeeinflusst durch eventuell bereits im selben Prozess bestehende Grammatiken. Ausnahme ist natürlich der explizit gewünschte Bezug auf eine bestehende Grammatik durch den Vererbungsmechanismus. Es ist möglich, eine OMeta-Grammatik von einer anderen erben zu lassen und sich in der Formulierung der Produktionsregeln auf die Regeln der Superklasse zu beziehen. Das erlaubt es, bestehende Parser, z.B. den notwendigerweise existierenden Parser der Hostsprache, mit wenigen Codezeilen um eigene Konstrukte zu erweitern. Dadurch ist OMeta ein ideales Werkzeug für Entwickler von Programmiersprachen, um neue syntaktische Idiome auszuprobieren.

  • Im Gegensatz zu kontextfreien Grammatiken (CFG) ist der Auswahloperator | in einer PEG nichtdeterministisch: Es ist ein priorisierter, "kurzschliessender" Operator, dessen Evaluation bei der ersten passenden Alternative abgebrochen wird. Das kann als Nachteil angesehen werden, da die "veroderten" Operanden dadurch nicht mehr gleichberechtigt sind. Andererseits ist es auch ein Vorteil, da die Interpretation eines priosierten Auswahloperators keine Mehrdeutigkeiten produziert, wodurch die PEGs einfacher nachvollziehbar werden.

  • Das letztgenannte Feature des priorisierten Auswahloperators bringt üblicherweise das Problem von Endlosschleifen für linksrekursive Regeln mit sich. Die Regel
    expr = expr "-" number | number

    zur Definition von Subtraktions-Ausdrücken kann in einer herkömmlichen PEG nicht ausgewertet werden: Um diese Regel zu erkennen, wird zuerst der linke Teil der Alternative ausprobiert; hierzu muss wiederum die Erkennung von expr ausgeführt werden usw. Das führt zu einem unendlichen Abstieg. In komplizierteren Fällen bildet sich ein geschlossener Zykel aus mehreren voneinander abhängigen Regeln, die sogenannte indirekte Linksrekursion.

    Nun lassen sich alle Regeln, die auf das Problem der Linksrekursion stossen, von Hand so umschreiben, dass die Linksrekursion nicht mehr auftritt. Aber die Ausdrücke werden dadurch umständlicher und sind nicht mehr so leicht nachvollziehen. Aus Sicht eines Grammatik-Entwicklers ist es eine unnötige Komplikation, in seiner Grammatik Linksrekursionen vermeiden zu müssen.

    Nun ist es Alessandro Warth mit OMeta gelungen, dieses Problem zu lösen. In OMeta kann man auch linksrekursive Regeln wie die obige notieren. Die Effizienz leidet darunter nicht wesentlich - die Parsezeiten hängen in der Regel weiterhin linear von der Länge des Inputs ab. Die Möglichkeit rekursiver Regeldefinitionen erhöht die Ausdruckskraft von OMeta in der Verwendung als alternative PEG-Implementierungen.

  • OMeta erlaubt die Definition sogenannter parametrisierter Regeln. Das sind, wie der Name sagt, Regeln, die von Parametern abhängen. Warth erläutert dieses Idiom anhand einer Regel für Zeichenmengen. Zeichenmengen kann man traditionell durch den Auswahloperator definieren. Wenn es die eingebaute Regel digit für die Erkennung von Ziffern in OMeta nicht schon gäbe, könnte man sie so definieren:
    digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 

    Solche Regeln sind fehleranfällig und langsam in der Ausführung. Hier helfen parametrisierte Regeln wie die folgende (die überdies zeigt, dass auch in parametrisierten Regeln sogenannte semantische Aktionen angegeben werden können, wie hier die Rückgabe des erkannten Zeichens):
    charRange :x :y = char:c ?(x <= c && c <= y) -> c

    Ist die Regel charRange einmal erklärt, kann sie in weiteren "konkreten" Regeln wiederverwendet werden:
    digit     = charRange('0','9'),
    lowerCase = charRange('a','z'),
    upperCase = charRange('A','Z')

    Das Beispiel lässt erkennen, dass die parametrisierten Regeln eine höhere Abstraktionsebene einführen. Wie Vererbung erlauben sie eine "Oben-Unten-Trennung", eine Trennung des Allgemeinen vom Konkreten.

  • Wie um dies noch zu überbieten, unterstützt OMeta sogar Regeln höherer Ordnung: Das sind Regeln, die andere Regeln als Argument haben! Dieses Feature wird möglich durch eine Kombination der parametrisierten Regeln mit einer speziellen eingebauten Regel namens apply. apply nimmt den Namen einer Regel entgegen und führt diese aus. Als sinnvolles, praktisches Beispiel gibt Warth folgende OMeta-Regel:
    listOf :p = apply(p) ( "," apply(p) )*

    Diese Regel erkennt ein oder mehrere, durch Komma getrennte Vorkommen von "etwas, das der Regel p genügt". Beispielsweise erkennt der Term
    listOf('expression')

    eine mit Kommas getrennte Liste von Ausdrücken (die einer früher definierten Regel expression genügen), während
    listOf('name')

    auf eine Liste von Namen (die der früher definierten Regel name genügen) passt.



Ich kann hier nicht die gesamte Sprache OMeta erläutern, sondern nur zu ihrem Studium anregen. Die obigen Erklärungen fassen wesentliche Teile von Warths Dissertation zusammen. Wer mehr erfahren will, sei auf http://www.tinlizzie.org/ometa/ verwiesen.

Immerhin will ich noch ein kleines, in der Szene der "Compilerbauer" beliebtes Beispiel vorführen: einen "Tischrechner". Dieser konkrete Tischrechner kann sogar mit Variablen arbeiten, was nebenbei beweist, dass man mit OMeta auch zustandsbehaftete Parser bauen kann. Auf meiner Testseite erreicht man seine Definition durch Auswahl von Calculator in der Liste der verfügbaren Grammatiken. Auch dieses Beispiel ist der Dissertation von Warth entnommen. Ich will es hier kurz diskutieren.

Mit der ersten Zeile
ometa Calc <: Parser {

wird eine neue Grammatik (eine neue Transformation, ein neuer Parser, ein neues OMeta-Objekt) deklariert. In diesem Fall soll es von der eingebauten Grammatik Parser erben. Diese eingebaute Klasse enthält aktuell nur eine einzige grundlegende Parser-Funktionalität – nämlich eine token-Regel, mit der durch Leerzeichen getrennte Strings erkannt werden können. Diese token-Regel wird in allen von Parser erbenden Grammatikdefinitionen automatisch den in Doppelhochkommata eingeschlossenen Strings unterlegt. Mehr bekommt man durch diese Vererbung nicht.

Die nächste Zeile
var           = spaces letter:x             -> x,

zeigt die typische Syntax einer Produktionsregel in OMeta. Dem Namen der Regel folgt nach einem Gleichheitszeichen ihre Definition, und nach der Definition kann, mit einem Pfeil -> abgetrennt, eine semantische Aktion angegeben werden. Eine semantische Aktion ist im Code der Hostsprache notiert und wird für ein erkanntes Muster evaluiert. Das Ergebnis der Evaluation ist auch der Rückgabewert der sogenannten matchAll()-Methode, mit der man schliesslich den Parser aufrufen kann.

In diesem Fall haben wir eine Regel var zur Erkennung von Variablennamen. Zunächst wird festgelegt, dass Leerzeichen unmittelbar vor einem Variablennamen ignoriert werden sollen. Dies erreicht man mit der eingebauten Regel spaces, die "Null oder mehr Leerzeichen" erkennt. Danach folgt die eigentliche Festlegung, was als Variable erkannt werden soll: Variablen sollen in unserem Tischrechner einbuchstabig sein. Hierzu benutzen wir die eingebaute OMeta-Basisregel letter. Nach dem Doppelpunkt folgt nun ein Variablenname, der bei erfolgreicher Anwendung der Regel das Evaluationsergebnis der semantischen Aktion enthält (oder, wenn keine semantische Aktion ist, den als passend erkannten Teil der Eingabe). Auf diese Variablen, die während des Parsens zugewiesen werden, kann man dann in der semantischen Aktion zurückgreifen – wie hier. Wir legen fest, dass der Rückgabewert der Regel genau der erkannte Buchstabe ist.

Eine einfachere, völlig äquivalente Formulierung wäre übrigens
var           = spaces letter

Aber wir wollen ja schliesslich lernen, wie OMeta funktioniert, und dafür ist die erste Variante besser geeignet.

Nach den Variablen legt die folgende Regel die erlaubten Zahlen fest - positive, ganze Zahlen:
num           = num:n digit:d        -> (n*10 + d*1)
| digit:d -> (d*1),

Hier ist neu der Gebrauch des Auswahloperators | für die Deklaration der Alternativen. Ausserdem lernen wir, dass auch Teilen einer Regel bereits eine semantische Aktion zugeordnet werden darf. Der erste Teil der Regel enthält darüberhinaus die bereits besprochene Linksrekursion.

Diese Regel hätte man äquivalent auch so schreiben können:
num           = digit+:d        -> (1*d.join('')),

Neu ist hier der Quantifier + mit derselben Semantik wie in regulären Ausdrücken. Ein mit dem Quantifier + oder * angereicherter Ausdrucks evaluiert immer zu einem Array mit den einzelnen Treffern als Elementen. Den Array können wir in der semantischen Aktion mittels der Methode join() der JavaScript-Klasse Array zu einem String zusammenspleissen und schliesslich durch Multiplikation mit 1 (type coercion) die Umwandlung in den numerischen Datentyp erzwingen. Auch diese Variante der Regel num ist sicher lehrreich.

Es folgen nun die primären Ausdrücke: Die Eingabe eines Variablennamens soll deren Inhalt zurückliefern, die Eingabe eines Zahlenstrings soll ebendiese Zahl zur Antwort haben, und komplexere Ausdrücke sollen geklammert werden können:
primaryExpr   = spaces var:x         -> self.vars[x]
| spaces num:n -> n
| "(" expr:r ")" -> r,

Neu ist hier der Zugriff auf die Variable self.vars, die in einer speziellen, bei der Parserinstanziierung aufgerufenen sogenannten initialize()-Methode als leerer Hash definiert wird und uns als Container (genauer: als Symboltabelle) für die im Tischrechner eingegebenen Variableninhalte dient.

Es kommen nun die Multiplikations- und Additionsregeln:
mulExpr       = mulExpr:x "*" primaryExpr:y -> ( x * y )
| mulExpr:x "/" primaryExpr:y -> ( x / y )
| primaryExpr,
addExpr = addExpr:x "+" mulExpr:y -> ( x + y )
| addExpr:x "-" mulExpr:y -> ( x - y )
| mulExpr,

Wir benutzen hierbei die Eigenschaft, dass der Auswahloperator priorisierend ist, um der Punktrechnung vor der Strichrechnung den Vorzug zu geben: Es werden immer zuerst die mulExpr erkannt (und evaluiert), bevor ein addExpr erkannt wird. Nicht ganz dem entsprechend, was der Name suggeriert, subsumiert die Regel addExpr insbesondere auch den mulExpr (dritte Alternation der Regel für additive Ausdrücke). Auf der rechten Seite stehen wieder die semantische Aktionen - in den Klammern kann beliebiger JavaScript-Code codiert sein.

Die addExpr ist somit allgemeiner als mulExpr und allgemeiner als die primaryExpr. Sie enthält letztere als Spezialfälle. Mit der folgenden Regel
expr          = var:x  "=" expr:r           -> (self.vars[x] = r)
| addExpr,

umfassen wir somit alle für den Tischrechner gültigen Ausdrücke. Variablenzuweisungen ebenso wie arithmetische Ausdrücke (die auch wieder Variablen enthalten dürfen).

Die letzte Regel doit legt fest, dass soviele Ausdrücke wie möglich evaluiert werden sollen, und schliesslich das Ergebnis des letzten gültigen Ausdrucks ausgegeben werden soll.
doit          = (expr:r)* spaces end        -> r


Hier folgt noch - ausserhalb der OMeta-Grammatik, die bereits erwähnte initialize()-Methode, mit der der Rechner in den Startzustand versetzt wird:
Calc.initialize = function() {  this.vars = {}; }

In meiner "Workbench" kann ich nun die Regel doit auf die Eingabe
x=1 y=2 x+y

anwenden und erhalte die Ausgabe 3. Das liegt daran, dass die doit-Regel mehrere, durch Leerraum getrennte Ausdrücke erkennt und auswertet. Ich hätte aber auch drei Eingaben nacheinander tätigen können: Zuerst
x=1

danach
y=2

und schliesslich
x+y

Auch dann hätte ich die Ausgabe 3 erhalten. Der Grund ist, dass meine "Workbench" in jedem Dialogschritt mit derselben Parserinstanz arbeitet und sich daher alle bereits erfolgten Variablenzuweisungen in der Symboltabelle vars gemerkt hat. Es ist eben ein stateful parser – und mit dieser Eigenschaft ist OMeta auch für REPL Shells verwendbar (read-evaluate-print loops).

Wer mag, kann sich auf meiner OMeta-Workbench auch meinen in OMeta implementierten XML-Parser anschauen: Ein meiner Ansicht nach durchaus lesbarer Quelltext von nur 42 Zeilen erlaubt die Transformation eines XML-Dokuments in einen in JavaScript modellierten Objektbaum. Das sind die Grössen, mit denen man in OMeta arbeiten kann; es ist auch eine der Fragestellungen, mit denen das Viewpoints Research Institute angetreten ist, unter dessen Dach auch OMeta entwickelt wurde: Um wieviele Zehnerpotenzen kann bestehender Programmquelltext bei Einsatz geeigneter, möglichst ausdrucksstarker Programmierumgebungen gekürzt werden, um dennoch das zu erreichen, wozu man heute Computer einsetzt (Textverarbeitung, Graphik, Internet usw.)?

Dies möge als ein erster Einblick in einen hochinteressanten Parsergenerator genügen, den ich zum näheren Studium nur empfehlen kann. Die Einsatzfelder, rund um den Bereich domänenspezifischer Sprachen (DSL) scheinen mir sehr vielversprechend zu sein. Die Sprache ist kompakt und ausdrucksstark. Ihren mächtigen Features, wie etwa der Objektorientierung, den parametrisierten Regeln und der Rekursion, ist es zu verdanken, dass Grammatiken wesentlich eleganter notiert werden können als in früheren Werkzeuge dieser Art.




[1] So in Thomas Stahl, Markus Völter, Sven Efftinge, Arno Haase: Modellgetriebene Software-Entwicklung, 2. Auflage, dpunkt Verlag, Heidelberg 2007.
Jan Köhnlein und Sebastian Zarnekow, Xtext-Praxis, eclipse-Magazin 1.2010, S. 50.
[2] Die folgenden Ideen sind der Dissertation von Alessandro Warth Experimenting with Programming Languages entnommen.
[3] Es geht jedoch auch ohne das Besucher-Entwurfsmuster, das leider intrusiv ist und dem in der Umsetzung eine gewisse Schwerfälligkeit anhaftet (wie ich in meinem Blog gezeigt habe).
[4] Dabei wird die Anwendung einer Methode m des Objekts o mit dem Argument-Array [args...] durch das Konstrukt [APPLY, o, m, [args...]] modelliert.

Donnerstag, 28. Oktober 2010

Eine Erfolgsgeschichte mit Simple Transformations

Simple Transformations (ST) sind eine wunderbare und effiziente Möglichkeit, ABAP-Daten in XML-Dokumente und zurück zu transformieren. Sie sind vom Konzept her bidirektional, somit ideal für die Serialisierung und Deserialisierung geeignet – und ausserdem sind sie, wie wir beobachten konnten, hocheffizient. Wenn man sich einmal in das Konzept eingearbeitet hat, sind sie darüberhinaus wirklich simple, wie es der Name verheisst, also einfach in der Implementierung.

Dank der Simple Transformations ist es uns gelungen, eine unzuverlässige Komponente unseres XI-Systems (den JDBC-Adapter) durch einen stabil laufenden, in ABAP programmierten Adapter zu ersetzen, der über seine Arbeit im Joblog genau Rechenschaft gibt und obendrein einen deutlich besseren Durchsatz hat als die bisherige Lösung.

Wir haben eine entfernte Datenbank, in die im Laufe des Tages aus verschiedenen Quellen immer wieder neue "Meldungen" eingespeist werden (Wert- und Warenflüsse). Um diese einzulesen und den Java-Mappingklassen zur weiteren Verarbeitung zuzuführen, haben wir bislang den von SAP mitgelieferten sogenannten JDBC-Adapter verwendet, der gemäss Beschreibung genau für eine solche Aufgabe geeignet wäre. Leider hat dieser Adapter (in unserem System) eine Schwachstelle, die sich trotz intensiver Bemühungen nicht beheben liess – er leidet gewissermassen unter Narkolepsie: Ganz unvermittelt stellt er immer wieder mal seine Arbeit komplett ein – ohne noch irgendein Lebenszeichen von sich zu geben, aber auch ohne dass der Prozess abbricht, geschieht mitten in einer Verarbeitung einfach nichts mehr.

Da die eingehenden Meldungen zeitkritisch sind, führte dies immer wieder zu extra Wartungsaufwand. Es mussten Überwachungsprogramme geschrieben werden, um die fristgerechte Abarbeitung der Meldungen zu garantieren. Zur Unzeit (so etwas passiert immer zur Unzeit!) wurde unser Support mit Alarm-SMS aus dem Bett geholt, weil es wieder einmal passiert war. Der manuelle Workaround, um den JDBC-Adapter wieder zum Laufen zu bekommen, bestand dann darin, einen zweiten, identisch konfigurierten Kanal zu definieren und auf diesen zu wechseln.

Der JDBC-Adapter ist für uns leider eine "Black Box". Das ist das Problematische an den von SAP ausgelieferten Java-Komponenten (nicht an der Sprache Java selbst – die Sprache Java ist grossartig). Wir können die SAP-Java-Komponenten nicht wie ein ABAP-Programm studieren, um unsere eigenen Schlussfolgerungen zu ziehen.[1] Wir können auch keine Fangschaltung einbauen, um diesen spontan auftretenden Fehler näher zu lokalisieren. Wir sind gezwungen, jedes Problem an den Support von SAP zu addressieren. SAP selbst hatte keine Antwort auf unser Problem und musste die OSS-Meldungen schliessen, da das Problem ja nicht reproduzierbar ist.

Wir haben uns daher dazu entschlossen, diesen JDBC-Adapter aus dem XI-Eingangsprozess zu eliminieren und durch eine in ABAP programmierte Komponente zu ersetzen, die letztlich den sogenannten Plain Adapter verwendet. Das ist ein vom ABAP-seitigen ICF angesprochener REST-artiger HTTP-Service (zu erreichen unter /sap/xi/adapter_plain). Im Body der HTTP-Anfrage erwartet der Plain Adapter das XML-Dokument in der Form, wie es schliesslich vom XI-Mapping verarbeitet wird. Er reicht dieses Dokument dann an die XI-Mappingschicht weiter.

Die eigenprogrammierte Komponente ist ein in regelmässigen Abständen gestarteter Job, der

  1. die Daten aus der entfernten Datenbank in interne Tabellen einliest,
  2. die ABAP-Daten in ein XML-Dokument transformiert – dies ist der Ort, an dem die Simple Transformations zum Einsatz kommen – und
  3. das XML-Dokument schliesslich über einen "internen" HTTP-Request an den Plain Adapter weiterleitet.[2]


In einem (typischen) Joblauf wurden 403 Meldungen mit insgesamt 125.254 Meldungspositionen in 123 Sekunden verarbeitet. Das bedeutet einen Durchsatz von rund 1 Millisekunde pro Position. Hierin enthalten ist das Lesen der Position von der Datenbank, die Abbildung der Meldungen in ein XML-Dokument und schliesslich die Verarbeitung im "Plain Adapter". Das sind wunderbare Laufzeiten!

Die folgende Graphik zeigt, wie sich die Laufzeit für einen typischen Joblauf auf diese Schritte verteilt:



Für das Marshalling, also die Konvertierung der ABAP-Daten in ein XML-Dokument, wurden nur 12 der 125 Sekunden benötigt. Die XML-Konvertierung, einschliesslich eines Prepare-Schritts in ABAP, in dem die Daten für die Transformation passend aufbereitet werden, schafft also 10 Detailsätze pro Millisekunde. Das ist mehr als zufriedenstellend.

Es ist sinnvoll, vor dem eigentlichen Aufruf der ST einen vorbereitenden Schritt zu implementieren, in dem die Daten für den Zugriff der ST passend aufbereitet werden. Simple Transformations sind nicht nur einfach zu verstehen, sie sollten auch einfach konzipiert werden. Denn je simpler eine Simple Transformation gerät, desto effizienter wird sie auch. Im vorliegenden Fall erzeugt die ST eine Sequenz von Elementen ("Zeilen"), die jeweils Daten aus Kopf und Position enthalten. Die passende Datenstruktur hierfür ist eine interne Tabelle mit einem tiefen Zeilentyp: Jede Zeile steht für eine Meldung, und die Komponente detail der Zeile ist selbst eine interene Tabelle, die die Positionen enthält. Bei geschachtelten Strukturen ist auf den erhöhten Speicherbedarf zu achten. Eine überschlägige Rechnung zeigte uns aber, dass der Hauptspeicher für das benötigte Datenvolumen auch für sehr grosse Meldungen noch ausreicht. Ausserdem handelt es sich ja nur um eine Hilfstabelle mit extrem kurzer Lebensdauer - sie lebt als lokales Feld "auf dem Stack" und wird genau in der Methode auf- und abgebaut, die die Simple Transformation durchführt: [3]
* Header/Detail in Tabelle mit tiefer Zeilenstruktur abbilden
loop at it_header assigning <ls_header>.

clear ls_meldung.
move-corresponding <ls_header> to ls_meldung.

loop at it_detail assigning <ls_detail>
where absender = <ls_header>-absender and
meldungs_id = <ls_header>-meldungs_id and
meldungs_datum = <ls_header>-meldungs_datum and
partition_knoten = <ls_header>-partition_knoten and
partition_tag = <ls_header>-partition_tag.
insert <ls_detail> into table ls_meldung-detail.
endloop.

insert ls_meldung into table lt_meldungen.

endloop.


Der Aufruf der Transformation ist dann sehr einfach:

* Meldungsspezifisch in XML transformieren
call transformation (gv_transformation)
source meldungen = lt_meldungen
result xml ev_xml.


Die Transformation wird dynamisch aufgerufen, wir haben für jeden Meldungstyp eine andere Simple Transformation. Die zuvor aufbereitete Meldungstabelle wird nun als source übergeben - das Resultatdokument wird im Parameter ev_xml vom Typ xstring entgegengenommen (es könnte auch ein string oder ein Objekt vom Typ if_ixml_document sein - die Transformation erkennt selbständig den erwarteten Typ).

Die Simple Transformation selbst ist nun wirklich einfach: Nach Festlegung des Referenz-Datenobjekts mittels <tt:root> werden die Meldungen mit einer geschachtelten <tt:loop> abgearbeitet. In der inneren Loop wird schliesslich die Ergebniszeile hergestellt, indem die entsprechenden Quellfelder aus Meldungskopf oder Meldungsposition in das durch das Mapping gewünschte Zielfeld übernommen werden.

Hier ein typisches Beispiel (wir haben rund ein Dutzend solcher Transformationen, und sie sehen alle ähnlich aus):

<?sap.transform.simple?>
<!-- Automatisch generierte Transformation.
Bitte Anpassungen nur an der Vorlage machen
(XSLT-Transformation ZGDBW_TO_XI_CREATE)-->
<tt:transform xmlns:tt="http://www.sap.com/transformation-templates">
<tt:root name="MELDUNGEN"/>
<tt:template>
<ns:MT_MVN_DESADV2500 xmlns:ns="http://migros.ch/xi/DESADV2500">
<tt:loop ref=".MELDUNGEN" name="header">
<tt:loop ref="DETAIL">
<row>
<ABSENDER>
<tt:value ref="$header.ABSENDER"/>
</ABSENDER>
<MELDUNGS_ID>
<tt:value ref="$header.MELDUNGS_ID"/>
</MELDUNGS_ID>
(... weitere Kopffelder ...)
<FELD_1 >
<tt:value ref="FELD_1 "/>
</FELD_1 >
(... weitere Detailfelder ...)
</row>
</tt:loop>
</tt:loop>
</ns:MT_MVN_DESADV2500>
</tt:template>
</tt:transform>


Was heisst hier "automatisch generiert"? Ein weiterer starker Vorteil der Simple Transformations (ebenso wie der XSLT-Transformationen) ist, dass es ein einfach zu bedienendes API zum Erstellen einer Transformation im Repository gibt - die Klasse CL_O2_API_XSLTDESC. Dies kam uns in unserer Situation sehr entgegen: Dinge wie der Namensraum des Zieldokuments und die tatsächlich zu extrahierenden Felder variieren nämlich je nach Meldungstyp. Wir haben daher eine simple Customizingtabelle vorgesehen, in der wir pro Meldungstyp diese Unterschiede festlegen. Ein Report Z_REGENERATE_ST baut aus dieser Customizingtabelle alle vorgesehenen ST-Transformationen auf (wobei er sich selbst einer XSLT-Transformation bedient). Diese werden dann dynamisch aufgerufen. Dieses Vorgehen hat den Vorteil, dass die Transformationen möglichst einfach und sehr schnell werden. Ausserdem hat eine im Repository gepflegte Transformation den Vorteil, dass sie bei Gebrauch im Hauptspeicher des Servers gepuffert wird - was ihre Ausführung noch mehr beschleunigt.


[1] Zwar gibt es neben dem Disassembler javap hervorragende Java-Decompiler mit begeisternden Benutzerschnittstellen, wie z.B. jd-gui. Es ist aber umständlich, ein Programm erst decompilieren zu müssen, um seine Logik zu verstehen und ggf. zu modifizieren. Bei der in der Branche zunehmenden Kleingeistigkeit ist ausserdem vermehrt mit dem Einsatz von Bytecode-Verdunklern (Obfuskatoren) zu rechnen, die die Lesbarkeit des decompilierten Codes weiter erschweren.

[2] Das geschieht mittels eines internen HTTP-Requests - d.h. der Baustein HTTP_DISPATCH_REQUEST erscheint im eigenen Callstack, der Request wird nicht in einem separaten Task bearbeitet. Insbesondere ist die Verarbeitung der Requests somit notwendigerweise synchron: Wenn der Job durchgelaufen ist, haben sämtliche Meldungen den Plain Adapter passiert.

[3] Wir haben hier eine Loop über eine Tabelle mit N Eintragen, die eine Loop über eine zweite Tabelle mit N*M Einträgen enthält. Das könnte ein Problem ergeben. Damit hier keine quadratischen Effekte auftreten, darf die Positionstabelle keine Standardtabelle sein. In unserem Fall sind beide Tabellen sortiert. Die innere Loop mit Where-Bedingung nutzt implizit die Sortierordnung gemäss Tabellenschlüssel aus, so dass kein Performanceproblem entsteht. Eine - noch etwas effizientere - Alternative wäre gewesen, mit der loop at it_detail zu beginnen (ohne Where-Bedingung) und mittels at new die Kopfdaten beim Wechsel zu einer neuen Meldung nachzulesen. Aber, wie man an den Laufzeiten sieht, ist der "Marshalling Prepare" Schritt auch in dieser Form unkritisch.

Samstag, 9. Oktober 2010

Tabellenpflege als Webanwendung

Die Situation ist elementar und kommt häufig vor: Irgendwelche tabellenförmigen Daten – zum Beispiel die Buchungen eines Kontos – sollen mit Hilfe eine Webanwendung gepflegt werden. Trotzdem habe ich nach einer ersten Sichtung keine passende Software für diesen allereinfachsten Anwendungsfall gefunden.[1]

Hier zeige ich eine Implementierung für die Pflegeoberfläche mit JavaScript, die die gesamte Änderungsverwaltung auf dem Client übernimmt. Die Applikation besteht aus einer einzigen Webseite, die weder verlassen noch neu geladen wird. Sie kommuniziert intern über Ajax-Requests mit dem Server, der die Daten anliefert und Änderungen fortschreibt.

Im einzelnen muss eine Anwendung zur Tabellenpflege

  • die Tabellendaten vom Server lesen,
  • dem Benutzer zur Ansicht und Pflege anbieten,
  • der Zellen verändern, ganze Zeilen löschen oder neue Zeilen hinzufügen kann
  • und schliesslich diese Änderungen sichern kann,
  • wodurch sie auf den Server zurückgeschrieben werden.

Wie ist eine solche Anwendung zu entwerfen? Sicher einmal benötigt man ein physisches Bild der Tabelle, das irgendwo im Filesystem hinterlegt sein muss. Auf der anderen Seite bedarf es einer Präsentationsschicht (GUI), um die Tabelle dem Benutzer anzuzeigen und ihm Änderungen anzubieten. Für Webanwendungen besteht die Präsentationsschicht in der Regel aus HTML-Seiten.

Zwischen diesen beiden Endpunkten – GUI und Filesystem – liegen im wesentlichen zwei Softwarekomponenten, die bei einer solchen Anwendung zusammenarbeiten:

  • Eine Persistenzkomponente, die dafür zuständig ist, dass die Daten über die Sitzung des Benutzers hinaus existieren.

  • Und eine Änderungsverwaltung, die die Änderungen des Benutzers in seiner aktuellen Sitzung protokolliert und zu einem vom Benutzer gewählten Zeitpunkt an die Persistenzkomponente übergibt.



An den Jargon der Datenbankprogrammierer angelehnt, liegt die Tabelle in der Anwendung in zwei Formen vor: Als Before-Image repräsentiert sie den Stand der Daten, bevor der Benutzer sie zu Gesicht bekommt und manipulieren kann (mit dem Image ist das Pflegebild gemeint, auf dem der Benutzer die Daten ändern kann). Das After-Image modelliert folglich die Änderungen, die der Benutzer an den Daten vorgenommen hat. Aus dem Vergleich des Before-Image mit dem After-Image ist eine Reihe von insert-, update- und delete-Operationen ableitbar, die an der Tabelle vorgenommen werden müssen.

Wenn die Tabelle vom Server kommt, stellt sie das Before-Image dar. Diese Daten muss der Client in einer Form präsentieren, die dem Benutzer leicht Änderungen ermöglicht. Neben der Präsentation verwaltet der Client auch die vom Benutzer getätigten Änderungen. Der Client weiss jederzeit, ob und welche Änderungen vorgenommen wurden. Nur wenn wirklich Änderungen vorgenommen wurden, braucht man dem Benutzer einen Sichern-Button anzubieten, mit dem er die gerade getätigten Änderungen "committen", das heisst in der Persistenzschicht fortschreiben kann.

Hier meine Beispiel-Implementierung [2]:

http://www.ruediger-plantiko.net/konto



Die Anwendung besteht aus einer einzigen HTML-Seite. Clientseitige Logik ist mit JavaScript abgebildet. Die Kommunikation mit dem Server erfolgt über Ajax, wobei als Struktur für den Datenaustausch in beide Richtungen das JavaScript-Datenformat JSON verwendet wird. Die HTML-Seite wird mittels JavaScript dynamisch manipuliert. Auf Serverseite nimmt ein Perl-Programm unter CGI die Anfrage entgegen, wobei ein Query-Parameter names action dem Server mitteilt, was er machen soll. Beispielsweise teilt die URL

/cgi-bin/konto.pl?action=get_all


dem Programm konto.pl mit, dass es alle Buchungszeilen aus der Kontodatei einlesen und dem Client im JSON-Format senden soll. Dagegen teilt eine zweite action namens save

/cgi-bin/konto.pl?action=save


dem Server mit, dass sich im Bauch des HTTP-Requests ein JSON-Hash mit Zeilendaten befindet, die in der Kontodatei aktualisiert werden sollen. Er verbucht diese Änderungen und sendet dann dem Client wie bei get_all das aktualisierte Bild der Datei (wieder im JSON-Format). Nur diese beiden actions werden übrigens benötigt.

Die beiden Schichten kommunizieren nur über diese Schnittstelle und sind ansonsten völlig unabhängig voneinander. Das heisst beispielsweise: Statt mit Perl unter CGI könnte man den Request auch mit einer beliebigen anderen Technologie behandeln. In meiner Implementierung ist der Schnitt zwischen den Systemen zugleich auch der Schnitt zwischen den beiden erwähnten Softwarekomponenten: Der Server verwaltet die Persistenzschicht, während die Verwaltung der Änderungen auf dem Client erfolgt.

Zu den Besonderheiten dieser Implementierung gehört, dass die Daten auf dem Client nur in der dynamischen Tabelle buchungen gehalten werden - das ist genau die Tabelle, die der Benutzer sieht. Es gibt kein Extrabild dieser Daten, etwa in Form eines globalen Arrays. Ähnlich ist es mit der Änderungsverwaltung: Es gibt kein globales Flag dataLoss. Wenn der Benutzer Änderungen vornimmt, werden diese für ihn sichtbar in der Tabelle als geändert markiert. Es gibt kein globales Flag, wohl aber eine Funktion dataLoss(), die einfach schaut, ob Zellen als geändert markiert sind.

Aber fangen wir mit dem Datenformat an. Ich habe mir der Einfachheit ein CSV-artiges Format ausgedacht. Man kann Kommentarzeilen und Leerzeilen verwenden. Diese bleiben bei Updates erhalten und werden im übrigen ignoriert. Kommentarzeilen beginnen wie in Perl mit dem Doppelkreuz (#). Ausserdem soll es neben den Buchungszeilen noch andere Zeilen in der Datei geben, die manuell oder von einem anderen Programm eingefügt werden könnten. Beispielsweise könnte ein Cron-Job regelmässig den Kontostand abfragen und als realen Saldo in die Datei einfügen. Unser Programm soll all diese Zeilen reproduzieren, aber für die eigene Verarbeitung ignorieren.

Das lässt sich so einrichten, dass die erste Spalte im CSV-Format eine ID des Satzes darstellt. Diese ID kann für die allein zu berücksichtigenden Buchungszeilen das Präfix buch haben, während andere Zeilen andere Präfixe haben. Saldozeilen zum Beispiel könnten mit dem Präfix saldo eingeleitete ID's haben. Die Datei kann also etwa folgendermassen aussehen:

# Abhebungen
buch1;1.09.2010;150.00;Norbert;Kursgebühr "Modernes JavaScript"
buch2;10.9.2010;605.00;Petra;Flug nach Florenz, Mietauto, Übernachtung
buch3;23.9.2010;200.00;Norbert;Spende an Wikipedia
saldo1;30.9.2010;15362.00
buch4;3.10.2010;350.00;Petra;Neues Smartphone


Wenn die Tabellenpflege nach Anmeldung im Web aufgerufen wird, erhält die zuständige Perl-Klasse CsvTableMaintainer den Befehl zum Laden aller Buchungszeilen (action=get_all). Die obige Beispieldatei wird dabei in folgendes JSON-Objekt transformiert:
{ buchungen:[
["buch1","1.09.2010","150.00","Norbert","Kursgebühr \"Modernes JavaScript\""],
["buch2","10.9.2010","605.00","Petra","Flug nach Florenz, Mietauto, Übernachtung "],
["buch3","23.9.2010","200.00","Norbert","Spende an Wikipedia"],
["buch4","3.10.2010","350.00","Petra","Neues Smartphone"]
],
user:"petra",
msg:""
}


Dieser Hash wird nun im Browser von der JavaScript-Funktion updatePage() entgegengenommen. Diese überträgt zunächst den User, unter dem die Anmeldung erfolgte, in ein dafür vorgesehenes Feld (wenn es nicht angezeigt werden soll, kann man dieses Feld auf unsichtbar setzen). Für jedes weitere Element des Hashs wird eine Funktion <key>_update(<value>) aufgerufen, wobei <key> den Schlüssel und <value> den Wert zu diesem Eintrag bedeutet — falls eine Funktion dieses Namens existiert. Wenn nicht, wird <key> einfach als ID eines Elements im HTML-DOM betrachtet, dessen Inhalt durch <value> zu ersetzen ist. Schliesslich wird noch die Sanduhr-Graphik auf unsichtbar geschaltet, die dem Benutzer die Server-Aktivität anzeigte:

// --- Nach Rückkehr eines Ajax-Requests: Seitenteile aktualisieren
function updatePage(transport) {
// Parameter transport ist das Ajax-Objekt (letztlich XMLHttpRequest)
var id, newCode;
newCode = transport.responseText.evalJSON();

// Den User zuerst aktualisieren
if (newCode.user) {
user_update( newCode.user );
delete newCode.user;
}

for (id in newCode) {
// Entweder mit spezieller Methode, falls implementiert ...
if (typeof self[id+"_update"] == "function") {
self[id+"_update"](newCode[id]);
}
// ... oder einfach durch Austausch des HTML-Contents
else {
$(id).update( newCode[id] );
}
}
// Ladezustand zurücksetzen
$("loading").hide();
}


Für den Schlüssel buchungen gibt es eine designierte Funktion buchungen_update(), die demnach mit dem Array der Buchungszeilen aufgerufen wird. Sie bekommt einen Array of Arrays übergeben (AoA), kann also auf jede Zelle jeder Zeile zugreifen. Sie durchläuft die anzuzeigenden Zellen in einer geschachtelten each()-Schleife und baut dabei die einzelnen <tr>- und <td>-Elemente der HTML-Tabelle auf. Die vom Anmeldeuser getätigten Buchungen bekommen darüberhinaus noch eine weitere Zelle, die controlCell, mit Ikonen zum Ändern und Löschen von Zeilen. Diese müssen dann noch alle klicksensitiv gemacht werden, indem der doOnClick() für das Event click registriert wird. Schliesslich wird der Button zum Sichern verborgen: Denn immer wenn diese Funktion durchlaufen wird, enthält die Tabelle den reinen Datenbankstand. Ein Sichern ist also unnötig:

// --- Vom Server als Array of Array (AoA) gesendete Buchungszeilen 
// ins HTML übernehmen
function buchungen_update( rows ) {

var tbody = $("buchungen").down("tbody");
var user = $("user").innerHTML;

tbody.update("");

var controlCellCode = controlCell();

rows.each( function(cells) {
var rowid = cells.shift();
var row = new Element( "tr", {id:rowid} );
cells.each( function(cellData, index) {
row.appendChild(
new Element( "td",
{className:"c"+(index+1)})
.update(cellData) );
});
row.appendChild(
new Element( "td",
{className:"c5"} ).update(
(cells[2] == user) ? controlCellCode : "" ) );
tbody.appendChild(row);
});

$("buchungen").show();

// Alle Bilder in der Buchungstabelle sind clicksensitiv
$$("#buchungen img").each( function( img ) {
img.observe("click", doOnClick );
});

// Datenbankstand - Sichern ist unnötig
$("save").hide();

}


Wenn der Benutzer nun "Ändern" oder "Neuer Eintrag" klickt, öffnet sich ein Formularbereich zum Pflegen einer einzelnen Tabellenzeile. Dieses Formular dient wirklich nur zum Editieren, es wird niemals verschickt. Zum Abschliessen drückt der Benutzer im Formularbereich auf den Button "Übernehmen". Dann schliesst sich das Formular, und die geänderten oder neuen Zellinhalte werden in der Tabelle eingetragen und erhalten eine markierung in Form der CSS-Klasse changed. Diese Markierung zeigt dem User ebenso wie dem Programm, dass diese Felder sich vom Datenbankstand unterscheiden.

Da die Funktion dataLoss() nun anspricht, wird auch der Button zum Sichern angeboten. Hierfür ist die Funktion checkDataLoss() zuständig, die nach allen Operationen, die potentiell zu Datenänderungen führen, aufgerufen wird. Diese ruft ihrerseits die schon erwähnte Funktion dataLoss() auf, um den Änderungsstatus durch Inspektion der Tabelle zu ermitteln. Das kann man mit dem Prototype-Framework sehr kompakt formulieren. Die vier Terme der Funktion lesen sich so: Schaue nach, ob es irgendeine echte Datenzeile der Buchungstabelle gibt, die entweder auf Zeilenebene die CSS-Klasse deleted besitzt oder irgendeine Zelle mit der CSS-Klasse changed enthält. Da die verwendeten Funktionen any() und down() kurzschliessen, d.h. die Iteration nach dem ersten Fund abbrechen, ist die Implementierung der Methode dataLoss() nicht nur kurz, sondern auch effizient:

// --- Save-Button nur anbieten, wenn sich Daten geändert haben
function checkDataLoss() {
$("save").style.display = dataLoss() ? "inline" : "none";
}

// --- Feststellen, ob Daten geändert wurden
function dataLoss() {
return $("buchungen").down("tbody").
select("tr").any( function(row) {
return row.hasClassName("deleted") ||
row.down("td[class~=changed]");
});
}


Irgendwann drückt der Benutzer "Sichern". Dann werden die Änderungen in der Funktion extractChanges() ermittelt und in Form eines JSON-Hashs an den Server übergeben. Neu angelegte Zeilen (die dadurch zu erkennen sind, dass ihre (vorläufige) ID mit dem Präfix new beginnt) werden hierbei gleich behandelt wie geänderte Zeilen: Die Zeilendaten werden als Datenteil zur ID in den Hash eingefügt. Für gelöschte Zeilen das spezielle Schlüsselwort deleted als Datenteil übergeben. Wenn die Benutzerin Petra in obigem Beispiel die Zeile buch2 löscht, den Betrag der Zeile buch4 von 350.00 CHF auf 400.00 CHF ändert und eine neue Zeile einfügt, um ihre Hannoversche Hotelrechnung zu erfassen, sieht der an den Server übergebene Hash folgendermassen aus:
{
buch2:"deleted",
buch4:"3.10.2010;400.00;Petra;Neues Smartphone",
new1:"9.10.2010;100.00;Übernachtung Hannover"
}

Der Server erhält diesen JSON-Hash im Body des HTTP-Requests, zusammen mit dem Wert save für den URL-Parameter action. Er wertet den Hash aus und übersetzt ihn in Anweisungen zur Änderung des Datenfiles. Auch wird für neu angelegte Zeilen eine endgültige ID vergeben. Schliesslich gibt er — wie das Kommando get_all — den aktuellen Stand der Buchungszeilen an den Client zurück.[3]

Der Einstiegspunkt für alle internen, von der Webseite mittels Ajax abgesetzten CGI-Anfragen ist das Perl-Programm konto.pl. Es wertet den URL-Parameter action aus. Wie macht es das? Der CGI-Mechanismus ist sehr einfach: Der Query-Teil einer URL wird dem aufgerufenen Programm in Form einer Umgebungsvariablen zur Verfügung gestellt. Der Body des Requests kann aus der Standardeingabe eingelesen werden, und alle Ausgaben an die Standardausgabe bilden den Body der HTTP-Antwort. Das Programm konto.pl extrahiert daher den Wert von action aus dem Query-String der URL und ruft dann dynamisch das Unterprogramm dieses Namens auf. Das geht so:
#!W:/perl/bin/perl.exe

# CGI Perl-Requesthandler, der mit der Kontopflegeseite kommuniziert

use strict;
use warnings;
no strict 'refs';

use CsvTableMaintainer; # Tabellenpflegeklasse
use MiniJSON; # Benötigte Perl-JSON-Konvertierungen

# URL-Querystring
my $queryString = $ENV{"QUERY_STRING"}
|| "action=get_all"; # Für standalone Testausführungen
my ($action) = ($queryString =~ /action=(\w+)/);

# Instanz der Tabellenpflegeklasse bilden
my $tableMaintainer = CsvTableMaintainer::new( file=>"konto.dat" );

# Erste Zeile der HTTP-Antwort
print "Content-Type:text/plain\n\n";

# Im Queryparameter übergebene Aktion ausführen
print &$action() if $action;

sub get_all {
...
}

sub save {
...
}


Wie man sieht, ist konto.pl nur der Dispatcher, der die Anfragen entgegennimmt und die entsprechenden Subroutinen aufruft. konto.pl ist schmal und hat nur wenige Subroutinen — normalerweise nur die den actions entsprechenden Unterprogramme. Bemerkenswert ist, dass für einfache Aufgaben wie diese auch kein use CGI::<irgendwas> notwendig ist: Query-Parameter auslesen und auf Payloads der ein- und ausgehenden HTTP-Nachrichten zuzugreifen, ist unter CGI so einfach, dass keine weiteren Hilfspakete dafür nötig sind.

konto.pl ist natürlich auch deshalb so schmal, weil es die eigentlichen Aufgaben an die Klasse CsvTableMaintainer und zu einem kleineren Teil an das Paket MiniJSON delegiert. Diese Programmteile befinden sich - wie auch alle anderen, für dieses Beispiel benötigten Programmteile - in meinem github-Reposoritory konto. Wer sich die Beispielapplikation daher noch genauer ansehen will, sei auf dieses Repository verwiesen.


[1] Vielleicht habe ich nicht gründlich genug gesucht oder war mit den gefundenen Objekten nicht zufrieden.
[2] Neben dem JavaScript-Framework Prototype von Sam Stephenso, das mich dabei unterstützt, lesbares JavaScript zu schreiben, verwende ich den Datepicker von Hugo Ortega Fernandez. Und - ja, ich mag die SAP-Ikonen!
[3] Um Codeduplizierung zu vermeiden, ruft er dafür nach Ausführung des Sicherns schlicht die Aktion get_all auf.

Donnerstag, 30. September 2010

Das schriftliche Wurzelziehen

Vor ein paar Tagen brachte mein Sohn interessante Rechenaufgaben aus der Schule mit: Der Mathematiklehrer hatte den Kindern das gute alte schriftliche Wurzelziehen beigebracht — ein Verfahren, bei dem die erste binomische Formel ausgenutzt wird, um sukzessive die Stellen der Wurzel zu ermitteln. Das Verfahren ist heute fast völlig in Vergessenheit geraten, wird jedoch an den Waldorfschulen noch in Ehren gehalten — ihrem ausgeprägten Konservatismus sei Dank.

Für mich war es eine nette Erfahrung, das Verfahren in Form von JavaScript-Code nachzubilden — und zwar so, dass es zum Üben benutzt werden kann. Statt die gesamte Rechnung auf einen Schlag anzuzeigen, sollten auf Knopfdruck die einzelnen Schritte ausgeführt werden. So kann man auf dem Papier das Verfahren selbst probieren und sich bei jedem Schritt vergewissern, keinen Fehler gemacht zu haben.

Das Programm soll die Rechnung, wie sie auf dem Papier entsteht, erzeugen, die Schritte sollen mit Kommentaren versehen werden, und in einem eigenen Bereich sollen kleine Nebenrechnungen ausgeführt werden — analog dem Schmierpapier, das man bei der schriftlichen Rechnung braucht.

Ich setzte mir ein Zeitlimit von drei Stunden, das ich gerade so einhalten konnte — in nostalgischer Erinnerung an die guten alten Zeiten, als ich mich noch in Prüfungen bewähren musste: "Sie haben drei Stunden — die Zeit läuft." Allerdings habe ich später noch eine weitere Stunde für ein paar Refaktorisierungen aufgewendet. Die Webseite

http://ruediger-plantiko.net/wurzeln/

zeigt das Ergebnis. Es folgen ein paar Anmerkungen zum Programmentwurf.


  • Die HTML-Seite zeigt nichts anderes als das Seitenlayout. Sie ist vollständig von JavaScript freigehalten. Auch Clickbehandler, onload-Behandler usw. sind in die Scriptdatei ausgelagert.

  • Um lesbares und dennoch kurzes JavaScript zu produzieren, verwende ich das Framework Prototype.

  • Der Unterbruch zwischen den einzelnen Rechenschritten erfordert es leider, dass Zwischenergebnisse in globalen Variablen mitgeführt werden müssen. Wenigstens werden diese globalen Variablen alle an einem Ort deklariert und an einem zweiten Ort, in einer reset()-Funktion wieder gelöscht.



Der Algorithmus besteht aus insgesamt neun Schritten. Die ersten drei Schritte werden nur einmal ausgeführt. Die nachfolgenden sechs Schritte werden so lange ausgeführt, bis keine weiteren Ziffern vom Radikanden mehr heruntergeholt werden können. Dann ist das Verfahren beendet.

Mein Entwurf packt diese neun Schritte als anonyme Funktionen in einen Array execute[], so dass sie dynamisch über einen Index angesprochen und ausgeführt werden können. Die Methode executeNextStep(), die bei Click auf den Weiter-Button aufgerufen wird, kann die Schleife dann ohne umständliche switch...case Konstrukte realisieren, indem der Schrittzähler step kontinuierlich erhöht wird, der Divisionsrest modulo 6 aber als Index für den Zugriff auf den execute-Array fungiert.[1]

Um aus der Schleife auszubrechen, empfiehlt sich eine Exception - im simpelsten Fall, wie hier, einfach in Form eines Stringobjekts, das dann angezeigt wird.

function executeNextStep() {

try {

// Schrittzähler erhöhen
incrementStepCounter( );

// Nächsten Schritt ausführen
if (step <= 2)
// Schritte 0 bis 2 nur einmal
execute[step]();
else
// Ab Schritt 3 wird es periodisch
execute[3+((step-3)%6)]();

} catch (e) {

// Ausnahme (z.B. STOP) melden und Verfahren beenden
commentOhneSchritt( "<b>" + e + "</b>");
verfahrenBeenden();
}
}

// Hier folgen die Schritte:
execute = [
function() {
comment("Zahl nach links in Zweiergruppen aufteilen");
var r = radikand+"",i;
for (i=r.length-2;i>=-1;i-=2) {
if (i>=0)
groups.unshift( r.substring(i,i+2) );
else
groups.unshift( r.substring(0,1) );
}
s = "";
groups.each( function(g) {
s+='<span class="group">' + g + '</span>';
});
write(s);
},
function() {
...
},
...
];


Diese Art, das Problem zu lösen, lässt sich sicher auch in anderen Kontexten einsetzen: Nämlich immer dann, wenn ein Algorithmus stückweise auszuführen ist, etwa wie hier in einer Schleife mit Abbruchbedingung. Der Zugriff auf Funktionen als Arrayelemente ist für eine Abfolge von Schritten naheliegender als der Zugriff über den Funktionsnamen, etwa self["schritt2"](), der ja alternativ auch möglich wäre. Die Lösung ist aber vor allem kompakter als eine switch...case...-Konstruktion, ohne an Lesbarkeit einzubüssen. Im Gegenteil, gerade die mühsam abzuhandelnden case's, die in C-artigen Sprachen (wie JavaScript) auch noch jeden case mit einem break beenden müssen, erschweren nach meiner Ansicht die Lesbarkeit von Code.

Im Rückblick fand ich es unbefriedigend, dass ich für mein geradezu spartanisches UI-Design relativ viel Zeit aufwenden musste. Für die eigentlichen Logik des Wurzelziehens brauchte ich nur knapp die Hälfte der Zeit, die ich mir vorgegeben hatte. Zu erwarten wäre eigentlich, dass man für die Problem Domain die meiste Zeit aufwenden muss, nicht für das User Interface. Das hat bei mir sicher auch damit zu tun, dass ich nicht wie ein hauptberuflicher Webdesigner ständig neue HTML/CSS-Oberflächen kreiere, sondern an einmal geschaffenen Entwürfen sehr lange und sehr genügsam festhalte, denn auch beruflich programmiere ich hauptsächlich Geschäftslogik und modifziere meist nur bestehende Entwürfe von Benutzeroberflächen. Insgesamt muss ich jedoch meine Kritik aus dem bösen Tabellenlayout wiederholen, dass ich CSS in der jetzigen Form für die Gestaltung der Präsentationsschicht einfach noch nicht griffig genug finde.

[1] Auf den Namen execute für den Funktions-Array bin ich übrigens erst nach einigem Versuchen gekommen: Ausdrücke wie execute[step]() gewinnen durch ihn an Lesbarkeit.

Montag, 28. Juni 2010

Ilsebils Problem

Das "Märchen vom Fischer und seiner Frau" erzählt uns Ilsebils Biographie:

Ilsebil war die Frau eines armen Fischers, der eines Tages das Glück hatte, einen sprechenden Butt zu fangen. Aus Respekt vor dessen ungewöhnlicher Begabung liess er ihn wieder frei. Als Ilsebil davon erfuhr, schickte sie ihn an den Strand zurück, denn sie war der Ansicht, dass man noch eine Gegenleistung für diese gute Tat aus dem Butt herausschlagen könnte, z.B. eine Wohnung von angemessener Qualität, statt des elenden Lochs (des "ollen Pisspotts"), in dem sie lebten (ihren Mann schienen die prekären Wohnverhältnisse ja nicht zu stören - tja: Männer!). Auf des Fischers Rufen erschien der Butt und kam ihrem Wunsch tatsächlich nach. Doch unfähig, sich mit einem erfüllten Wunsch zu bescheiden, schickte Ilsebil ihren Fischermann immer wieder an die See, damit der Butt ihr weitere Wünsche erfüllte. Sie liess sich vom Butt zum Bürgermeister, zur Königin, zur Päpstin machen. Aber die Unzufriedenheit blieb. Dann kostete sie der frevelhafte Wunsch, Gott zu werden, alles bisher Erreichte, und die beiden sassen schliesslich wieder dort, wo sie schon am Anfang der Geschichte gesessen hatten: In ihrem "ollen Pisspott".

Es ist naheliegend, dieses Märchen als Warnung vor dem menschlichen Hochmut zu deuten. Ist es nicht bloss eine Illustration des Sprichworts "Hochmut kommt vor dem Fall"? Daran ist sicher etwas. Meiner Ansicht nach greift diese Erklärung aber zu kurz. Woher kommt denn dieser Hochmut? Was ist eigentlich Ilsebils Problem?

Wenn wir Ilsebil ihrem Partner gegenüberstellen, wird einiges deutlich. Der Fischer ist grossherzig und freigebig, nicht berechnend: Er gibt den Butt einfach wieder frei und denkt gar nicht daran, dass man für diese Tat etwas verlangen könnte. Er ist auch mit seiner Lage zufrieden, es ist für ihn "in Ordnung" - er hat noch nicht einmal darüber nachgedacht, ob man sich nicht wohnlich verbessern könnte. Als der Butt sie mit einem prächtiges Haus beschenkt, staunt der Fischer, freut sich und sagt: "So soll's bleiben, nun wollen wir recht vergnügt leben." Nach nur einer kurzen Weile verdüstert sich aber Ilsebils Stimmung, und sie strebt nach Höherem - wieder schickt sie den Fischer aus. Der Fischer sagt bei sich: "Es ist nicht recht". Aber aus Loyalität zu der Frau, seiner Partnerin, der er sich in der Ehe versprochen hat, fügt er sich in ihren Willen.

Was also ist Ilsebils Problem? Aus der Gegenüberstellung mit ihrem Mann wird es klar: Ihr Problem ist die Sorge. Sie ist unfähig zur Lebensfreude, kann ihr Leben nicht geniessen. Die Sorge treibt sie immer weiter, einem unbestimmten Ziel entgegen. Sich auf dem Erreichten auszuruhen und es zu geniessen, wie es der Fischer ihr immer wieder vorlebt und vorschlägt, ist ihr nicht möglich. Der Wunsch nach besseren Wohnverhältnissen und höherem Status sind nur eine Folge dieser Unfähigkeit zum Lebensgenuss. Um diese Unfähigkeit nicht wahrhaben zu müssen, projiziert sie ihr Problem in die Aussenwelt: die Verhältnisse sind schuld an ihrer Unzufriedenheit. Sie versucht verzweifelt, diese Verhältnisse zu verändern - denn natürlich spürt sie, dass ihr etwas fehlt. Aber nach der Wanderung durch alle möglichen Verhältnisse wird sie erkennen müssen, dass ihr Problem bestehen bleibt, weil es in ihr selbst liegt.

Ich sehe das Märchen als eine Warnung vor dem Dämon der Sorge. Das vereinseitigte Sich-Sorgen, ohne das Gegengewicht der Lebensfreude, lähmt die höchsten und edelsten Antriebe. Dann macht die Sorge den Menschen klein, raubt ihm seine Ideale, den Optimismus, die Tatkraft und letztlich alle positiven Gefühle. Nur wer geniessen und sich freuen kann und dabei im Herzen sowohl bescheiden als auch grosszügig bleibt, kann auch in diesem Leben etwas Substantielles schaffen und hoffnungsvoll voranschreiten auf einem selbstgewählten Weg - statt sich im Vegetativen, in der Sorge um die täglichen Dinge, um Essen, Trinken, Wohnen, Schlafen, Kleidung, Mode, Aussehen, Ansehen usw. zu erschöpfen.

Astrologisch ist Ilsebil ein Saturn-, ihr Mann ein Jupitersymbol. Alle aufgeführten Attribute des Mannes sind Jupiterattribute, einschliesslich des Fischerberufs (die Fische sind Domizil des Jupiter). Die Sorge aber ist dem Saturn zuzuordnen. Man könnte einwenden, dass das Motiv der Unbegrenztheit, hier der unbegrenzten Vermehrung von Macht und Gütern, eher Jupiternatur habe. Aber diese Art der Unbegrenztheit ist nicht die jovische, aus vollem, weitem Herzen schöpfende Fülle, sondern eine Frucht von Engherzigkeit und Sorge. Sie trägt etwas Zwanghaftes, ist letztlich aus der Not geboren. Es hätte zur Illustration auch irgendeine andere Tretmühle als die der Macht gewählt worden sein.

In diesem Zusammenhang gibt das Märchen, in deren Protagonisten wir ja auch gewisse geschlechtstypische Züge zu erkennen glauben, der Geschlechterfrage eine interessante Wendung: Statt der wohlbekannten Polarität von Mars und Venus lädt dieses Märchen dazu ein, die Geschlechter im Lichte der Jupiter-Saturn-Polarität zu beleuchten. Vielleicht passt es auch in diese ungewohnte Typologie, wenn in einer gemeinsamen Studie von Neurowissenschaftlern und Ökonomen nun ermittelt wurde, dass das übel beleumundete Testosteron, dessen Sklaven wir Männer ja angeblich sind und dem wir unsere Aggressionen und Sexgier zuschreiben sollen, in Wahrheit offenbar umgekehrt wirkt, indem es zu fairen, klaren und kooperativen Angeboten geneigt macht! [1]

In diesem Lichte liegt die grösste Gefahr für den Mann in der Übertreibung des Jupiterprinzips: Im Rauschhaften, im Sich-Selbst-Überheben, in der sich selbst verleugnenden grenzenlosen Hingabe an etwas, das dieser Hingabe nicht würdig ist, in einer zu grossen Risikofreude und Leichtfertigkeit im Umgang mit seinem Besitz, ja mit seiner ganzen Existenz. Die grösste Gefahr für die Frau läge dagegen darin, sich in ihrem Leben auf das Reich der Notwendigkeit zu beschränken, allen höheren Perspektiven abzuschwören und nur noch von der Sorge um das Alltägliche und Grundlegende geleitet zu werden.


[1] Christoph Eisenegger, Michael Naef, Romana Snozzi, Markus Heinrichs, Ernst Fehr: Prejudice and truth about the effect of testosterone on human bargaining behaviour, Nature 463, 356-359 (21.1.2010) - doi:10.1038/nature08711.

Samstag, 1. Mai 2010

Eine Alternative zum Besucher-Entwurfsmuster

Der Besucher ist ein unter Compilerbauern beliebtes Entwurfsmuster aus dem bekannten Buch der "Viererbande" [1]. Es hat jedoch einige schwerwiegende Nachteile. Meist kommt man mit einer ebenso naheliegenden wie praktischen Alternative zum Besucher-Entwurfsmuster aus, die ich hier vorstellen möchte.

Die Ausgangslage des Besucher-Entwurfsmusters lässt sich am besten am Beispiel eines Parsers verdeutlichen. Sobald der Quelltext eines Programms, also ein textförmiges, von Mensch und Maschine lesbares Dokument, vom Parser einmal in ein komplexes, strukturiertes Datenobjekt transformiert worden ist, sind verschiedene Weiterverarbeitungen möglich: Ein Compiler kann Bytecode erzeugen. Ein Interpreter könnte das Programm direkt ausführen. Ein Pretty Printer könnte eine formatierte Ausgabe des Programms erzeugen. Ein Code Inspector könnte Metriken errechnen oder problematische Anweisungen aufspüren. In allen Fällen wäre das grundsätzliche Vorgehen gleich: Die einzelnen Elemente der komplexen Objektstruktur werden nach einem bestimmten Verfahren durchlaufen, und für jedes Objekt wird eine - für die Klasse dieses Objekts jeweils passende - Operation ausgeführt. Ziel des Besucher-Entwurfsmusters ist, dieses Vorgehen zu vereinheitlichen und dabei die auszuführenden Operationen von den Klassen der Objektstruktur zu trennen.

Was sind die Teilnehmer des Entwurfsmusters "Besucher"?


  • Eine Objektstruktur (Baum, Liste, ...): Eine Sammlung von Objekten, nennen wir sie Elemente, die Instanzen einer überschaubar kleinen Menge von Klassen darstellen. Nennen wir diese Klassen Elementtypen.

  • Eine Operation, die auf dieser Objektstruktur ausgeführt werden soll, indem

  • ein Prozessor die Objektstruktur traversiert und für jedes durchlaufene Element eine je nach Elementtyp unterschiedliche Methode aufruft.



Das Ziel des Entwurfsmusters "Besucher" ist, den zur Operation gehörenden Code sowohl vom Prozessor als auch von den Elementtypen zu trennen und in einem eigenen Objekttyp zu kapseln: dem Besucher-Interface.

Das Besucher-Interface stellt für jeden Elementtyp eine Methode zur Verfügung. Alle diese Methoden können gleich benannt werden und unterscheiden sich nur im Typ ihres Arguments. Das ist so nur in Sprachen wie Java möglich, die das Überladen von Methoden zulassen, aber z.B. nicht in ABAP (dort müssen die Methoden dann eben pro Elementtyp unterschiedlich benannt werden).

Wenn wir als Objektstruktur zur Veranschaulichung das XML-DOM wählen, müsste die Besucherklasse zum Beispiel folgendes Set von Methoden anbieten:

interface Visitor {
public void visit( ElementNode n );
public void visit( TextNode n );
public void visit( CommentNode n);
...
}


Darüberhinaus stellt jede Elementklasse eine - in einer Abstraktion, z.B. einem Interface oder einer Oberklasse definierte - Methode accept( Visitor v) zur Verfügung. Der Prozessor ruft dann für jedes durchlaufene Element diese accept()-Methode auf, so dass auf dem Weg über das Element die jeweils passende Operation des Visitors ausgeführt wird.

Diese Notwendigkeit, die Objektstruktur um eine accept()-Methode zu erweitern, ist ein böser Nachteil des Besucher-Musters: Wir können nicht den Standard-XML-DOM verwenden, in Java etwa das Paket org.w3c.dom, sondern müssen jede Klasse des DOM durch eine selbstprogrammierte Klasse verschalen, um die neue Methode einführen zu können. Da ist es ein schwacher Trost, dass diese Methode wenigstens allgemein formuliert ist, so dass sie für die verschiedensten Aufgaben verwendet werden kann. Dass die in der Objektstruktur verwendeten konkreten Elementklassen die Methode accept() implementieren müssen, ist vielmehr ein klassischer Fall von "intrusion".

Auch muss man sich, wenn man das Prinzip Don't repeat yourself wirklich ernst nimmt, daran stossen, dass die Implementierungen pro Elementklasse den folgenden immer gleichen Delegations-Code enthalten (obwohl das natürlich für das Entwurfsmuster nicht zwingend ist):

class ElementNode implements Node {
...
public void accept( Visitor visitor) {
visitor.visit( this );
}
}


Die konkrete Implementierung ist nur da, damit der Überlade-Mechanismus anhand des Elementtyps die jeweils richtige visit()-Methode des Besuchers bindet. Wenn die Implementierung fehlte, würde das Überladen nicht korrekt funktionieren, da in Java die Bindung beim Überladen statisch, d.h. bereits zur Compilezeit erfolgt: Der Compiler kann und will gar nicht wissen, welcher konkreter Objekttyp zur Laufzeit tatsächlich beim Aufruf der visit(Node)-Methode übergeben wird.

Der Prozessor befiehlt nun den durchlaufenen Elementen, den Besucher zu akzeptieren. Nehmen wir einmal an, die Klasse DomTree implementiere einen Iterator, um ihre Elemente zu traversieren. Dann wird die Operation des konkreten Besuchers wie folgt ausgeführt:

void process( Visitor visitor, DomTree domTree)
for ( Node n: domTree ) {
n.accept( visitor );
}
}


Ein wesentlicher Zug des Besucher-Entwurfsmusters ist, das Besucherobjekt mit Hilfe der accept(Visitor)-Methode durch alle Elemente der Objektstruktur hindurchzuzwängen. Die auszuführenden Operationen werden gewissermassen von innen, von den Elementen der Objektstruktur aus aufgerufen. Ist das wirklich nötig, nur um eine Trennung des Operations- vom Elementcode zu erreichen?

Warum besteht überhaupt ein Interesse daran, den Besucher durch die Objekte hindurchzuschicken? Die Trennung der Concerns liesse sich auch mit einem viel einfacheren Entwurf erreichen, bei dem die Elemente nicht mit einer ihrer Logik fremden accept()-Methode verschmutzt werden, sondern so bleiben können, wie sie sind. Um im Bild zu bleiben: Das Besucher-Entwurfsmuster lässt den Besucher ins Wohnzimmer. Man kann es aber auch so einrichten, dass man den Besucher gar nicht hineinlässt, sondern bereits an der Haustür abfertigt! Man könnte dies als Hausierer-Entwurfsmuster bezeichnen.

Nehmen wir wieder zur Einfachheit an, unsere Objektstruktur sei ein DOM-Baum. Dann können wir neben dieser Objektstruktur als eigenständige Komponente einen TreeWalker ausmachen. Im TreeWalker ist festgelegt, wie der DOM-Baum traversiert wird. Da der TreeWalker dafür zuständig ist, die Elemente der Objektstruktur zu durchlaufen, ist es eine naheliegende Entscheidung, den Aufruf der visit()-Methode dem TreeWalker zu überlassen: Er sendet dem Visitor für jedes durchlaufene Element die visit()-Nachricht. Eine accept()-Methode wird dann gar nicht benötigt! Der TreeWalker könnte den DOM-Baum zum Beispiel mit der folgenden rekursiven Methode process(Node,Visitor) aufrufen:

import org.w3c.dom.*;
import org.w3c.dom.Node; // Der Name "Node" ist in Java nicht eindeutig
import static org.w3c.dom.Node.*; // Node-Konstanten

class TreeWalker {
public void process( Node n, Visitor v) {
NodeList children = n.getChildNodes();
int numberOfChildren = children.getLength();
for (int i = 0; i < numberOfChildren; ++i) {
process( children.item(i), v);
}
v.visit( n );
}
}


Bei diesem intuitiven Entwurf hat man alle drei Komponenten klar getrennt: Die Objektstruktur, das Durchlaufen der Teilobjekte und schliesslich den Besucher. Beim Durchlaufen der Objektstruktur werden die Teilobjekte der Reihe nach beschafft und dem Besucher zur Bearbeitung vorgelegt.

Es obliegt damit dem Visitor, was er mit den konkreten Node-Objekten macht, die der TreeWalker ihm bei seinem Gang durch den DOM-Baum vorlegt. Um die Operationen vom Typ des zur Laufzeit übergebenen Elements abhängig zu machen, muss irgendeine Art von Reflection eingesetzt werden: Abhängig vom Typ müssen jeweils unterschiedliche Methoden ausgeführt werden. In org.w3c.dom hat jeder Knoten einen bestimmten Knotentyp, der mit der Methode getNodeType() abgefragt werden kann. Damit ersparen wir es uns in diesem Fall, das Java Reflection API zu verwenden, um die aktuell vorliegende Elementklasse zu ermitteln.

Die Knotentypen sind kleine ganze Zahlen. Man kann sie in der Visitor-Implementierung als Index eines Arrays von Methodenreferenzen verwenden. Da es in Java keine Methodenreferenzen gibt, empfehlen sich anonyme innere Klassen. Der folgende Visitor gibt für Elemente den Elementnamen und für Textknoten den Textinhalt in der Konsole aus:

class ExampleVisitor implements Visitor {
// Der folgende Array "visitorFor" dient uns als Dispatcher
private static Visitor[] visitorFor = new Visitor[10];
static {
visitorFor[ELEMENT_NODE] = new Visitor() {
public void visit( Node n ) {
System.out.println( "Element " + ((Element)n).getTagName() );
}
};
visitorFor[TEXT_NODE] = new Visitor() {
public void visit( Node n ) {
System.out.println( "Textknoten: " + n.getTextContent() );
}
};
// usw. für die anderen Knotentypen
// begrenzte Menge, die sich praktisch nicht mehr ändert
}

public void visit( Node n) {

// Dispatchen
visitorFor[n.getNodeType()].visit( n );

}

}


Der Vollständigkeit halber sei hier noch die Testklasse aufgeführt, mit der ich den ganzen Entwurf ausprobiert habe. Sie baut ein Beispieldokument auf und übergibt dessen Wurzelelement und die Besucherinstanz an den TreeWalker

class Test {
public static void main( String[] args ) throws Exception {

// XML-Dokument aus einem Teststring einlesen
Document testDoc = getDocumentFromString(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"+
"<a>b<c><d/>e</c></a>" );

// Alle Objekte mit Hilfe des TreeWalkers besuchen
new TreeWalker().process(
// 1.Argument: Wurzelknoten des XML-Dokuments
testDoc.getDocumentElement(),
// 2.Argument: Besucherobjekt
new ExampleVisitor() );

}

private static Document getDocumentFromString( String doc )
throws Exception {
return
javax.xml.parsers.DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(
new org.xml.sax.InputSource(
new java.io.StringReader( doc ) ) );
}
}


Die Testklasse gibt für das Beispieldokument <a>b<c><d/>e</c></a> das erwartete Ergebnis aus:
Textknoten: b
Element d
Textknoten: e
Element c
Element a



Gehen wir einmal die in [1] aufgeführten Konsequenzen des Entwurfsmusters Besucher durch und vergleichen sie mit dem hier präsentierten Entwurf:


  • Besucher machen das Hinzufügen neuer Operationen einfach. Das ist hier genauso: In der Tat müssen weder die Elementtypen noch der TreeWalker geändert werden, wenn ein neues Visitor-Objekt entwickelt wird.

  • Ein Besucher führt verwandte Operationen zusammen und trennt sie von Operationen, die nichts mit der Aufgabe des Besuchers zu tun haben. Ist hier offensichtlich ebenfalls erfüllt.

  • Das Hinzufügen neuer Elementklassen ist schwer. Bei uns ist es leichter als im Besucher-Muster. Unsere Visitor-Schnittstelle enthält ja nur eine Methode. Bei Hinzufügen neuer Elementklassen bleibt diese Methode gültig, da sie mit der allgemeinen Elementschnittstelle definiert ist. Der für eine bestimmte Operation auszuführende Code muss natürlich implementiert werden. Der Dispatchmechanismus könnte jedoch leicht so formuliert werden, dass für unbekannte Elementklassen einfach gar keine Aktion ausgeführt wird.

  • Klassenhierarchieübergreifende Besucher. Hier wird festgestellt, dass die besuchten Elemente im Besuchermuster nicht unbedingt von einer gemeinsamen Oberklasse abgeleitet werden müssen. Das ist zwar richtig. Dafür müssen sie aber alle die accept(Visitor)-Methode in ihrer öffentlichen Schnittstelle enthalten. Sie haben also nicht notwendig eine gemeinsame Oberklasse, wohl aber ein gemeinsames Interface. Das schränkt die Verwendbarkeit des Besucher-Musters ein. Für den hier vorgestellten Entwurf gibt es dagegen überhaupt keine Einschränkungen an die Elementklassen. Auch wenn im DOM-Beispiel alle Elementklassen von Node erben, so ist das keineswegs nötig. Die gemeinsame Oberklasse könnte auch Object sein. Die Existenz einer gemeinsamen Element-Schnittstelle ermöglicht es den Besuchern allerdings, ihre Arbeit effizienter auszuführen (weil sonst mit Reflection gearbeitet werden muss).

  • Ansammeln von Zustandsinformationen. Der Besucher kann, während er die Knoten besucht, in seinen Attributen Informationen sammeln. Das ist hier gleichermassen möglich.

  • Aufbrechen der Kapselung. Da der Besucher nur auf die öffentlichen Komponenten der Elementklasse zugreifen kann, kann es vorkommen, dass man gezwungen ist, interne Informationen des Objekts zu veröffentlichen, um dem Besucher seine Arbeit zu ermöglichen. Das ist eine Konsequenz, die in dieser Form für alle Client-Server-Beziehungen von Objekten gilt, ganz unabhängig vom Besuchermuster.



Es gibt also keine Notwendigkeit, den Besucher mit einer accept(Visitor)-Schnittstelle durch die Objekte der Objektstruktur hindurchzuschicken. Es gibt einfachere und naheliegendere Entwürfe, die das Gleiche leisten wie das Besucher-Muster.

[1] Gamma, E; Helm, R.; Johnson, R.; Vlissides, J.: Entwurfsmuster.- Elemente wiederverwendbarer objektorientierter Software, Addison-Wesley Germany 1996.