
ABAP Cloud - Übergreifende Komponenten
Welche Punkte gibt es zu beachten, wenn du übergreifende Komponenten in ABAP Cloud entwickeln möchtest? Hier schauen wir auf verschiedene Beispiele.
Inhaltsverzeichnis
In diesem Artikel schauen wir uns die Arbeit über Software Komponenten hinweg an und welche Herausforderungen die Nutzung dieser Komponenten mit sich bringt.
Einleitung
Auf jedem System gibt es normalerweise eine handvoll Komponenten, die in der Entwicklung wiederverwendet werden. Solche zentralen Komponenten werden in den meisten Fällen zentral zur Verfügung gestellt und auch gewartet. Solche Komponenten sind zum Beispiel eine Fassade für das Application Log, Templates und Services für den Mailversand, Parametertabellen oder verschiedene Konfigurationen. Im Bereich ABAP Cloud verwenden wir verschiedene Software Komponenten, um die Bestandteile unserer Anwendungen zu trennen.
Wieso wir viele TIER-1 Software Komponenten (SWC) verwenden, haben wir dir in diesem Artikel zusammengefasst. Dabei geht es um die bewusste Trennung von Code und DDIC Elementen, um mehr Struktur und eine saubere Kapselung ins System zu bringen. Am Ende machen wir uns darüber Gedanken, welche Objekte wir für andere Freigeben.
Szenario
In diesem Fall nutzen wir zwei Klassen und ein Interface. Eine Klasse ist eine Re-Use Komponente und die anderen Klasse implementiert einige Methoden und hat ein Interface, wo auch Typen definiert sind, die wir in unserem Szeanrio testen. Vom Aufbau her sehen die Artefakte wie folgt aus.
Freigabe
Da wir mit einer wiederverwendbaren Komponente arbeiten und diese in einer anderen Software Komponente ist, müssen wir einen C1 Contract definieren und damit einen Stabilitätsvertrag eingehen. Das Objekt wird damit unsere eigene freigegebene API im System. Wollen wir die Klasse ohne Freigabe nutzen, erhalten wir die folgende Fehlermeldung.
Im nächsten Schritt sollten wir also noch unsere C1 Freigabe auf der Klasse definieren.
Project Explorer
Wenn das Objekt im Project Explorer wählst und per Rechts-Klick das Kontextmenü öffnest, findest du bei passenden Objekten eine Aktion im unteren Bereich.
Properties
Ist das Objekt geöffnet, kannst du in den "Properties" View wechseln und findest dort unter "API State" die entsprechenden Contract Informationen. Über den "+" Button kannst du dann in den Standardflow zur Freigabe einsteigen.
Flow
Im Anschluss läufst du durch den Standard für die Freigabe der Objekte. Wichtig ist der C1 Contract für die Nutzung des Objekts und das die Einstellung "Use in Cloud Development" gesetzt ist, damit wir das Objekt innerhalb von ABAP Cloud verwenden können.
Zum Abschluss wird der Eintrag in den Transport übernomen, wo einige Tabelleneinträge für die Freigabe transportiert werden. Zusammengefasst wird das Ganze unter dem Objekttyp "APIS".
Datentyp
Wie sieht es nun mit der allgemeinen Nutzung von Datentypen bei übergreifenden Komponenten aus? In diesem Fall gehen wir davon aus, dass der Typ außerhalb der Re-Use Komponente in der aufrufenden SWC liegt.
Vorbereitung
Dazu legen wir einen Datentyp im Interface ZIF_BS_DEMO_SWC_USE an, den wir in unserer übergreifenden Komponente befüllen lassen wollen. Der Typ besteht aus unterschiedlichen Feldern mit unterschiedlichen Datentypen.
TYPES charlike TYPE c LENGTH 25.
TYPES packed TYPE p LENGTH 15 DECIMALS 2.
TYPES: BEGIN OF dummy,
number TYPE i,
char TYPE charlike,
string TYPE string,
packed TYPE packed,
timestamp TYPE utclong,
END OF dummy.
TYPES dummys TYPE STANDARD TABLE OF dummy WITH EMPTY KEY.
Referenz
In diesem Szenario geben wir den Datentyp als Referenz in die Methode. Dazu definieren wir unsere Re-Use-Methode wie folgt:
METHODS table_reference
IMPORTING !data TYPE REF TO data.
Bei der Methodenimplementierung benötigen wir zuerst ein Feldsymbol um die Referenz zuweisen zu können. Im nöchsten Schritt würden wir dann die Daten dem Feldsymbol zuweisen, um sie zurückzugeben.
METHOD table_reference.
FIELD-SYMBOLS <table> TYPE STANDARD TABLE.
ASSIGN data->* TO <table>.
DATA(parts) = get_default_parts( ).
<table> = CORRESPONDING #( parts ).
ENDMETHOD.
Allerdings funktioniert diese Methode nicht, da bereits beim ASSIGN ein SY-SUBRC von 4 gesetzt wird. Wir haben aktuell keinen Zugriff auf den Typen aus dem Interface.
Changing
Im nächsten Versuch verwenden wir einen Changing Parameter, um die Tabelle von außen in die Methode zu geben und dort zu befüllen.
METHODS table_changing
CHANGING !data TYPE ANY TABLE.
Die Methodenimplementierung ist recht einfach gehalten, wir erzeugen nur unsere Dummy Daten und weisen sie per CORRESPONDING dem Changing Parameter zu.
METHOD table_changing.
DATA(parts) = get_default_parts( ).
data = CORRESPONDING #( parts ).
ENDMETHOD.
Diese Variante funktioniert so weit auch und die entsprechend befüllten Felder kommen beim Aufrufer an. Im Beispiel findest du auch das gleiche Beispiel für das Thema EXPORTING, auch dieses Szenario funktioniert.
Dynamisch
Im letzten Beispiel übergeben wir den Typen dynamisch als String und erwarten von der Routine die Daten als erzeugte Referenz von dem Typen zurück.
METHODS table_with_type
IMPORTING type_name TYPE string
RETURNING VALUE(data) TYPE REF TO data.
Bei der Implementierung erzeugen wir dynamisch unsere Daten Referenz über den mitgelieferten Typen, weisen die Referenz dem neuen Feldsymbol zu und übergeben die Daten.
METHOD table_with_type.
CREATE DATA data TYPE (type_name).
ASSIGN data->* TO FIELD-SYMBOL(<data>).
DATA(parts) = get_default_parts( ).
<data> = CORRESPONDING #( parts ).
ENDMETHOD.
In diesem Fall erhalten wir direkt einen Dump, da der CREATE nicht funktioniert. Die Meldung deutet allerdings darauf hin, dass der Typ unbekannt ist. Bei der dynamischen Zuweiseung, kann erst einmal nicht der Typ geprüft werden und damit ist die Meldung nicht so genau.
Lösung
Grundsätzlich kannst du auch das Interface ZIF_BS_DEMO_SWC_USE mit einer C1 Freigabe ausstatten, allerdings wäre das Interface dann im gesamten System freigegeben und könnte auch von anderen genutzt werden, was widerrum zu Abhängigkeiten führen könnte. Der sauberste Weg wäre daher die Anlage einer "Software Component Relation". Dort geben wir unsere Komponenten für die SWC der übergreifenden Komponenten frei (Access Permission).
Führen wir nun noch einmal alle Methoden unserer Klasse aus, dann funktionieren alle Verfahren. Aktuell sind die Software Component Relations allerdings nur im ABAP Environment und der S/4HANA Cloud Public Edition verfügbar und werden sehr wahrscheinlich mit 2025 verfügbar sein.
Application Log
Im nächsten Beispiel schauen wir uns das Anlegen eines Application Logs an. In den meisten Fällen verwenden wir eine Fassade für das Application Log, da die Übernahme der Meldungen auch mit den neuen Schnittstellen noch relativ viel Code benötigt, ebenso wie die Initialisierung.
Szenario
Dazu definieren wir eine Methode in unserer übergreifenden Komponente die eine Tabelle von Nachrichten entgegen nimmt und diese dann in einem Application Log speichert.
METHODS save_messages
IMPORTING !messages TYPE messages
!header TYPE REF TO if_bali_header_setter OPTIONAL.
Die Implementierung der Methode ist recht lang, dazu eine kurze Erklärung. Wir erzeugen ein Application Log Objekt und im Anschluss einen entsprechenden Header, wenn keiner übergeben wurde. Dann übernehmen wir den Header in unser Log Objekt, speichern die Meldung und rufen zum Abschluss den SAVE auf, um die Meldungen ins Log zu persistieren.
METHOD save_messages.
DATA(application_log) = cl_bali_log=>create( ).
DATA(local_header) = header.
IF local_header IS INITIAL.
local_header = cl_bali_header_setter=>create( object = 'ZBS_APPL_REUSE'
subobject = 'TEST'
external_id = CONV #( xco_cp=>uuid( )->value ) ).
ENDIF.
local_header->set_expiry( expiry_date = CONV d( cl_abap_context_info=>get_system_date( ) + 7 )
keep_until_expiry = abap_true ).
application_log->set_header( local_header ).
LOOP AT messages INTO DATA(message).
application_log->add_item( cl_bali_message_setter=>create_from_bapiret2( message ) ).
ENDLOOP.
cl_bali_log_db=>get_instance( )->save_log( application_log ).
ENDMETHOD.
Das verwendete Application Log Objekt "ZBS_APPL_REUSE" liegt in diesem Fall in unserer aufrufenden Komponente, also bei der Anwendung die die übergreifende Komponente nutzt. Zur Einfachheit haben wir das Objekt hartcodiert hinterlegt.
Fehler
Führen wir nun die Methode aus, kommt es bei der Anlage des Header in der Methode zu einem Fehler und unsere Routine bricht ab. Beim Debuggen wirst du feststellen, dass der Standard hier Prüfungen vornimmt, ob unser Application Log Object freigegeben ist. Damit kann unsere Komponente den Header nicht anlegen und damit das Log auch nicht speichern.
Lösung
In diesem Beispiel gibt es zwei Lösungen die uns zum Ziel bringen können:
- Anlage des Headers (CL_BALI_HEADER_SETTER) in der aufrufenden Komponente und Übergabe an die Speichern Methode.
- Nutzung der Software Component Relations, um alle Objekte für die übergreifenden Komponenten freizugeben.
Anpassung
Möchtest du freigegebene APIs anpassen, ist dies grundsätzlich möglich, du wirst aber auch mit einer Sicherheitsmeldung beim Editieren begrüßt.
Daher solltest du die Standardregeln für das Ändern von Artefakten beachten
Vollständiges Beispiel
Hier findest du noch einmal alle gezeigten Resourcen aus dem Artikel, auf eine Ablage in GitHub haben wir dieses Mal verzichtet.
Interface
Das Interface stellt einen Typen zur Verfügung.
INTERFACE zif_bs_demo_swc_use
PUBLIC.
TYPES charlike TYPE c LENGTH 25.
TYPES packed TYPE p LENGTH 15 DECIMALS 2.
TYPES: BEGIN OF dummy,
number TYPE i,
char TYPE charlike,
string TYPE string,
packed TYPE packed,
timestamp TYPE utclong,
END OF dummy.
TYPES dummys TYPE STANDARD TABLE OF dummy WITH EMPTY KEY.
ENDINTERFACE.
Klassen
Die Klasse soll die übergreifende Komponente konsumieren.
CLASS zcl_bs_demo_swc_use DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
INTERFACES zif_bs_demo_swc_use.
PRIVATE SECTION.
METHODS call_method
IMPORTING !out TYPE REF TO if_oo_adt_classrun_out.
METHODS call_with_changing
IMPORTING !out TYPE REF TO if_oo_adt_classrun_out.
METHODS call_with_reference
IMPORTING !out TYPE REF TO if_oo_adt_classrun_out.
METHODS call_for_reference
IMPORTING !out TYPE REF TO if_oo_adt_classrun_out.
METHODS call_with_exporting
IMPORTING !out TYPE REF TO if_oo_adt_classrun_out.
METHODS call_application_log
IMPORTING !out TYPE REF TO if_oo_adt_classrun_out.
ENDCLASS.
CLASS zcl_bs_demo_swc_use IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
call_method( out ).
call_with_changing( out ).
call_with_reference( out ).
call_for_reference( out ).
call_with_exporting( out ).
call_application_log( out ).
ENDMETHOD.
METHOD call_method.
DATA(dummy_data) = VALUE zif_bs_demo_swc_use=>dummys(
( number = 1 char = 'one' string = `First field` packed = '1.11' timestamp = utclong_current( ) )
( number = 2 char = 'two' string = `Second field` packed = '2.22' timestamp = utclong_current( ) ) ).
out->write( dummy_data ).
ENDMETHOD.
METHOD call_with_changing.
DATA changed_data TYPE zif_bs_demo_swc_use=>dummys.
DATA(reuse) = NEW zcl_bs_demo_swc_reuse( ).
reuse->table_changing( CHANGING data = changed_data ).
out->write( changed_data ).
ENDMETHOD.
METHOD call_with_reference.
DATA changed_data TYPE zif_bs_demo_swc_use=>dummys.
DATA(reuse) = NEW zcl_bs_demo_swc_reuse( ).
reuse->table_reference( data = REF #( changed_data ) ).
out->write( changed_data ).
ENDMETHOD.
METHOD call_for_reference.
DATA(reuse) = NEW zcl_bs_demo_swc_reuse( ).
DATA(reference) = reuse->table_with_type( type_name = 'ZIF_BS_DEMO_SWC_USE=>DUMMYS' ).
out->write( reference->* ).
ENDMETHOD.
METHOD call_with_exporting.
DATA changed_data TYPE zif_bs_demo_swc_use=>dummys.
DATA(reuse) = NEW zcl_bs_demo_swc_reuse( ).
reuse->table_exporting( IMPORTING data = changed_data ).
out->write( changed_data ).
ENDMETHOD.
METHOD call_application_log.
DATA(messages) = VALUE zcl_bs_demo_swc_reuse=>messages( id = 'ZDUMMY'
( type = 'S' number = '001' )
( type = 'W' number = '002' ) ).
* DATA(header) = cl_bali_header_setter=>create( object = 'ZBS_APPL_REUSE'
* subobject = 'TEST'
* external_id = CONV #( xco_cp=>uuid( )->value ) ).
DATA(reuse) = NEW zcl_bs_demo_swc_reuse( ).
reuse->save_messages( messages = messages
* header = header
).
out->write( messages ).
ENDMETHOD.
ENDCLASS.
Die Klasse stellt unsere übergreifende Komponente dar.
CLASS zcl_bs_demo_swc_reuse DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES messages TYPE STANDARD TABLE OF bapiret2 WITH EMPTY KEY.
METHODS table_changing
CHANGING !data TYPE ANY TABLE.
METHODS table_reference
IMPORTING !data TYPE REF TO data.
METHODS table_with_type
IMPORTING type_name TYPE string
RETURNING VALUE(data) TYPE REF TO data.
METHODS table_exporting
EXPORTING !data TYPE ANY TABLE.
METHODS save_messages
IMPORTING !messages TYPE messages
!header TYPE REF TO if_bali_header_setter OPTIONAL.
PRIVATE SECTION.
TYPES: BEGIN OF part,
number TYPE i,
string TYPE string,
timestamp TYPE utclong,
END OF part.
TYPES parts TYPE STANDARD TABLE OF part WITH EMPTY KEY.
METHODS get_default_parts
RETURNING VALUE(result) TYPE parts.
ENDCLASS.
CLASS zcl_bs_demo_swc_reuse IMPLEMENTATION.
METHOD table_changing.
DATA(parts) = get_default_parts( ).
data = CORRESPONDING #( parts ).
ENDMETHOD.
METHOD table_reference.
FIELD-SYMBOLS <table> TYPE STANDARD TABLE.
ASSIGN data->* TO <table>.
DATA(parts) = get_default_parts( ).
<table> = CORRESPONDING #( parts ).
ENDMETHOD.
METHOD table_with_type.
CREATE DATA data TYPE (type_name).
ASSIGN data->* TO FIELD-SYMBOL(<data>).
DATA(parts) = get_default_parts( ).
<data> = CORRESPONDING #( parts ).
ENDMETHOD.
METHOD table_exporting.
DATA(parts) = get_default_parts( ).
data = CORRESPONDING #( parts ).
ENDMETHOD.
METHOD get_default_parts.
RETURN VALUE parts( ( number = 13 string = `Thirteen` timestamp = utclong_current( ) )
( number = 20 string = `Twenty` ) ).
ENDMETHOD.
METHOD save_messages.
DATA(application_log) = cl_bali_log=>create( ).
DATA(local_header) = header.
IF local_header IS INITIAL.
local_header = cl_bali_header_setter=>create( object = 'ZBS_APPL_REUSE'
subobject = 'TEST'
external_id = CONV #( xco_cp=>uuid( )->value ) ).
ENDIF.
local_header->set_expiry( expiry_date = CONV d( cl_abap_context_info=>get_system_date( ) + 7 )
keep_until_expiry = abap_true ).
application_log->set_header( local_header ).
LOOP AT messages INTO DATA(message).
application_log->add_item( cl_bali_message_setter=>create_from_bapiret2( message ) ).
ENDLOOP.
cl_bali_log_db=>get_instance( )->save_log( application_log ).
ENDMETHOD.
ENDCLASS.
Fazit
Die Entwicklung von übergreifenden Komponenten funktioniert in der neuen Welt weiterhin, ist aber mit gewissen Risiken und Hindernissen verbunden, wenn es um eine saubere Implementierung in allen Fällen geht. Anhand der Beispiele sollten du aber viele Probleme aus dem Weg räumen können.