
RAP - Auxiliary Class
Wenn die Implementierung in der Verhaltensimplementierung eines RAP Objekts wächst, welche Möglichkeiten hast du dann noch für eine saubere Kapselung? Schauen wir uns das einmal im Detail an.
Inhaltsverzeichnis
In diesem Artikel implementieren wir weitere Methoden in unserem RAP Objekt und machen uns Gedanken, wie wir Code noch sauber handeln können.
Einleitung
Bisher waren unsere Implementierungen innerhalb der Sales App noch sehr überschaubar. Im letzten Artikel hatten wir die Änderungsdokumente in die Implementierung aufgenommen, welche für die eigentliche Methode zu groß wären. In so einem Fall sollten wir uns Gedanken machen und ein Refactoring anstreben, bevor die Implementierung zu groß und nicht mehr handlebar und/oder wartbar wird.
MVC
Die eigentliche Trennung in RAP ist schon von SAP sehr gut vorgegeben. So trennen wir die verschiedenen Objekte und Layer nach dem Model-View-Controller Prinzip und haben damit automatische Vorgaben für eine gute Software Entwicklung. Dazu können wir die verschiedenen Objekte den verschiedenen Layern zuordnen:
Diese entspricht dann den folgenden Objekten:
- Model - Tabelle, Core Data Service, Service und Version
- View - SAP Fiori App
- Controller - Verhaltensdefinition und -implementierung
Die verschiedenen Schichten sind voneinander getrennt und jede erfüllt ihr eigene Aufgabe, der Vergleich mit MVC liegt dabei recht nah, ist aber nicht zu 100% korrekt. Allerdings können wir auch hier wieder den Controller Layer schnell mit Informationen und schlechten Implementierungen ruinieren. Zum Beispiel wenn wir die komplette Implementierung einer Action in der Methode der Action durchführen. Damit verstoßen wir gegen Clean ABAP und die Methode wird schnell unübersichtlich.
Vorbereitung
Im ersten Schritt wollen wir noch eine zusätzliche Action im RAP Objekt implementieren. Der "Check Consistency" Button soll das gesamte Objekt und die Daten validieren und dazu ein Logo erstellen, welches im Objekt dann auch verlinkt ist. Dazu hatten wir bisher die Aktion definiert, im UI ausgerollt, aber bisher noch nicht implementiert.
Implementierung
Fangen wir also an mit der Implementierung der Methode "ConsistencyCheck" in der Verhaltensimplementierung an und lesen uns alle Daten des aktuellen Datensatzes ein, damit wir diese prüfen können.
READ ENTITIES OF zbs_r_sasale IN LOCAL MODE
ENTITY SASale
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(selected_sales)
ENTITY SASale BY \_SASeller
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(selected_sellers)
ENTITY SASale BY \_SAInfo
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(selected_infos)
ENTITY SASale BY \_SASold
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(selected_materials).
Nachdem wir per EML die Informationen gelesen haben, können wir nun die verschiedenen Bestandteile bewerten und Nachrichten in einem Applikation Log erzeugen. Hierfür verwenden wir unser kleines Open Source Projekt, den ABAP Message Logger (AML) und wollen folgende Prüfungen haben.
- Differenz - Wurde nur eine der beiden Differenzen befüllt. Es dürfen nicht beide Felder oder keine Information vorhanden sein.
- Information - Wurde eine Information erfasst und diese auch in alle nötigen Sprachen übersetzt.
- Verkäufer - Sind Verkäufer vorhanden, diese freigegeben und ist die gesamte Summe 100%.
- Material - ist Material vorhanden und gepflegt.
Dazu legen wir noch eigene Meldungen an, um dem User eine gute und übersetzbare Ausgabe zu erzeugen.
Nun können wir die eigentliche Methode implementieren und die Prüfungen durchführen. Dabei lassen wir erst einmal alle Bestandteile in dieser Methode. Die Nachrichten erzeugen wir lokal, um dem Verwendungsnachweiß zu unterstützen und verwenden dabei ein spezifisches Attribut des Loggers, damit wir keine zusätzlichen Variablen mit "NEEDED" zu erzeugen.
LOOP AT selected_sales INTO DATA(sale).
IF sale-DifferenceAmount IS NOT INITIAL AND sale-DifferenceQuantity IS NOT INITIAL
OR sale-DifferenceAmount IS INITIAL AND sale-DifferenceQuantity IS INITIAL.
MESSAGE e001(zbs_demo_rap) INTO logger->message_text.
logger->add_message_system( ).
ENDIF.
ENDLOOP.
LOOP AT selected_sellers INTO DATA(seller).
IF seller-Confirmed IS INITIAL.
MESSAGE e004(zbs_demo_rap) WITH seller-SellerId INTO logger->message_text.
logger->add_message_system( ).
ENDIF.
ENDLOOP.
SELECT FROM @selected_sellers AS table
FIELDS SUM( Quota ) AS QuotaSum
INTO @DATA(overall_sum).
IF overall_sum <> 100.
MESSAGE e002(zbs_demo_rap) INTO logger->message_text.
logger->add_message_system( ).
ENDIF.
IF selected_sellers IS INITIAL.
MESSAGE e003(zbs_demo_rap) INTO logger->message_text.
logger->add_message_system( ).
ENDIF.
DATA(helper) = NEW zcl_bs_demo_rap_auxiliary( ).
LOOP AT helper->get_supported_languages( ) INTO DATA(supported_langauge).
IF NOT line_exists( selected_infos[ Language = supported_langauge-language ] ).
MESSAGE e005(zbs_demo_rap) WITH supported_langauge-language INTO logger->message_text.
logger->add_message_system( ).
ENDIF.
ENDLOOP.
IF selected_materials IS INITIAL.
MESSAGE e003(zbs_demo_rap) INTO logger->message_text.
logger->add_message_system( ).
ENDIF.
IF NOT logger->has_error( ).
MESSAGE s007(zbs_demo_rap) INTO logger->message_text.
logger->add_message_system( ).
ENDIF.
Ist die Prüfung abgeschlossen, speichern wir das Log über eine zweite Datenbankverbindung, da wir von RAP sonst eine Ausnahme bekommen, da wir in dieser Phase (Interaktionsphase) keinen Commit Work oder Inserts machen dürfen. Die Konfiguration der zweiten Verbindung machen wir bereits bei der Erzeugung des Objekts, hier einmal der Anfang der Methode und das Speichern.
DATA(logger) = zcl_aml_log_factory=>create( VALUE #( object = 'Z_AML_LOG'
subobject = 'TEST'
default_message_class = 'ZBS_DEMO_RAP'
use_2nd_db_connection = abap_true ) ).
" ...
logger->save( ).
Per EML machen wir nun einen Update des Feldes für die LoggingId, um die Information am Objekt zu persistieren. Zum Abschluss geben wir noch eine Erfolgsmeldung der Prüfung an das UI, damit der User die Veränderung mitbekommt.
MODIFY ENTITIES OF zbs_r_sasale IN LOCAL MODE
ENTITY SASale
UPDATE FIELDS ( LoggingId )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky LoggingId = logger->get_log_handle( ) ) )
FAILED DATA(failed_updates).
IF failed_updates-sasale IS INITIAL.
INSERT new_message( id = 'ZBS_DEMO_RAP'
number = '008'
severity = if_abap_behv_message=>severity-success )
INTO TABLE reported-%other.
ELSE.
INSERT new_message( id = 'ZBS_DEMO_RAP'
number = '009'
severity = if_abap_behv_message=>severity-success )
INTO TABLE reported-%other.
ENDIF.
Aktualisierung
Aktuell wird nach der Erstellung des Logs und der Aktualisierung zwar eine Nachricht ausgegeben, allerdings wird das UI nicht aktualisiert und somit sehen wir die neue LoggingId nicht direkt im UI. Dafür arbeiten wir mit einem Side Effect und aktualisieren nach der Ausführung der Aktion das Feld im UI. Hier müssen wir nicht die ganze Entität neu laden, sondern beschränken uns auf das Feld. Wir nehmen den Side Effect in der Verhaltensdefinition ZBS_R_SASALE auf, da ab diesem Level auch die Aktion zur Verfügung steht.
side effects {
action ConsistencyCheck affects field LoggingId;
}
Schauen wir uns den Punkt im Browser und im Netzwerk an (Entwicklertools - F12), dann sehen wir nach der Ausführung der Aktion immer noch nur einen BATCH Request im Netzwerk. Allerdings im Detail wird es eine Änderung geben, da in dieser Anfrage eigentlich zwei eigene Aktionen stecken.
Im ersten Schritt wird per POST unsere Aktion ausgelöst, die die eigentliche Prüfung durchführen soll. Als zweite Anfrage schicken wir einen GET, der die neuen Daten liest. Dieser Liest auch nur das Feld LoggingId nach, so wie wir es im Seiteneffekt definiert haben.
Hilfsklasse
Nun haben wir bereits zwei große und zwei ähnliche Implementierungen in unserer Verhaltensimplementierung. Eigentlich schon viel zu groß für die einzelnen Methoden. Daher beginnen wir mit dem eigentlichen Refactoring der vorhandenen Logik.
Lösungen
Dafür stehen uns aktuell verschiedene Lösungen zur Verfügung. Zum Beispiel können wir zusätzliche lokale Klassen und Methoden anlegen, um das Coding aus der eigentlichen Implementierung auszulagern. Allerdings erreichen wir damit immer noch keine Wiederverwendung im OO Kontext. Wir könnten aber auch eine globale Klasse verwenden, damit hätten wir das Coding global und wiederverwendbar zur Verfügung. Allerdings gibt es hier auch ein Problem, da wir einem Behavior Pool (BP) vor allem mit EML arbeiten, können wir in einer normalen Klasse keinen LOCAL MODE verwenden. Dieser ist allerdings wichtig, wenn wir auf den aktuellen Stand eines Eintrags zugreifen wollen.
Auxiliary Class
Für die korrekte Verwendung können wir daher eine Hilfsklasse verwenden, die wir in unserer Verhaltensdefinition angeben. Damit bekommt die Klasse die gleichen Eigenschaften wie ein Behavior Pool und wir können unseren Coe auslagern. Dazu legen wir im ersten Schritt unsere neue globale Klasse ZCL_BS_DEMO_RAP_AUXILIARY an.
CLASS zcl_bs_demo_rap_auxiliary DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
ENDCLASS.
CLASS zcl_bs_demo_rap_auxiliary IMPLEMENTATION.
ENDCLASS.
Ist die Klasse definiert, können wir die Verhaltensdefinition ZBS_R_SASALE erweitern und die Klasse dem RAP Objekt bekannt geben. Dazu verwenden wir im Header den Zusatz AUXILIARY CLASS und können hier eine oder mehrere globale Klassen angeben.
managed implementation in class ZBS_BP_R_SASALE unique;
strict ( 2 );
with draft;
extensible;
auxiliary class zcl_bs_demo_rap_auxiliary;
Bevor wir mit dem eigentlichen Refactoring beginnen, müssen wir der Klasse bekannt geben, für welches RAP Objekt bzw. für welche Verhaltensdefinition die Klasse steht. Dazu definieren wir in den Eigenschaften der Klasse mit FOR BEHAVIOR die Beziehung zum RAP Objekt.
CLASS zcl_bs_demo_rap_auxiliary DEFINITION
PUBLIC FINAL
CREATE PUBLIC
FOR BEHAVIOR OF zbs_r_sasale.
PUBLIC SECTION.
ENDCLASS.
CLASS zcl_bs_demo_rap_auxiliary IMPLEMENTATION.
ENDCLASS.
Refactoring
Wir können nun mit dem Refactoring der lokalen Implementierung starten und die verschiedenen Bestandteile in unsere Hilfsklasse auslagern. Natürlich sollten wir nicht nur ein einfaches Verschieben durchführen, sondern auch die Methoden kleiner und besser handlebar gestalten. In diesem Beispiel verwenden wir die Klasse direkt und ohne Interface, hier bietet sich immer ein sauberes Objekt an, auch wenn diese viel Arbeit bedeutet. Weitere Informationen zu den verschiedenen Designs in diesem Artikel.
Änderungsdokumente
Im letzten Artikel hatten wir die Änderungsdokumente in RAP Implementiert, dazu haben wir zwei Hilfsmethoden definiert und relativ viel Code für die unterschiedlichen Änderungen implementiert. Den eigentlichen CATCH würden wir nun stehen lassen, da wir hier auch noch die Möglichkeit behalten wollen, eine Nachricht an das UI zurückzugeben. Dafür implementieren wir die verschiedenen Hilfsmethoden für die einzelnen Tabellen und lagern den Code aus. Damit sieht die Implementierung der Methode SAVE_MODIFED schon recht überschaubar aus.
TRY.
DATA(helper) = NEW zcl_bs_demo_rap_auxiliary( ).
helper->change_document_for_create( create ).
helper->change_document_for_update( update ).
helper->change_document_for_delete( delete ).
CATCH cx_chdo_write_error INTO DATA(error).
RAISE SHORTDUMP error.
ENDTRY.
Konsistenzprüfung
Die eigentliche Konsistenzprüfung würden wir nun auch komplett auslagern. Diese schieben wir in eine neue Methode, die uns noch zurückgibt, ob die Prüfung erfolgreich durchlaufen ist oder ob es vielleicht Fehler bei der Anlage des Logs gab. Die Erzeugung der Nachricht lassen wir in der eigentlichen Prüfung bestehen, alle anderen Prüfungen werden dann noch weiter in kleinere Methoden ausgelagert.
DATA(consistent_flag) = NEW zcl_bs_demo_rap_auxiliary( )->is_consistent( keys ).
IF consistent_flag = abap_true.
INSERT new_message( id = 'ZBS_DEMO_RAP'
number = '008'
severity = if_abap_behv_message=>severity-success )
INTO TABLE reported-%other.
ELSE.
INSERT new_message( id = 'ZBS_DEMO_RAP'
number = '009'
severity = if_abap_behv_message=>severity-success )
INTO TABLE reported-%other.
ENDIF.
Abschluss
Wir gehen nicht auf alle Details des Refactorings ein, da es hier je nach Entwickler auch zu Abweichungen kommen kann, wie die eigentlichen Methoden neu geschnitten werden. Nach dem Refactoring haben wir einige neue Typen und Methoden in der neuen Hilfsklasse. Grundsätzlich kannst du hier noch weiter granular aufteilen und andere Methoden definieren. Dies ist erst einmal eine erste Version der Umstellung.
Vollständiges Beispiel
Das vollständige Beispiel findest du in GitHub im entsprechenden Paket für die Sales App. Die Änderungen aus diesem Artikel findest du in diesem Commit und kannst damit die Änderungen, plus die Zusatzinformationen, nachvollziehen.
Fazit
Die zusätzliche Hilfsklasse bietet die Möglichkeiten den Code besser zu strukturieren und Ordnung in eine Business Objekt zu bringen. Allerdings solltest du solche Änderungen bereits früh angehen, wenn du merkst, dass der Code zu viel wird und zu unübersichtlich wird.
Quelle:
SAP Help - Auxiliary class



