Die Tage des klassischen, prozeduralen ABAP-Codes sind längst gezählt. Moderne ABAP-Lösungen verlassen sich immer mehr auf das objektorientierte Programmierparadigma. In seiner aktuellen Version hat SAP®s primäre Programmiersprache ABAP bereits eine Vielzahl der technischen Konzepte integriert, die ein Entwickler im objektorientierten Umfeld nicht missen möchte: Datenkapselung, Vererbung, Generalisierung und Polymorphismus sind inzwischen fester Bestandteil des ABAP-Repertoires. Auch einige tiefergehende Techniken wie das Konzept der „Friends“ hat sich ABAP vor langer Zeit von C++ geborgt und verinnerlicht. Ebenso haben Events, wie man sie zum Beispiel aus C# kennt, ihren Weg in die ABAP-Welt gefunden.
Nichtsdestotrotz gibt es noch einige Features, die ABAP im Gegensatz zu OOP-Vorreitern wie Java und C# noch nicht adaptiert hat. Da beispielsweise keine Generics zur Verfügung stehen, müssen Entwickler meist noch auf etwas lockerer typisierte Objekte und Type-Casting zurückgreifen. Auch gibt es keine Möglichkeit, anonyme Methoden zu implementieren. In diesem Artikel wollen wir uns letztere Problematik einmal genauer anschauen.
Inhalt:
ABAP-Implementierung mit gegebenen Mitteln
Ein ABAP-Framework für anonyme Klassen
Das Problem unter der Lupe
In anderen OOP-Sprachen ist das Feature der anonymen Methoden und Funktionen lange etabliert. PHP bietet die sogenannten Callables und Closures. In C# finden wir Delegates und Lambda-Ausdrücke wieder, die uns die Verwendung und Übergabe anonymer Funktionen erlauben. Java löst das Problem ähnlich mit funktionalen Interfaces, Lambdas und anonymen Objekten.
Betrachten wir zunächst ein Anwendungsbeispiel in Java. Gegeben sei folgende Klasse „Customer“, welche einen Kunden in unserem System beschreibt:
Neben Eigenschaften wie einer eindeutigen ID sowie einem Vor- und Zunamen, kapselt eine Kunden-Instanz zudem eine Sammlung von Auftragsnummern (Order Numbers). In diesem einfachen Beispiel soll dies eine Historie von Aufträgen darstellen, die diesem Kunden zugeordnet sind. Über die Methode printOrderHistory können die Auftragsnummern aus der Historie zeilenweise ausgegeben werden.
Nun erhalten wir die Anforderung, eine Funktion bereitzustellen, mit welcher sich die Auftragshistorie anonymisieren lässt. Die Auftragsnummern sollen abgewandelt werden, sodass sie keinem konkreten Auftrag mehr zugeordnet werden können. Die Art und Weise der Anonymisierung spielt zunächst keine Rolle und könnte sich je nach Anwendungsfall ändern. Ideal wäre es, eine Methode in unserer Customer-Klasse bereitzustellen, welche eine Transformationsfunktion entgegennehmen kann. Genau das haben wir im obigen Beispiel mit einem sogenannten funktionalen Interface implementiert.
Die Methode anonymizeOrderHistory nimmt ein abstraktes Objekt zur Anonymisierung der Auftragsnummern entgegen.
Das Interface Anonymizer ist wie folgt definiert:
Die Tatsache, dass das Interface über eine einzige Methode verfügt und mit „FunctionalInterface“ annotiert ist, erlaubt es uns, eine anonyme Methode mit entsprechender Signatur zu übergeben.
Schauen wir uns ein Beispiel-Programm hierzu an:
Wir erzeugen zunächst eine Customer-Instanz, die unseren Kunden Ludwig Müller abbildet. Wir fügen testweise drei Auftragsnummern zu seiner Historie hinzu. Diese Historie wird dann über die entsprechende Methode ausgegeben. Anschließend soll die besagte Anonymisierung stattfinden. Hierfür übergeben wir eine anonyme Methode in Form eines Lambda-Ausdrucks.
Dies erlaubt uns, eine beliebige Funktion beim Methoden-Aufruf zu übergeben, solange sie mit der von dem funktionalen Interface vorgegebenen Signatur übereinstimmt. In unserem Beispiel ersetzen wir über einen einfachen regulären Ausdruck alle Buchstaben in den Auftragsnummern mit „X“, sodass nur noch Zahlen unverändert bleiben. Anschließend lassen wir uns die anonymisierte Auftragshistorie ausgeben.
Folgenden Output produziert das obige Programm:
Unter der Haube wird hier vom Compiler ein anonymes Objekt erzeugt, welches das von uns vorgegebene Interface „Anonymizer“ implementiert. Die Logik in dem übergebenen Lambda-Ausdruck stellt dabei die konkrete Realisierung der einzigen Methode in dieser anonymen Klasse dar. Instanzen anonymer Klassen lassen sich in Java auch regulär über den new-Operator erzeugen. Fakt ist: Es wird keine explizite Klassendefinition mit Interface-Implementierung benötigt; dies geschieht alles implizit.
ABAP-Implementierung mit gegebenen Mitteln
In der ABAP-Umgebung stehen uns leider keine funktionalen Interfaces, Lambda-Ausrücke oder anonyme Objekte zur Verfügung. Würden wir dieselben Anforderungen aus obigem Beispiel in ABAP abbilden, ergäbe sich wahrscheinlich eine Implementierung wie die folgende.
Die Definition der Customer-Klasse:
Die dazugehörige Implementierung:
Auch hier nimmt die Methode anonymize_order_history ein Objekt entgegen, welches ein Interface mit dem Namen ZAH_IF_ANONYMIZER implementiert. Dieses Interface ist wie folgt definiert:
Es liegt lediglich eine einzige Methode vor, welche einen veränderlichen String entgegennimmt. Die Aufgabe der implementierenden Klasse ist es, eine Logik bereitzustellen, welche den Changing-Parameter CV_INPUT entsprechend der Anonymisierung transformiert.
Möchte man die Methode ANONYMIZE_ORDER_HISTORY nun nutzen, so benötigt man eine konkrete Implementierung des Interfaces ZAH_IF_ANONYMIZER. Oft reicht hier eine lokal definierte Klasse, wenn kein Wert auf globale Wiederverwendbarkeit gelegt wird.
Ein Test-Report, analog zu unserer Java-Lösung, könnte wie folgt aussehen:
Algorithmisch und semantisch ist dieses Programm identisch mit dem vorherigen Java-Beispiel. Der Kunde Ludwig Müller wird als Customer-Instanz erzeugt, die Auftragsnummern werden hinzugefügt, ausgegeben, anonymisiert und schließlich erneut ausgegeben.
Das Programm produziert somit den uns bekannten Output:
Für die Implementierung des Anonymizer-Interfaces haben wir in diesem Fall eine Klasse im Scope unseres globalen ABAP-Programms definiert und implementiert. Um die einzeilige Anonymisierungs-Logik bereitzustellen, mussten wir also folgende Schritte durchführen:
- Klassen-Definition erstellen; dabei angeben, dass das Anonymizer-Interface implementiert wird.
- Klassen-Implementierung erstellen
- Logik der Methode ZAH_IF_ANONYMIZER~ANONYMIZE ausprägen
- Im Programmablauf Instanz der Klasse erzeugen
- Instanz an Methode ANONYMIZE_ORDER_HISTORY übergeben
Dabei sollte doch lediglich die Implementierung der Anonymisierungs-Logik explizit notwendig sein, nicht wahr? Die restlichen Schritte sollten doch – wie auch in unserer Java-Lösung – implizierbar sein. Die Signatur der erwarteten Funktion ist bekannt. Das Anonymizer-Interface verfügt lediglich über eine einzige Methode. Also sollt es doch möglich sein, sich als Entwickler auf den wesentlichen und vor allem variablen Teil der Lösung zu beschränken, welcher da ist:
Streng genommen handelt es sich bei dem restlichen Implementierungs-Aufwand um Boilerplate-Code, also Overhead, den ein effizienter, lösungsorientierter Entwickler nach Möglichkeit vermeiden möchte. Betrachten wir daher einen alternativen Approach.
Der Schlachtplan
Wie zuvor erwähnt, bietet uns die Programmiersprache Java die Möglichkeit, Interfaces mithilfe anonymer Klassen zu implementieren. In Langform könnte ein Anwendungsbeispiel so aussehen:
Obwohl der Typ „Anonymizer“ abstrakt ist, können wir hier eine konkrete Instanz erzeugen. Der Compiler füllt hier für uns die Lücken. Im Hintergrund wird eine anonyme Klasse im lokalen Scope generiert, welche das gewünschte Interface implementiert. Dies lässt sich auch in den erzeugten Binaries in der Ordner-Struktur des Projektes erkennen.
Neben der Klasse „Program“ wird hier zusätzlich eine Klasse „Program$1“ aufgeführt, welche nirgends explizit definiert wurde. Dies wurde auf Implikationsbasis durch den Compiler automatisch ergänzt. Bei der Verwendung eines Lambda-Ausdrucks – wie in unserem ursprünglichen Java-Beispiel – wird unter der Haube eine andere Technik verwendet. Die Strategie bleibt jedoch dieselbe: Implizite Objektgenerierung mit Implementierung des benötigten Interfaces. Von eben dieser Strategie lassen wir uns inspirieren.
Ein ABAP-Framework für anonyme Klassen
Das Ziel ist nun, eine Möglichkeit zu schaffen, mit möglichst kurzer und prägnanter Syntax einen Funktionskörper zur Verfügung zu stellen, ohne zusätzlichen Boilerplate-Code schreiben zu müssen. Wir orientieren uns dabei an den genannten Lambda-Ausdrücken aus Sprachen wie Java und C#.
Die Basis: Dynamische Source-Code-Generierung
Mit dem Statement GENERATE SUBROUTINE POOL bietet ABAP eine Möglichkeit, Programmkomponenten oder gar ganze Programme auf dynamische Weise zur Laufzeit zu generieren. Dies funktioniert sowohl für einfachen prozeduralen Code als auch objekt-orientierte Module.
Generieren wir zum Beispiel eine Klasse mit einer statischen Methode und rufen diese auf:
Dem GENERATE SUBROUTINE POOL Statement übergeben wir eine Tabelle mit Quell-Code-Zeilen, welche zur Laufzeit in ein lauffähiges ABAP-Modul übersetzt werden sollen. Den Namen des generierten Programms legen wir in der Variable lv_generated_prog_name ab. Dieser hilft uns dabei, den vollqualifizierten Namen der dynamisch generierten Klasse zu ermitteln und die darin implementierte statische Methode SHOW_MESSAGE aufzurufen.
Beim Ausführen des Programms begrüßt uns wie erwartet folgende Nachricht:
Die Lambda-Klasse
Auf Basis dieses Wissens entwerfen wir eine Klasse, mit deren Hilfe sich Boilerplate-Code zum Implementieren eines bestimmten Interfaces automatisch generieren lässt. Eine Instanz dieser Klasse beschreibt, wenn man so will, eine Art Funktionszeiger. Diesem geben wir mittels Konstruktor ein Interface mit, welches die Methode mit der Ziel-Signatur enthält. Eine dedizierte Methode der Lambda-Klasse bietet dann die Möglichkeit, ein anonymes Objekt zu erzeugen, welches das gewünschte Interface implementiert. Alles, was der Entwickler dann noch tun muss, ist die eigentliche Funktionslogik zu übergeben, ähnlich wie bei einem Lambda-Ausdruck in Java oder C#.
Die Definition einer solchen Klasse könnte wie folgt aussehen:
Eine dazugehörige Implementierung könnte sich in einfacher Form wie folgt gestalten:
Betrachten wir einmal die einzelnen Implementierungsdetails dieser Klasse. Der Konstruktor ist simpel. Dieser legt den übergebenen Interface-Namen lediglich in einem Feld ab. Um den Rahmen dieses Blog-Beitrages nicht zu sprengen, sparen wir an dieser Stelle ein wenig mit dem Error-Handling. Natürlich wäre es sinnvoll, an dieser Stelle einmal zu validieren, dass es sich bei dem übergebenen String wirklich um den Namen eines bekannten Interface-Typs handelt.
Spannend wird es bei der Implementierung der Methode BUILD. Diese ist dafür verantwortlich, ein ABAP-Programm dynamisch zu generieren, welches eine Klasse enthält, die das angegebene Interface implementiert. Die Methode nimmt die gewünschte Methodenlogik als String entgegen und fügt diese in den zu erzeugenden Source-Code ein. Schließlich wird eine Instanz dieser dynamisch generierten Klasse zurückgegeben. Beleuchten wir einmal den Algorithmus Schritt für Schritt.
Als Erstes benötigen wir eine Möglichkeit, Informationen über die in dem Interface deklarierten Methoden auszulesen. Ein ABAP-Type-Descriptor kann diese Aufgabe übernehmen.
Da der Name des Interfaces bekannt ist, können wir einen Type-Descriptor über die statische Factory-Methode DESCRIBE_BY_NAME instanziieren. Auch an dieser Stelle überspringen wir der Vereinfachung halber die Behandlung möglicher Fehler. Kann kein Type-Descriptor erzeugt werden, so belassen wir den Rückgabewert schlichtweg initial und geben an den Aufrufer zurück. Ist die Instanziierung erfolgreich, so lesen wir als Nächstes alle Methoden-Beschreibungen über das Attribut METHODS aus. Dabei wird auf das Visibility-Kennzeichen „U“ gefiltert. Dieses identifiziert alle Methoden, welche als „public“ deklariert sind. Es können nun natürlich mehrere Methoden in dem Interface deklariert worden sein. Ist dies der Fall, wird bei dieser Implementierung nur die erste Methode betrachtet. Die Methoden-Struktur hilft nun dabei, den dynamischen Klassen-Quellcode zu generieren.
Die zu generierende Klasse benötigt einen eindeutigen und unter Garantie einmaligen Namen. Maximal darf ein Klassenname in ABAP 20 Zeichen enthalten. Da trifft es sich gut, dass die Standard-API für UUIDs eine Methode zur Erzeugung einer 16-stelligen alphanumerischen ID anbietet. Die übrigen 4 Zeichen füllen wir mit dem Präfix „ANO_“ für „anonym“.
Da nun der Klassen-, Interface- und Methodenname sowie auch die innere Logik der Methode bekannt sind, können wir den vollständigen Quellcode der anonymen Klasse generieren. Hierfür wird eine Tabelle mit Strings erzeugt, welche die einzelnen Code-Zeilen darstellen.
Schließlich kann der dynamisch erzeugte Source-Code zur Laufzeit kompiliert werden, indem die String-Tabelle an das GENERATE SUBROUTINE POOL Statement übergeben wird. Den Namen des generierten Programms gibt die Routine zurück.
Da sich die generierte Klasse im Scope des dynamisch erzeugten Programms befindet, müssen wir den Klassennamen vollqualifizieren. Mittels CREATE OBJECT und dem vollständigen Klassennamen lässt sich nun eine Instanz der „anonymen“ Klasse erzeugen und über den Rückgabeparameter zurückgeben. Mangels Typ-Parametern (Generics) sind wir gezwungen, den Rückgabeparameter schwach zu typisieren und auf den Typ „object“ zurückzugreifen. Da der Verwender der Lambda-Klasse jedoch das Interface im Konstruktor explizit übergibt, ist dieses im aufrufenden Kontext bekannt, sodass ein Down-Cast problemlos möglich ist.
Das Framework in Aktion
Mit der Lambda-Klasse eröffnen sich somit neue Möglichkeiten, kurze, prägnante Logikblocks in einem objekt-orientierten Kontext zwischen Methodenaufrufen weiterzugeben. Blicken wir also noch einmal zurück auf das eingangs gezeigte ABAP-Beispiel. Durch Verwendung der Lambda-Klasse lässt sich der Programm-Code deutlich vereinfachen.
Anstelle einer lokal definierten Klasse wird hier eine Instanz der Lambda-Klasse verwendet. Mittels Konstruktor wird das zu implementierende Interface spezifiziert. Die BUILD-Methode erzeugt dann die benötigte anonyme Objekt-Instanz. Da die BUILD-Methode jedoch schwach typisiert ist (Rückgabetyp ist „object“), muss an dieser Stelle ein Type-Cast durchgeführt werden. In diesem Beispiel geschieht dies über den zuweisenden Cast-Operator „?=“. Mit dem richtigen Error-Handling innerhalb des Lambda-Konstruktors oder der BUILD-Methode, könnte ein unzulässiges Down-Casting verhindert werden, sodass der Erfolg des Type-Casts gewährleistet ist. Die erzeugte Instanz kann dann wie jedes handelsübliche Interface-konforme Objekt an die Methode ANONYMIZE_ORDER_HISTORY übergeben werden.
Der Output des Programms bleibt unverändert:
Der Aufruf der Methode ANONYMIZE_ORDER_HISTORY lässt sich bei Verwendung des expliziten Cast-Operators auch als einzelnes Statement implementieren:
Auf diese Weise kann ein Entwickler die variable Transformationslogik direkt beim Aufruf der Methode definieren. Der Overhead der Klassendefinition und -implementierung fällt vollständig weg.
Vorteile
Der Nutzen, den ein Entwickler aus dem oben beschriebenen Framework für anonyme Klassen ziehen kann, ist durch das gezeigte Anwendungsbeispiel nicht zu leugnen. Hier noch einmal die Vorteile zusammengefasst.
Vereinfachte Abstraktion einzelner Funktionen
Wie in Java stehen dem Entwickler durch das Framework funktionale Interfaces zur Verfügung, mit denen sich einzelne Funktionen auf simple Weise abstrahieren lassen.
Realisierung von ABAP-Interfaces ohne Klassendefinition/-implementierung
Code für die Realisierung eines Interfaces, einschließlich der Klassendefinition und der dazugehörigen Implementierung wird automatisch generiert.
Reduzierung von Boilerplate-Code
Durch die Code-Generierung zur Laufzeit muss das Entwicklerteam die abstrahierte Methode nicht explizit in einer neuen – zum Beispiel lokalen – Klasse implementieren. Stattdessen kann sich auf die eigentliche Geschäfts- bzw. Anwendungslogik konzentriert werden.
Simple Kapselung und Übergabe von Logikblöcken
Eine kurze, prägnante Syntax erlaubt die Weitergabe ganzer Logikblöcke ohne zusätzlichen Overhead.
Nachteile
Keine Software-Lösung bringt nur Vorteile. Natürlich hat auch das oben beschriebene Framework einige Makel, denen sich das Entwicklerteam bewusst sein sollte. Nachteile dieses Designs sind zum Beispiel:
Fehlende Syntaxprüfung
Da der Methodenkörper und somit die eigentliche Funktionslogik als einfacher String übergeben wird, kann die ABAP-Entwicklungsumgebung hier keinen Syntax-Check durchführen. Syntax-Fehler fallen somit schlimmstenfalls erst zur Laufzeit auf und bringen das Programm zum Abbruch.
Probleme mit Code-Profiling / Qualitätssicherung
Da der Funktionscode lediglich als String angegeben wird und somit nicht der regulären Syntax-Prüfung unterliegt, haben QS-Werkzeuge wie automatisierte Code-Profiler Schwierigkeiten, mögliche Probleme und Sicherheitsrisiken in der Funktionslogik zu erkennen. Daher ist es besonders wichtig, dass ein manuelles Code-Review sowie eine Abnahme durch die QS durchgeführt werden, um potenzielle Probleme zu eliminieren. Zudem ist es möglich, dass die QS-Richtlinien des Kunden oder des Projektes dynamische Code-Generierung prinzipiell verbieten.
Schwache Typisierung
Der Rückgabewert der BUILD-Methode in der Lambda-Klasse ist schwach typisiert (Typ „object“). Leider unterstützt ABAP bis dato noch keine Typ-Parameter (Generics). Es besteht daher das potenzielle Risiko, dass ein Down-Cast fehlschlägt.
Fazit
Wie bei jeder Design-Entscheidung muss das Entwicklerteam die Vor- und Nachteile der Integration eines solchen Frameworks für anonyme Klassen und Lambda-Ausdrücke gewissenhaft abwägen. Die Möglichkeit, einzelne Funktionen aus der Geschäftslogik heraus zu abstrahieren, ohne unzählige Zeilen Boilerplate zu produzieren, kann für einige Entwicklungsprojekte sehr attraktiv sein. Ein Deal-Breaker könnten jedoch die fehlende Syntax-Prüfung und die dadurch entstehenden QS-Maßnahmen sein. In einem Kundenprojekt sollten die möglichen Auswirkungen auf vorgegebene Entwicklungsrichtlinien selbstverständlich abgestimmt werden.