Das Konzept für ein sauberes Entwurfsmuster
Veröffentlicht am 27. Januar 2021 von | BEx Query Variables | BW/4HANA |
Jetzt werden wir konkreter. Die im vorigen Artikel über die Probleme der BEx Exit Variablen gesehenen Themen werden wir jetzt Stück für Stück angehen. Dazu möchte ich zunächst noch ein paar Begriffe einführen, die ich im Weiteren verwende.
Begriffe
Logik
Mit einer Logik meine ich ein Algorithmus, der für die Ermittlung des Wertes einer Variable notwendig ist. Eine Logik entspricht nicht 1:1 einer Variable, denn häufig verwenden mehrere Variablen die gleiche Logik, auch wenn sie sich in Nuancen unterscheiden. Diese Nuancen können ein Parameter oder eine andere Referenzvariable sein. Um diese 1:1 Zuordnung aufzuheben, bietet sich eine Customizingtabelle für die Zuordnung Variable zu Logik an.
Referenzvariable
Viele Logiken beziehen sich auf eine andere Variable. Diese andere Variable nennen wir Referenzvariable. Diese ist in den meisten Implementierungen fest im Quelltext als Literal hinterlegt. Das ist fehlerträchtig und man sieht erst beim Blick in den Code die tatsächlichen Abhängigkeiten. Auch Systemfelder wie z.B. SY-DATUM
können als Referenzvariablen betrachtet werden. Durch das Herauslösen aus dem Code und eine Erfassung in Customizing ergeben sich mehrere positive Effekte:
- Die Abhängigkeiten können in einem Customizing transparent dargestellt werden
- Die Logiken lassen sich für unterschiedliche Referenzvariablen wiederverwenden
- Zum Testen mit UnitTests können Referenzvariablen gemockt werden
- Eine Zugriffsmethode für Referenzvariablen kann eine einheitliche Fehlerverarbeitung gewährleisten
Parameter
Viele Logiken können generalisiert werden, wenn man einzelne Elemente herauszieht anstatt sie fest im Code als Konstante zu zementieren. Beispielsweise:
- Gib mir Monat X des Jahres
- Berechne Y Tage vor dem aktuellen Datum
Diese Werte für X oder Y nenne ich Parameter. Ich möchte sie ebenfalls aus dem Code in ein Customizing auslagern.
Verarbeitungszeitpunkt
Der Verarbeitungszeitpunkt wird letztendlich vom Parameter I_STEP
gesteuert. Wenn wir die gleiche Logik in unterschiedlichen Variablen zu unterschiedlichen Zeitpunkten nutzen wollen, dürfen wir diesen nicht hartkodiert in den Logiken abfragen. Statt dessen muss dieser ebenfalls in ein Customizing ausgelagert werden.
Das Customizing der Variablen Exits
Das Grundgerüst
Jetzt bleibt noch die Aufgabe, diese unterschiedlichen Objekte im Customizing clever miteinander zu verbinden:
- Variable -
VNAM
- Logik - Eine ABAP Klasse, die das entsprechende Interface implementiert.
- Parameter - Ein Einzelwert. Oder Parameter-Wert Paare wie URL-Parameter.
- Referenzvariable - Eine Referenzvariable.
- Verarbeitungszeitpunkt PAI - Ein Flag, ob die Logik zu diesem Zeitpunkt durchlaufen werden soll
- Verarbeitungszeitpunkt PBO - Dito.
Im einfachsten Falle machen wir das mit einer flachen Tabelle und einem Eintrag für jede Variable. Damit beschränken wir uns auf einen Parameter und eine Referenzvariable pro Variable, was in den meisten Fällen völlig ausreicht.
Die Ausnahme von der Regel - was nun?
Mit Sicherheit wird das aufgezeigte Customizing irgendwann an Grenzen stoßen. Es gibt immer ein paar wenige Customer Exit Variablen, die spezieller sind. Beispielsweise werden bei komplexen Variablen gelegentlich mehrere Referenzvariablen oder Parameter benötigt. Auch kann es vorkommen, dass man zu den unterschiedlichen Zeitpunkten unterschiedliche Logik braucht. Das könnte man mit untergeodneten Customizingtabellen abbilden. Aber ist das wirklich nötig?
Das KISS Prinzip (Keep it simple, stupid!) besagt, das man nur das implementieren soll, was man wirklich braucht. Und im Vorfeld alle möglichen und unmöglichen Fälle mit Customizing abzufangen, erhöht den Aufwand enorm und reduziert die Akzeptanz bei den Nutzern eines Entwurfsmusters. Von daher halten wir uns hier zurück und erweitern gegebenenfalls später.
Es gibt ja in unserem Konzept ja auch eine "Notlösung" für diese Ausnahmen: Man kann eine Klasse für genau eine Variable programmieren, wie man es bislang auch getan hätte....
Die Anzeige
Die Suche nach passenden Variablen sollte so einfach wie möglich sein. Denn nur so kann man wirksam verhindern, dass immer wieder neue Variable mit quasi gleichen Funktion angelegt werden. Dafür braucht man eine einfache Oberfläche, die sowohl das dargestellte Customizing als auch die normalen Eigenschaften der Variablen darstellt. Hierbei sollte es ausreichend Filter- und Sortierfunktionen geben, damit man die passenden Kandidaten schnell identifizieren kann.
Die Klassenstruktur
Implementierung des BAdIs
Wir wollen uns nicht auf die BAdI Filter stützen, sondern einen eigene Verzweigung für die Auswahl der Logik schreiben. Aber trotzdem müssen wir den BAdI einmal implementieren, eben um dort unsere Verzweigung unterzubringen. Dies geschieht in der Klasse ZCL_VFW_BADI_IMP
.
Die Logik, die in der Methode IF_RSROA_VARIABLES_EXIT_BADI~PROCESS
abläuft ist denkbar einfach:
- Statische Factory Methode
GET_INSTANCE
der KlasseZCL_VFW_LOGIC_FACTORY
nach der passenden Instanz für die Logik fragen - Logik-Instanz mit Parametern aufrufen
Die Factory Klasse kennt das Customizing und entscheidet darüber, welche Klasse gewählt wird. Falls keine Logik ausgeführt werden soll, so wird eine Instanz von ZCL_VFW_LOGIC_DUMMY zurückgegeben, die nichts macht.
UML-Klassendiagramm
Die Schnittstelle der Logik
Ich habe in dem UML-Klassendiagramm den Namen GET_RANGE
für die Methode des Interfaces ZIF_VFW_LOGIC
gewählt, zum Einen weil es beschreibt, was diese Methode machen soll und zum Anderen um klarzustellen, dass es nicht 1:1 um die Methode PROCESS des BAdIs handelt.
Aufräumen in der Parameterliste
Die ganzen Parameter des BAdIs wollen wir nicht in die Signatur mit aufnehmen. Statt dessen werden sie in Instanzvariablen der Superklasse der konkreten Implementierungen gehalten. Alle notwendigen Informationen für die Implementierung eines Variablenexits erlangen wir über die folgenden Zugriffsmethoden - und -klassen:
- Werte aus anderen Variablen werden über ein Objekt der Klasse
ZCL_VFW_VARIABLE
angesprochen, dass die Daten der Referenzvariable im Customizing kapselt. Dieses liefert uns dann ggf. die notwendigen Daten und wir müssen nicht selber in der I_T_VAR_RANGE rumferkeln. Mit der Methode REF_VAR( ) bekommen wir die entsprechende Instanz. - Die Parameter für unsere Logik liefert die Methode DEF_PARAM( )
Die Rückgabe eines Variablenexits besteht entweder aus der Tabelle RT_RANGE oder aus einer Ausnahme, um Fehlersituationen mitzuteilen. Damit reduziert sich die Definition der Methode GET_RANGE auf das folgende:
METHODS get_range RETURNING VALUE(rt_range) TYPE rsr_t_rangesid
RAISING zcx_vfw
cx_rs_error .
Um die Implementierung zu erleichtern, legen wir uns jetzt noch eine Reihe von Methoden an, die wir in dem folgenden Beitrag zeigen. Diese sind allgemein, und können mit etwas Anpassung auch außerhalb des Frameworks eingesetzt werden.
Das Ergebnis unserer Logik ist stets eine RANGE. Um diese zusammenzubauen, erben wir aus der Superklasse ebenfalls ein paar hilfreiche Methoden:
- CREATE_SINGLE_VALUE_RANGE( IV_VALUE ) - Das ist der am häufigsten vorkommende Fall: Es soll genau ein Wert zurückgegeben werden. Standardmässig INCLUDE/EQ
- CREATE_SINGLE_INTERVAL_RANGE( IV_LOW, IV_HIGH) - Erzeugt ein Intervall (BT), standardmässig mit INCLUDE
- CREATE_MULTI_VALUE_RANGE( IT_VALUES ) - Erzeugt eine Liste mit Werten. Per Default mit INCLUDE/EQ
Mit diesen drei Methoden decken wir schon weit über 95% der Fälle ab. Und jetzt wollen wir mal ein einfaches Beispiel damit anschauen. Ein sehr typisches Bild für eine einfache Exit-Variable:
"Klassischer" Exit
CASE I_VNAM.
WHEN '<Variable 1>'.
IF I_STEP = 2.
* Ermittlung Ende Datum aus Selektion für <Variable 2>
READ TABLE I_T_VAR_RANGE INTO LS_VAR_RANGE WITH KEY VNAM = '<Variable 2>'.
IF SY-SUBRC = 0.
CLEAR L_S_RANGE.
L_S_RANGE-SIGN = 'I'.
L_S_RANGE-OPT = 'EQ'.
L_S_RANGE-LOW = LS_VAR_RANGE-HIGH.
APPEND L_S_RANGE TO E_T_RANGE.
ENDIF.
ENDIF.
Alternative mit Zugriffsmethoden
Die gleiche Logik wird mit den Zugriffsmethoden wesentlich einfacher. Dabei fällt auf, dass hier auch einige Informationen fehlen.
DATA(lv_ref_high) = ref_var( )->get_high_value( ).
e_t_range = create_single_value_range( lv_high ).
Was nicht mehr im Code steht:
- Für welche Variable soll der Exit ausgeführt werden?
- Zu welchem Zeitpunkt soll der Exit durchlaufen werden?
- Wie heisst die Variable, aus der gelesen werden soll?
Diese Informationen stehen nicht mehr im Code, sondern werden per Customizing festgelegt.