Freitag, 3. April 2009

Externe Unterprogrammaufrufe

Unterprogramme sind - nach Macros - der elementarste Entwurf wiederverwendbarer Programmeinheiten. Immerhin haben sie im Vergleich zu Macros einen echten lokalen Kontext (Stack). Ihr Schnittstellenkonzept ist sicher veraltet — Unterprogramme arbeiten mit Positionsparametern und machen kein Type Casting — aber sie haben den Vorteil, dass sie sehr schnell zu implementieren sind. So alt das Unterprogrammkonzept ist – in ABAP wird es niemals sterben, da es fest in die Syntax von komplexeren Konstrukten eingebaut ist:

  • Für den parallelisierten Funktionsbausteinaufruf mit asynchronem RFC werden Unterprogramme für die Rückmeldung der Tasks benötigt. Das geschieht mit dem folgenden ABAP-Befehl:
    call function ... 
    starting new task ...
    performing ... on end of task.

    Gäbe es diese Konstruktion nicht, könnte man keine lastabhängige Parallelisierung programmieren!
  • Der perfom on commit bietet eine bequeme Möglichkeit, Verbuchungslogik in der aktuellen Funktionsgruppe zu hinterlegen (wenn auch die Verbuchung mit Funktionsbausteinen in einem echten Update Task meist vorzuziehen ist).

Es gibt keinen Grund, Unterprogramme oder auch Macros in Programmierrichtlinien zu verwerfen. Man sollte überhaupt nichts verwerfen, was dem Entwickler Möglichkeiten bietet, seinen Code wiederverwendbar zu gestalten. Selbstverständlich sind in den vergangenen zwanzig Jahren bessere Möglichkeiten der Wiederverwendung entwickelt worden. Aber ein Entwickler mag seine Gründe haben, gewisse Funktionen nicht in einer Klasse oder Funktionsgruppe, sondern in einem Unterprogrammpool anzubieten. Dann sollte man nicht die Verwendung von Unterprogrammen kritisieren, sondern froh sein, dass überhaupt etwas in wiederverwendbarer Form vorliegt. Mehr Gewicht sollte man auf die Vermeidung von globalen Daten sowie von wirklich obsoleten Sprachelementen wie dem tables- oder common part-Statement legen. Unterprogramme helfen auch, bestehende monolithische Programme in der Wartung mit vernünftigem Aufwand zu refaktorisieren. Ein Programm, das in Unterprogramme gegliedert ist, ist besser änderbar (also korrigierbar und erweiterbar) als der klassische Monolith, wie wir ihn von manchen älteren und nicht lernbereiten Programmierern noch kennen.

"Ich schreib' da mal einen ABAP" sagte man vor zwanzig Jahren, wenn man einen solchen Monolithen im Auge hatte: Ein einziges Programm, das bei Ausführung zeilenweise bis zum Schluss abgearbeitet wird. Ein solcher ABAP bietet dem User zuerst ein paar Selektionsoptionen an (die Details stehen in einem schwergewichtigen "Entwicklungsantrag" und sind Gegenstand zäher Verhandlungen), danach führt er mit dem Befehlssatz von Open SQL ein paar Selektionen auf die Datenbank aus — als besonders elegant empfanden gewisse Schulen den zehnfachen inneren Join auf alle involvierten Datenbanktabellen — danach geht es im Hauptspeicher weiter: es folgt ein Kampf mit internen Tabellen (natürlich keine Hashs oder implizit sortierte Tabellen, sondern nur Standardzugriffe, also lineare Suche), die mit LOOP und READ TABLE auf ziemlich holprige Weise ausgewertet werden, und schliesslich kommt die Ausgabe in eine ABAP-Spoolliste, wobei die höchste Herausforderung darin besteht, sich bei den Spaltennummern und Feldlängen im WRITE AT-Statement nicht zu verzählen. Alle Datenobjekte sind selbstverständlich global. Aufrufe in andere Programme gibt es nicht. Selbstverständlich wird das "elegante" TABLES-Statement verwendet, um für eine Datenbanktabelle gleich eine Workarea zu deklarieren. Und selbstverständlich ist keine Codezeile eines solchen "ABAPs" wiederverwendbar. Denn die Sprachelemente form und endform gelten als überflüssiger, von irgendwelchen Designaposteln verkündeter Schnickschnack, die keine Ahnung von echter, harter Programmierung haben.

Seien wir also froh, dass es Unterprogramme gibt: Denn sie erlauben es, selbst diese Urform der ABAP-Programmierung, den ABAP-Report, modular zu gestalten und bringen lokale Kontexte in die Programmausführung. Dank Unterprogrammen kann man Reports schreiben, die ausser den Parametern, Selektionsoptionen und Textelementen keine globalen Daten enthalten und die eine Reihe von klar definierten Operationen ausführen, die jede für sich möglicherweise auch in anderen Zusammenhängen verwendbar ist.

Aber bei der Verwendung von Unterprogrammen in anderen Zusammenhängen, dem "externen Perform", gibt es einen Fallstrick, den uns die wirklich gefährlichen Anweisungen tables und common part bereiten. Wenn diese Anweisungen in den Programmierrichtlinien verboten werden, sind externe Unterprogrammaufrufe völlig unproblematisch. Lediglich mit globalen Daten gibt es das bekannte Reentrance-Problem, dass ihr Zustand unbestimmt ist und von der Vorgeschichte des Aufrufs abhängt: Dies ist aber ein Problem, das man ebenso in Klassen und Funktionsgruppen hat, es liegt an den globalen Daten selbst und nicht an der verwendeten Modularisierungsform.

Zur Demonstration des Problems möchte ich Ihnen den folgenden Unterprogrammpool zdemo_ext_perf_a vorstellen:
report  zdemo_ext_perf_a.
tables: nast.
*
form write changing ev_spras type spras.
ev_spras = nast-spras.
endform. "write
*
form irgendeine_form.
endform.

Dieser Unterprogrammpool werde nun von folgendem Hauptprogramm teils direkt, teils indirekt auf dem Weg über eine Funktionsgruppe angesprochen:
report  zdemo_ext_perf_main.
tables: nast.
parameters: p_call_f as checkbox default space.
data: gv_spras type spras.
nast-spras = sy-langu.
if p_call_f ne space.
call function 'SD_SALES_DOCUMENT_PERFORM'
exporting
perform = 'IRGENDEINE_FORM'
in_program = 'ZDEMO_EXT_PERF_A'.
endif.
perform write(zdemo_ext_perf_a) changing gv_spras.
write: / 'NAST-SPRAS in ZDEMO_EXT_PERF_MAIN:', nast-spras.
write: / 'NAST-SPRAS in ZDEMO_EXT_PERF_A :', gv_spras.

Der Funktionsbaustein SD_SALES_DOCUMENT_PERFORM ist Teil der Komponente SAP_APPL, macht aber nichts anderes als einen dynamischen Unterprogrammaufruf der angegebenen Routine. Sie können dieses Beispiel auch leicht auf einem System ohne SAP_APPL (zum Beispiel einem BW-System) nachvollziehen, dann benötigen Sie in einer Test-Funktionsgruppe einen Funktionsbaustein Z_EXTERNAL_PERFORM mit der einzigen Anweisung perform (perform) in program (in_program)..

Wie verhält sich das Programm nun, je nach Wert der Parameters p_call_f? Wenn man ABAP nicht kennt, würde man vermuten:

"Die Routine irgendeine_form macht ja nichts. Also spielt es auch keine Rolle, ob sie ausgeführt wird oder nicht. Das globale Feld NAST-SPRAS im Hauptprogramm wird zu Beginn der Programmausführung auf die Anmeldesprache gesetzt. Es sollte also immer die Anmeldesprache enthalten, egal ob irgendeine_form nun aufgerufen wird oder nicht. Das Feld gv_nast wird in der Routine write des Unterprogrammpools von dessen Exemplar der Struktur nast abgeholt und sollte daher, da letzteres an keiner Stelle des Programms verändert wurde, immer den Initialwert haben."

Leider verhält sich das Programm nur dann wie erwartet, wenn man das Feld p_call_f ankreuzt. Wenn man es nicht ankreuzt, enthält am Ende auch die Variable gv_spras die Anmeldesprache!

Dieses fragile, anstössige Programmverhalten hat seine Ursache in der tables-Anweisung. Wenn man die Anweisung tables: nast durch die ansonsten identische Anweisung
data: nast type nast.
ersetzt, verhält sich das Programm so, wie es jeder unbefangene Programmierer erwarten würde!

ABAP legt den mit tables deklarierten (und damit automatisch globalen) Arbeitsbereich wenn möglich nur einmal an. Sobald es den externen Perform antrifft, lädt es den Unterprogrammpool, bemerkt, dass auch dort eine Struktur nast via tables deklariert ist und verwendet den Originalarbeitsbereich des rufenden Programms auch im Unterprogramm. Wenn irgendeine_form jedoch auf dem Umweg über den Funktionsbaustein aufgerufen wird, wird der Unterprogrammpool nicht dem Hauptprogramm zdemo_ext_perf_main, sondern der Funktionsgruppe zugeordnet, in der sich der Baustein SD_SALES_DOCUMENT_PERFORM befindet.

Das bedeutet, in diesem Fall haben wir ein zweites Exemplar der Struktur nast im Hauptspeicher, und das Programm verhält sich so, wie es der unbefangene Programmierer erwarten würde.

Die Wurzel dieses Problems ist aber nicht der externe Unterprogrammaufruf, sondern das für die Anweisung tables implementierte Data Sharing. Wenn man also auf der Suche nach bösen Dingen ist, die man in seinen Programmierrichtlinien verbieten möchte, sollte man tables und ebenso die common parts auf diese Liste setzen – nicht aber form und endform.

Keine Kommentare :