Donnerstag, 6. Januar 2011

Domänenspezifische Programmierung in ABAP

Mit dem Thema domänenspezifischer Sprachen (DSL) habe ich mich hier schon beschäftigt und werde es noch häufiger tun, da ich es für ein zukunftsweisendes Feld in der Programmierung halte.

Die einfachste und dem ABAP-Entwickler geläufigste Form der domänenspezifischen Programmierung ist es sicher, Customizingtabellen zu definieren und für die Steuerung des Systems zu verwenden [1]: In einer gut definierten Customizingtabelle sind z.B. die möglichen Festwerte einer (DDIC-)Domäne unmittelbar in der Fachsprache der Anwendung nachvollziehbar.

Manchmal ist der Rahmen einer Datenbanktabelle für die gewünschte Form der Steuerbarkeit des Systems unzureichend: Man möchte z.B. flexibler sein. Vielleicht ist das relationale Datenmodell, das ausschliesslich Schlüssel/Wert-Beziehungen ableitet, für die Abbildung der gewünschten Steuerung unpassend. Oder man möchte die Steuerung des Programms im Quelltext der Klasse hinterlegen, die die Logik ausführt, um sie nahe beim Code selbst zu haben.

Eine Lösung ist es dann, die Logik so in einer Methode zu programmieren, dass der Quelltext dieser Methode radikal auf den Anwendungskontext beschränkt ist - die Programmierung selbst läuft gewissermassen hinter den Kulissen. Das kann man in ABAP mit Macros und der Doppelpunkt/Komma-Syntax erreichen.[2]

Ein Beispiel. In einer Klasse namens ZCL_GDBW_SELECTOR hatte ich die Aufgabe implementiert, Selektionen von verschiedenen externen Datenbanktabellen auszuführen, die im wesentlichen ähnlich strukturiert sind. Die Details, in denen sie voneinander abweichen, habe ich in einer statischen Hashtabelle festgehalten, die beim Laden der Klasse aufgebaut wird. Das Auffüllen dieser Tabelle ist programmiertechnisch ein harmloser, geradezu langweiliger Vorgang. Die insert-Anweisungen sowie die Zuweisungen an die Felder des Arbeitsbereichs gehören zum "Rauschen" der Sprache (syntactic noise): Notwendig zwar für die Maschine, für den Menschen aber erschweren sie den Blick auf das Wesentliche. In diesem Fall habe ich diese Anweisungen in Macros verborgen, so dass nur die wesentlichen, die steuernden Parameter im Quelltext der Methode stehenbleiben:

method build_info_on_tables_and_conns.
data: ls_table_data type ty_table_data,
ls_con_data type ty_con_data.

_add_table_data
* Connection Node Zusatz
* Databox Tabellenname
'GDBW' 'gdnf1bw' ' ' :
'INBOX' 'V_INFO_DATEN_RECV',
'OUTBOX' 'V_INFO_DATEN_SEND',
'ALL' 'V_INFO_DATEN_ALL',
'ARCH' 'V_INFO_DATEN_SEND_ARCH'.

_add_table_data
'MVN' 'gdnf2bw' 'X' :
'INBOX' 'V_INFO_DATEN_RECV_NF',
'OUTBOX' 'V_INFO_DATEN_SEND_NF',
'ARCH' 'V_INFO_DATEN_SEND_ARCHIV'.

* ...

* Q-Verbindungen
_copy_table_data_from_to :
'MVN' 'QMVN',
'KMVN' 'KQMVN',
* ...


Der "Domänenexperte" ist in diesem Fall jemand, der sich mit den externen Datenbanken und ihren Verbindungen zum SAP-System auskennt.

Man beachte in diesem Beispiel auch die Verwendung der Doppelpunkt/Komma-Notation: Der vor dem Doppelpunkt stehende Teil des Statements wird der Reihe nach als immer gleicher Vordersatz für alle durch Komma getrennten, nach dem Doppelpunkt stehenden Hintersätze verwendet. Auch dies erleichtert den Blick auf das Wesentliche. Alternativ hätte ich auch die Vordersätze n-mal hinschreiben müssen. Die Doppelpunkt/Komma-Notation und Macros haben mir in diesem Beispiel zu einer radikalen Durchsetzung von Don't repeat yourself verholfen. Keine Information muss mehrfach hingeschrieben werden, auch keine Programmzeile.

Ein anderes Beispiel aus meinem BSP-Thread Eingabebereitschaft in deklarativer Syntax zeigt den Einsatz dieser Technik, um zu steuern, wann welche Buttons in einer Anwendung aktiv, inaktiv und wann sie unsichtbar sind - abhängig vom Transaktionsmodus (Anzeigen, Ändern, Anlegen). Auch hier wieder die Reduktion auf das Wesentliche. Die tatsächliche Manipulation des Feldstatus erfolgt hinter den Kulissen, verborgen im Macro:

     _set_button_state_per_mode : 
* FCODE \ CREATE CHANGE DISPLAY
'ACTU' active active disabled,
'SAVE' active active disabled,
'UNDO' active invisible invisible,
'CANCEL' invisible active disabled,
'PRINT' invisible active active.


Wieder erlaubt es die Anwendung, die Steuerung in Tabellenform zu notieren. Das bedeutet, für diese Art von Steuerungen wäre es im Prinzip auch möglich, kleine Customizingtabellen zu definieren, die dasselbe leisten.[3]

In beiden Fällen habe ich bewusst keine Customizingtabelle gewählt, schon aus Gründen der Sparsamkeit: Wenn ich alle Fallentscheidungen und Steuerungen dieser Art in Customizingtabellen ausprägen würde, würde ich das System mit Customizingtabellen geradezu fluten.

Ausserdem hätte ich dann nicht nur einen Ort, sondern zwei, die für die Steuerung zuständig sind: Um später in Problemfällen zu verstehen, warum ein bestimmter Status gewählt wird, muss ich mir die Einträge der Customizingtabelle und das Programm ansehen, das sie auswertet.

In der Macro-Notation habe ich dagegen alles an einem Ort, im selben Programmobjekt. Das Macro dient mir nur dazu, die Programmlogik innerhalb des Objekts von den eigentlich wichtigen steuernden Werten zu trennen, denn diese sind das Wesentliche. Die hinter den Kulissen laufende Programmlogik ist für alle Buttons die gleiche. Einmal programmiert - und mit einem Unit Test abgesichert - wird man in der Wartung nie mehr mit dem Code zu tun haben. Wohl aber mit den steuernden Werten: Es kann zum Beispiel ein neuer Button dazukommen. Oder ein neuer Auftraggeber findet es geschmackvoller als sein Vorgänger, Buttons grundsätzlich auf invisible zu schalten, wenn sie nicht verwendbar sind. Das sind typische Fälle von Änderungen - und immer müssen nur die Werte im obigen Codeabschnitt angepasst oder erweitert werden.

Macros und die Doppelpunkt/Komma-Syntax verhelfen also in ABAP zur Erstellung von internen DSLs, also Sprachen, die eingebettet in den normalen ABAP-Kontext verarbeitet werden.

Weitere Möglichkeiten ergeben sich durch die in ABAP integrierten XSLT- und JavaScript-Prozessoren. In meinem BSP-Framework stellt die in der Datei config.xml ausgeprägte Flow Logic eine DSL für die Ablaufsteuerung der Views in einer BSP-Applikation dar. Sie wird mit einer einzigen ABAP-Anweisung in ein Set von internen Tabellen gewandelt (siehe den Konstruktor der Klasse zcl_mvc_framework):
* config.xml in ABAP-Daten wandeln
call transformation (config_to_abap) source xml lv_sxml
result application = ls_application
tree = tree
controller = controllers
model = models.

In anderen Fällen kann es sinnvoller sein, statt XML die JavaScript-Objektnotation (JSON) zu verwenden. Denn XML produziert auch selbst wieder einen beachtlichen syntactic noise. Die JavaScript-Daten, die man z.B. in einem Standardtext (SO10) ablegen könnte, können entweder mit dem von mir in diesem Blog vorgestellten JSON-Parser oder mit dem in ABAP zugänglichen JavaScript-Interpreter cl_java_script eingelesen werden. Steuernde, sitzungsübergreifend relevante Informationen sollten, nachdem sie in ABAP-Daten gewandelt wurden, auf jeden Fall auf dem Application Server gepuffert werden.[4]

Aber auch JSON ist nicht ganz frei von syntactic noise. Der Anwender - und der DSL-Programmierer - will ja nicht die vielen geschweiften Klammern und Anführungszeichen sehen, sondern auf den ersten Blick bereits die wesentlichen Informationen. Dann kommen wir allmählich in die Gefilde einer "echten", externen DSL. Die Notation der Daten wird für ein bestimmtes spezifisches Problem ad hoc entworfen. Ein typisches Beispiel einer solchen DSL ist ein CSS-Sheet. Die CSS-Notation ist optimal auf genau einen Zweck zugeschnitten: Das Erscheinungsbild von HTML-Elementen zu steuern.

Eine externe DSL muss in ein semantisches Modell übersetzt werden. Für jede Notation wird ein eigener Übersetzer benötigt. Das klingt nach einem beträchtlichen Extraaufwand, da mit einem solchen Übersetzer ja eine weitere Softwarekomponente benötigt wird. Der Aufwand, einen Parser für DSLs oder an einen Parser angekoppelte Übersetzer zu schreiben, wird aber tendenziell überschätzt. Je eingeschränkter die Verwendung der DSL ist – und ein eingeschränkter Sprachumfang gehört zu den Charakteristika einer DSL – umso einfacher wird es auch, sie zu parsen, vor allem wenn man mit testgetriebener Entwicklung arbeitet (siehe zu diesem Thema den schönen Bliki ParserFear von Martin Fowler). Parser sind geradezu ein Paradebeispiel für die Effizienz testgetriebener Entwicklung, da sie so wenig Abhängigkeiten haben: Sie wandeln nur einen textförmigen Input in einen Syntaxbaum oder irgendein semantisches Modell um. Es bestehen keine weiteren Abhängigkeiten, etwa von Datenbanktabellen. Auch müssen ausser Basisfunktionen - etwa im Bereich der Stringverarbeitung - keine weiteren API's aufgerufen werden.

Für die Arbeit auf der Stringebene stehen uns in ABAP darüberhinaus die mächtigen regulären Ausdrücke zur Verfügung. Das dem eigentlichen Parsen oft vorangestellte Tokenizing lässt sich häufig bereits durch eine Folge von einfachen replace regex-Konstrukten realisieren.

Zur Illustration mag eine DSL zur Formulierung von sogenannten Verpackungsregeln dienen. Mit einer Verpackungsregel kann gesteuert werden, wie der Inhalt von mehreren SAP-Lieferungen auf einzelne Paletten aufzuteilen oder zu gruppieren ist. Ich habe für solche Verpackungsregeln einen kleinen Editor gebaut. Die Regeln werden von unserem SAP CC zum Test verschiedener Prozessfälle gepflegt und verwendet. Hier ein Beispiel:

1. Palette ( 
2. Palette (
1. Lieferung, 1. Pos.
)
3.Palette (
1. Lieferung, Rest
)
4.Palette (
2. Lieferung, 1. Pos.
2. Lieferung, 2. Pos., 50%
)
5.Palette (
2. Lieferung, Rest
)
)

Hier wird also eine hierarchische Handling Unit mit dem Inhalt von zwei Lieferungen bestückt, wobei eine Gruppenbildung von Lieferpositionen oder Teilmengen von Lieferpositionen möglich ist. Eine solche Regel kann mit einem Namen versehen und zur Laufzeit in einem Testfall zum automatischen Verpacken herangezogen werden. Ein Parser validiert die syntaktische Richtigkeit der Regel bereits zur Designzeit und erstellt ein semantisches Modell in Form von internen Tabellen. Dieses semantische Modell wird dann zur Laufzeit auf die real vorliegenden Lieferungen angewendet.

Zur Illustration folgt hier der Tokenizer für diese DSL, der vollständig mit regulären Ausdrücken notiert ist. Er erzeugt aus den vom Benutzer eingegebenen Regeln eine Folge von wohldefinierten, normalisierten Tokens in Form einer stringtab:

  lv_norm = iv_code.

translate lv_norm to upper case.
replace all occurrences of regex :

* Komma wird wie ein Trenner behandelt
','
in lv_norm with gc_space,

* Normierung der Schlüsselwörter
'\bLIEF(\.|ER\.|ERUNG)?'
in lv_norm with ' LIEF ',
'\bPOS(\.|ITION)?'
in lv_norm with ' POS ',
'\bPAL(\.|ETTE)?'
in lv_norm with ' PAL ',
'\b(ALLES|REST)'
in lv_norm with ' REST ',

* Mehrfache Leerzeichen, Zeilenumbrüche etc. auf je 1 Space normieren
'\s+'
in lv_norm with gc_space,

* Menge mit Postfix MG
'([\d.]+) ?(CU|%)'
in lv_norm with '$1 MG $2',

* Keywort folgt nach Nummer
'(\d+)\. ?(LIEF|POS|PAL)'
in lv_norm with '$1 $2',

* 2x, da Tokens aneinanderhängen können: "(REST)"
'([\d.]+|\(|\)|REST|LIEF|POS|PAL)(\(|\)|REST|LIEF|POS|PAL)'
in lv_norm with '$1 $2',
'([\d.]+|\(|\)|REST|LIEF|POS|PAL)(\(|\)|REST|LIEF|POS|PAL)'
in lv_norm with '$1 $2'.

shift lv_norm left deleting leading space.
split lv_norm at space into table et_code.

Die Tokentabelle wird nun in einem folgenden Schritt in eine Tabelle von Tokens gewandelt; diese Tabelle enthält für jeden Token ein allfällig erfordertes numerisches Argument.
  loop at lt_code into lv_token.
case lv_token.
when '('.
_add_token ct_tokens bra space.
when ')'.
_add_token ct_tokens ket space.
when 'REST'.
_add_token ct_tokens rest space.
when 'PAL'.
_add_token ct_tokens pal lv_number.
when 'LIEF'.
_add_token ct_tokens lief lv_number.
when 'POS'.
_add_token ct_tokens pos lv_number.
when 'MG'.
_add_token ct_tokens meng lv_number.
when 'CU' or '%'.
_add_token ct_tokens einh lv_token.
when others.
find regex '(\d+)' in lv_token submatches lv_number.
if sy-subrc ne 0.
* Fehlermeldung - unbekanntes Token lv_token
_raise_syntax lv_token text-003.
else.
* Kein clear lv_number!
continue.
endif.
endcase.
clear lv_number.
endloop.

In dieser Form kann sie nun vom Builder Befehl für Befehl abgearbeitet werden, um das gewünschte semantische Modell zu erzeugen.

Wenn wir in ABAP einen Parsergenerator aus der mächtigen Familie der Parsing Expression Grammars (PEG) zur Verfügung hätten, etwa OMeta, so liesse sich ein Parser für diese DSL mit wesentlich weniger Codezeilen notieren. Das folgende OMeta-Objekt dient zugleich als Parser, als Builder des semantischen Modells (hier in Form eines AST mit verschachtelten JavaScript-Arrays) wie auch zur Definition der verwendeten Grammatik. Darüberhinaus ist es auch lesbarer als die oben ausprogrammierte Folge von regulären Ausdrücken, die demselben Zweck dient.

ometa HandlingUnitDefinition <: Parser { 
ordnum = digit+:ds '.' -> parseInt(ds.join('')),
// Vollständige Lieferung mit allen Pos. bekommt Posnr 0:
dlv = ordnum:n ( "Lieferung" | "Lief" | "LF" ) -> ["DLV", n, 0],
// Eine SSCC (Serial Shipment Container Code = ID einer Handling Unit)
sscc = ordnum:n ( "Palette" | "Pal" | "SSCC" ) -> ["SSCC", n],
// Ausdruck für eine Position
item = ordnum:n ( "Position" | "Pos" | "POS" ) -> ["POS", n],
// Lieferposition
dlvItem = dlv:d "," spaces item:p -> ["DLV",d[1],p[1]],
// Rekursiver Aufbau einer HU in beliebige Tiefe:
contentPart = dlvItem | dlv | hu | sscc,
// Regel für den Inhalt einer HU
content = contentPart:x (spaces contentPart)*:xs
-> [x].concat(xs),
hu = sscc:s spaces "(" spaces content:c spaces ")"
-> ["HU", s[1],c],
// expr definiert die gesamte Regel und damit die DSL:
expr = spaces hu:first (spaces hu)*:more
-> [first].concat(more)
}


Diese OMeta-Grammatik ist fast vollständig äquivalent zu dem von mir ausprogrammierten ABAP-Code, von dem ich oben einen Auszug gezeigt habe! Das zeigt Perspektiven, in die man gehen könnte, um DSLs noch schneller und einfacher zu formulieren und zu definieren. Da es eine JavaScript-Implementierung von OMeta gibt, könnten sich Grammatiken auch in der ABAP-Welt verwendet werden - denn es gibt ja den in der Klasse CL_JAVA_SCRIPT verschalten JavaScript-Interpreter. Die sogenannten semantischen Aktionen, die nach jeder Regel hinter dem Pfeil-Operator -> folgen, könnten darüberhinaus Callbacks in ABAP enthalten. Dass der CL_JAVA_SCRIPT ressourcenintensiv ist und dass eine interpretierte Sprache nicht besonders schnell ist, gibt auch keinen Grund zur Beunruhigung. Denn das Ergebnis des Parse-Vorgangs wird man sowieso auf dem Application Server puffern, da es normalerweise für alle Benutzer identisch ist.

Statt ein semantisches Modell in Form von internen Tabellen zu füllen, kann ein DSL-Parser natürlich auch verwendet werden, um ABAP-Programmcode zu erzeugen. Auch hierfür halten unsere SAP-Systeme eine Reihe von Beispielen bereit. Diese gedenke ich in künftigen Blogs zu dokumentieren.


[1] Dass die Einträge von Customizingtabellen als ein "DSL-Programm" aufgefasst werden können, mag Verwunderung oder gar Befremden hervorrufen: Ich hatte diesen Standpunkt bereits in meinem Blog Software-Steinzeit erläutert.

[2] Macros werden im Debugger wie eine Ausführung behandelt, man kann nicht in ein Macro hineindebuggen. Daher empfiehlt es sich, das Macro ausser in den einfachsten Fällen nur zum Aufruf von Methoden zu verwenden und die Logik des Macros in diesen Methoden zu programmieren. Das Macro hilft dann, die beabsichtigte Steuerung in einer besser lesbaren Form hinzuschreiben.

[3] Um Tippfehler zu vermeiden, habe ich in diesem Fall die möglichen Zustände eines Buttons in Form von Konstanten ausgeprägt. Hinter den Kulissen gibt es Konstanten gc_state-active, gc_state-invisible usw. So hält mich bereits der ABAP Syntaxchecker davon ab, ungültige Zustandswerte einzugeben.

[4] Nur in Entwicklungssystemen sind Pufferungen normalerweise lästig: Der Entwickler ist mehr daran interessiert, bei seinen Entwicklertests stets die aktuellsten Daten zu sehen, die er für seine Tests auch ständig ändert. Im Entwicklungssystem zu puffern, kann lästige Debuggingsitzungen verursachen. Dagegen ändern sich die steuernden Daten im Produktivsystem eher selten, während andererseits die höchstmögliche Performance angestrebt werden sollte: Dort also unbedingt puffern! Schliesslich sollen nicht hunderte von Benutzern in ihren Sitzungen alle dieselbe Konfigurationsdatei von der Datenbank oder vom Dateisystem einlesen müssen. Aus demselben Grund sind Customizingtabellen im DDIC mit einem Click - ohne zusätzlichen Programmieraufwand - auf dem Applikationsserver pufferbar.

Kommentare :

Noddi hat gesagt…

Hallo Herr Plantiko,

bin gerade auf ihre Blogsite gestossen. Sehr interessanter Artikel. Programmiere selbst sehr gerne mit Makros, zuletzt um ein komplexes Regelwerk im Coding für den Kunden einfach ergänzbar zu machen.
Ich denke, die Ausgestaltung (Makro, Customizing) hängt letztlich auch von der ausführenden bzw. das System wartenden Person ab (Entwickler, Berater, Fachabteilung).
Vielen Dank für die vielen informativen ABAP-Artikel.

Viele Grüße
Noddi

Rüdiger Plantiko hat gesagt…

Hallo Noddi,

vielen Dank für die positive Rückmeldung!

Auch am Ende des Jahres bewege ich noch das Thema "Domänenspezifische Programmierung in ABAP". Mache gerade einige Experimente mit BRF+ - man wird vielleicht demnächst hier davon lesen können :-)

Freundliche Grüsse,
Rüdiger