
OO Design - Methodenschnittstelle
Wie sollte eigentlich eine Schnittstelle einer ABAP Methode aussehen und welche Vorteile bringen die verschiedenen Designs? In diesem Artikel schauen wir auf Parameter, Strukturen und Objekte für die Abbildung.
Inhaltsverzeichnis
In diesem Artikel schauen wir uns das Design und die Verwendung von Methoden-Interfaces an und wie sie nach heutigem Stand eigentlich aussehen sollten.
Einleitung
Der ABAP-Code sollte in der heutigen Zeit vor allem in Klassen definiert sein. Dabei kannst du verschiedenen Pattern folgen, um Best Practices einzuhalten und so die Testbarkeit sicherzustellen. Weitere Best Practices gibt es aber auch bei der Definition und beim Design von Methodenschnittstellen. Wie viele Parameter sollte diese Schnittstelle haben und wie werden die Daten eigentlich von Methode zu Methode weitergegeben? Dabei wird jeder Entwickler, egal wie lange er das schon macht, auf bestimmte Herausforderungen treffen oder Schnittstellen sehen, die vielleicht nicht den Best Practices entsprechen.
Ausgangssituation
Schauen wir zuerst einmal auf die Ausgangssituation. Eine Beispielimplementierung, die nicht komplett alle Best Practices implementiert, sondern erst einmal nur zu Demonstrationszwecken dienen soll. Die Klasse implementiert kein Interface, ist aber instanzbasiert. Denk bitte daran, in einer echten Klasse die Best Practices einzuhalten (siehe Pattern oben).
Klasse
Das Beispiel implementiert eine relativ große Klasse. Wir haben einen Konstruktor, der verschiedene Werte entgegennimmt, um Variablen innerhalb der Klasse zu initialisieren. Des Weiteren haben wir eine Run-Methode, die die eigentliche Logik durchführt. Innerhalb der Run-Methode validieren wir zuerst einmal, ob die Konfiguration vollständig und gepflegt ist, und führen dann eine zusätzliche interne Run-Methode aus, welche die eigentliche Logik implementieren soll und diese vielleicht noch in weitere Logiken verzweigt.
CLASS zcl_bs_demo_oo_intf1_start DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING config_id TYPE char10
!destination TYPE string
number_of_errors TYPE i
!log TYPE REF TO zif_aml_log.
METHODS run
EXPORTING system_id TYPE char3
!message TYPE string.
PRIVATE SECTION.
DATA config_id TYPE char10.
DATA destination TYPE string.
DATA number_of_errors TYPE i.
DATA log TYPE REF TO zif_aml_log.
METHODS is_config_valid
RETURNING VALUE(result) TYPE abap_boolean.
METHODS internal_run
IMPORTING config_id TYPE char10
!destination TYPE string
number_of_errors TYPE i
EXPORTING system_id TYPE char3
!message TYPE string.
ENDCLASS.
CLASS zcl_bs_demo_oo_intf1_start IMPLEMENTATION.
METHOD constructor.
me->config_id = config_id.
me->destination = destination.
me->number_of_errors = number_of_errors.
me->log = log.
ENDMETHOD.
METHOD run.
IF NOT is_config_valid( ).
RETURN.
ENDIF.
internal_run( EXPORTING config_id = config_id
destination = destination
number_of_errors = number_of_errors
IMPORTING system_id = system_id
message = message ).
ENDMETHOD.
METHOD is_config_valid.
IF config_id IS INITIAL.
log->add_message_text( `Config is initial` ).
ENDIF.
IF destination IS INITIAL.
log->add_message_text( `Destination is mandatory` ).
ENDIF.
IF number_of_errors <= 0.
log->add_message_text( `Insert a number bigger than 0` ).
ENDIF.
RETURN xsdbool( log->has_error( ) = abap_false ).
ENDMETHOD.
METHOD internal_run.
" Do processing
ENDMETHOD.
ENDCLASS.
Herausforderungen
Nun haben wir verschiedene Herausforderungen, die wir in dieser Klasse eigentlich ganz gut definieren können. So haben wir zum Beispiel beim Aufruf des Konstruktors ein relativ großes Interface mit Variablen. Hier wurden bereits vier verschiedene Variablen übergeben, welche intern einzeln gehandhabt werden.
METHODS constructor
IMPORTING config_id TYPE char10
!destination TYPE string
number_of_errors TYPE i
!log TYPE REF TO zif_aml_log.
Die Schnittstelle ist damit relativ groß. Müssen wir später die Schnittstelle um einen weiteren Parameter anpassen, dann müssen wir ebenfalls ein lokales Attribut ergänzen, die Eingangsschnittstelle anpassen und unsere Run-Methoden ebenso erweitern, da diese auch die Parameter übernehmen. Das heißt, eine Anpassung an einem Parameter hat zur Folge, dass wir drei Stellen innerhalb unseres Codings erweitern müssen. Ebenso sind die Schnittstellen nicht besonders schlank, wenn wir auf die Implementierung und den Aufruf schauen.
internal_run( EXPORTING config_id = config_id
destination = destination
number_of_errors = number_of_errors
IMPORTING system_id = system_id
message = message ).
Ein weiterer Punkt, den wir vielleicht später fixen können, ist, dass wir hier eine Konfigurationsvalidierung haben. Diese ist lokal implementiert und unabhängig von den übergebenen Daten. In einem späteren Refactoring könnte dies aber auch bedeuten, dass wir die Logik verdoppeln müssen, um die Validierung auch in einem anderen Objekt durchführen zu können.
Clean ABAP
Ein erster wichtiger Hinweis, den du bekommen kannst, liegt im Clean ABAP Styleguide. Dies ist ein durch die Community erzeugter Guide, welcher Best Practices für die Entwicklung im ABAP-Kontext, aber insgesamt auch im ABAP-Kontext, festlegt. Dabei werden viele Best Practices und gute Hinweise gegeben, wie du guten und sauberen ABAP-Code erstellen kannst. Hier solltest du im Bereich für die Methoden einmal näher nachschauen, um das Thema Naming, Aufbau und Aufruf von Methoden kennenzulernen.
Grundsätzlich ist unser Ziel aber, die Methoden so schlank wie möglich zu halten, um möglichst wenig Parameter hineinzugeben und möglichst wenig Parameter wieder herauszubekommen. Dabei können wir die verschiedenen Inhalte bündeln. Dies erlaubt eine einfache Anpassung des gebündelten Objektes. So sparen wir uns die Anpassung durch das gesamte Objekt hindurch und passen die Inhalte nur an sehr wenigen Stellen konsistent an. Gleichtzeitig ist der Schreibaufwand geringer und die Flexibilität bei späteren Anpassungen groß.
Variante A - Struktur
In der ersten Variante verwenden wir eine Struktur, um unsere Konfiguration in das Objekt zu geben, aber auch um Ergebnisse aus dem Objekt wieder nach draußen zu geben.
Definition
Dazu definieren wir uns erst einmal zwei verschiedene Strukturen, um erstens unsere Konfigurationen entgegenzunehmen und zweitens das Ergebnis des Laufs wieder zurückzugeben. Der Vorteil der gebündelten Strukturen ist dabei, dass wir diese in jede Methode übergeben können, sie an sich aber flexibel bleiben, um sie zum Beispiel um weitere Felder zu ergänzen. Damit müssen wir im Zweifelsfall nur die Struktur erweitern, aber nicht die Schnittstellen der Methoden, diese bleiben stabil.
TYPES:
BEGIN OF configuration,
config_id TYPE char10,
destination TYPE string,
number_of_errors TYPE i,
log TYPE REF TO zif_aml_log,
END OF configuration.
TYPES:
BEGIN OF run_result,
system_id TYPE char3,
message TYPE string,
END OF run_result.
Methoden
Tauschen wir dann die Parameter in der Methode durch unsere Strukturen aus, dann erhalten wir ein überschaubares Bild. Wir haben genau einen Eingangsparameter und einen Ausgangsparameter. Jeweils ist dieser als RETURNING definiert, was die Übergabe an andere Werte oder die Direktzuweisung viel, viel einfacher macht.
METHODS internal_run
IMPORTING config TYPE configuration
RETURNING VALUE(result) TYPE run_result.
Den Aufruf tauschen wir dann auch in unserer Run-Methode aus und sehen, dass der Aufruf der Methode nur noch eine Zeile beträgt. Wir können direkt beim Return die Methode aufrufen, die Konfiguration übergeben und damit alles handhaben. Zusätzlich sparen wir uns noch die Zuweisung der Parameter beim Aufruf, da wir nur einen Eingangsparameter haben und damit den Namen weglassen können.
METHOD run.
IF NOT is_config_valid( ).
RETURN.
ENDIF.
RETURN internal_run( config ).
ENDMETHOD.
In der Konfigurationsüberprüfung (IS_CONFIG_VALID) müssen wir dann noch auf die Strukturen verweisen und nicht mehr auf die Einzelwerte gehen. Ansonsten ändert sich der Aufbau der Methode nicht. Unsere Run-Methode gibt nun eine Struktur zurück und kann damit den RETURNING-Parameter verwenden. Möchten wir später dann die Struktur erweitern und weitere Felder als Ergebnis zurückgeben, können wir einfach die Struktur anpassen und sparen uns damit die Umstellung auf zum Beispiel EXPORTING.
Variante B - Objekt
In der zweiten Variante bauen wir unsere Konfiguration um und erstellen dazu ein eigenes Objekt. Der Einfachheit halber verwenden wir hier eine einfache Klasse ohne ein Interface. Möchtest du das Ganze aber testbar gestalten, solltest du zusätzlich auch ein Interface anlegen, um die Implementierung flexibel zu halten. In diesem Beispiel legen wir alle Attribute als öffentliche Attribute an und setzen sie auf READ-ONLY. Die Befüllung erfolgt über den Konstruktor, wie bereits in der anderen Implementierung. Als entscheidende Designentscheidung kannst du hier ebenfalls festlegen, ob du Getter und Setter pro Attribut definieren möchtest. Dies ist auch leicht über die ABAP Development Tools umsetzbar. Getter und Setter haben jeweils noch den Vorteil, dass wir die Werte und Attribute, die wir erhalten, vorher noch einmal prüfen oder anpassen können, bevor wir sie in die eigentliche Implementierung übernehmen. Im letzten Schritt können wir im Konfigurationsobjekt sogar noch zusätzliche Methoden übernehmen, die auf der Basis der Daten funktionieren. So übernehmen wir auch die IS_VALID Methode, um die übergebenen Attribute testen und validieren zu können und die Konsistenz des Objekts sicherzustellen.
CLASS zcl_bs_demo_oo_intf_config DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
DATA config_id TYPE char10 READ-ONLY.
DATA destination TYPE string READ-ONLY.
DATA number_of_errors TYPE i READ-ONLY.
DATA log TYPE REF TO zif_aml_log READ-ONLY.
METHODS constructor
IMPORTING config_id TYPE char10
!destination TYPE string
number_of_errors TYPE i
!log TYPE REF TO zif_aml_log.
METHODS is_valid
RETURNING VALUE(result) TYPE abap_boolean.
ENDCLASS.
CLASS zcl_bs_demo_oo_intf_config IMPLEMENTATION.
METHOD constructor.
me->config_id = config_id.
me->destination = destination.
me->number_of_errors = number_of_errors.
me->log = log.
ENDMETHOD.
METHOD is_valid.
IF config_id IS INITIAL.
log->add_message_text( `Config is initial` ).
ENDIF.
IF destination IS INITIAL.
log->add_message_text( `Destination is mandatory` ).
ENDIF.
IF number_of_errors <= 0.
log->add_message_text( `Insert a number bigger than 0` ).
ENDIF.
RETURN xsdbool( log->has_error( ) = abap_false ).
ENDMETHOD.
ENDCLASS.
Grundsätzlich haben wir nun den Großteil der Logik aus der eigentlichen implementierenden Klasse herausgezogen. Dies ermöglicht uns eine dynamische Implementierung der Konfiguration und auch der aufrufenden Klasse, was die beiden Objekte voneinander entkoppelt. Verwenden wir dann das Konfigurationsobjekt in unserer RUN Methode, können wir direkt über das Konfigurationsobjekt die IS_VALID Methode aufrufen, um zu validieren, dass die Daten konsistent sind. Die Instanz können wir dann einfach an die RUN Methode übergeben, um sich die Daten daraus zu ziehen.
METHOD run.
IF NOT config->is_valid( ).
RETURN.
ENDIF.
RETURN internal_run( config ).
ENDMETHOD.
Auch diese Implementierung ist sehr flexibel, da wir weitere Attribute in der Konfigurationsklasse einfach hinzufügen können, ohne die eigentliche Schnittstelle anpassen zu müssen. Zusätzlich haben wir die Flexibilität, Getter und Setter dazu zu implementieren sowie weitere Methoden, die mit der Konfiguration zusammenhängen, wie zum Beispiel auch das Speichern der Konfiguration oder die Erstellung eines einheitlichen Logs. Vorteil davon ist, dass wir die Konfiguration nun in verschiedenen anderen Klassen verwenden können und diese nicht nur beschränkt ist auf unsere eigentliche Implementierung.
Vollständiges Beispiel
Alle gezeigten Quellcodes findest du im GitHub Repository für die ABAP OO Beispiele. Dort findest du auch weitere Beispiele, wenn es Richtung Designpattern von Klassen geht.
Fazit
Für die Varianten der Struktur oder des Objektes gibt es jeweils Vor- und Nachteile, und hier ist sich die Community noch nicht 100% einig. Vielleicht ist das beste Mittel der Wahl, je nach Szenario zu entscheiden, welche der genannten Methoden du verwenden willst, da auch entsprechend neue Objekte dabei herausfallen oder Aufwände bei der Implementierung entstehen. Der wichtigste Punkt ist aber, auf eine schlanke und saubere Methodenarchitektur zu setzen, um später weniger Aufwände bei der Anpassung von Code zu haben.