Clean ABAP – Clean Code in ABAP

Clean ABAP – Sauberer ABAP-Code. Clean ABAP ist eine Programmiermethode, die im SAP-Entwicklungsumfeld zunehmend an Bedeutung gewinnt. Clean ABAP fördert nicht nur die Lesbarkeit und Wartbarkeit des Codes, sondern trägt auch zur Effizienz und Qualität von ABAP-Anwendungen bei. Clean ABAP erleichtert die Entwicklung und Wartung von SAP-Softwareprojekten erheblich und führt zu einer verbesserten Gesamtperformance und Kundenzufriedenheit.

Die SAP hat die Styleguides auf GitHub veröffentlicht, die die wichtigsten Erkenntnisse aus dem Buch „Clean Code“ für die Programmiersprache ABAP umfangreich erläutern. Dieser Artikel ist sehr empfehlenswert. Der Artikel wird regelmäßig überprüft und aktualisiert.

In Rahmen von Clean Code in ABAP sind außerdem folgende Artikel sehr nützlich:

Inhaltsverzeichnis Anzeigen

Namenskonventionen

Beschreibende Namen verwenden

Verwende Namen, die den Inhalt und die Bedeutung der Dinge vermitteln.

CONSTANTS max_wait_time_in_seconds TYPE i ...
DATA customizing_entries TYPE STANDARD TABLE ...
METHODS read_user_preferences ...
CLASS /clean/user_preference_reader ...

Konzentriere Dich nicht auf den Datentyp oder die technische Kodierung. Sie tragen kaum zum Verständnis des Codes bei.

" Anti-Muster
CONSTANTS sysubrc_04 TYPE sysubrc ...
DATA iso3166tab TYPE STANDARD TABLE ...
METHODS read_t005 ...
CLASS /dirty/t005_reader ...

Versuche nicht, schlechte Namen durch Kommentare zu korrigieren.

Bevorzuge Begriffe aus dem Lösungsbereich und dem Problembereich

Suche nach guten Namen in der Lösungsdomäne, d. h. nach Informatikbegriffen wie „Queue“ oder „Tree“, und in der Problemdomäne, d. h. nach Begriffen aus der Wirtschaft wie „Konto“ oder „Hauptbuch“.

Schichten, die geschäftsähnlich sind, klingen am besten, wenn sie entsprechend der Problemdomäne benannt werden. Dies gilt insbesondere für Komponenten, die mit Domain-Driven Design entworfen wurden, wie z. B. APIs und Geschäftsobjekte.

Schichten, die hauptsächlich technische Funktionalität bieten, wie Factory Klassen und abstrakte Algorithmen, klingen am besten, wenn sie nach der Lösungsdomäne benannt werden.

In jedem Fall sollten Sie nicht versuchen, Ihre eigene Sprache zu erfinden. Wir müssen in der Lage sein, Informationen zwischen Entwicklern, Produkteigentümern, Partnern und Kunden auszutauschen, also wählen Sie Namen, mit denen sich alle Beteiligten ohne ein individuelles Wörterbuch identifizieren können.

Plural verwenden

Bei SAP ist es üblich, Tabellen von Dingen im Singular zu benennen, z. B. Land für eine „Tabelle von Ländern“. In der Außenwelt ist es üblich, für Listen von Dingen den Plural zu verwenden. Wir empfehlen daher, stattdessen Länder zu verwenden.

Dieser Ratschlag zielt in erster Linie auf Dinge wie Variablen und Eigenschaften ab. Für Entwicklungsobjekte kann es konkurrierende Muster geben, die ebenfalls sinnvoll sind, zum Beispiel die weit verbreitete Konvention, Datenbanktabellen („transparente Tabellen“) im Singular zu benennen.

Aussprechbare Namen verwenden

Wir denken und sprechen viel über Objekte, also verwende Namen, die Du aussprechen kannst, zum Beispiel lieber detection_object_types als etwas Kryptisches wie dobjt.

Snake_case verwenden

ABAP unterscheidet nicht zwischen Groß- und Kleinschreibung, weshalb wir empfehlen, die Konvention snake_case konsequent zu verwenden.

Es gibt eine Zeichenbegrenzung für Namen, z.B. 30 Zeichen für Methoden. Wenn Du die maximale Länge eines Objekts erreichst, solltest Du nicht auf die Groß- und Kleinschreibung zurückgreifen. Versuche stattdessen, gewissenhaft Abkürzungen zu verwenden. Verwende überall die gleichen Abkürzungen.

" eine Variable, die die maximale Antwortzeit, gemessen in Millisekunden, enthält
DATA max_response_time_in_millisec TYPE i.

ist besser als

" Anti-Muster
DATA maxresponsetimeinmilliseconds TYPE i.

Vermeide Abkürzungen

Du kannst Abkürzungen verwenden, aber sei vorsichtig, da sie oft mehrdeutig werden können. Zum Beispiel kann „cust“ in „Customizing,“ „Kunde,“ oder „kundenspezifisch“ bedeuten, und alle drei sind in SAP-Anwendungen gebräuchlich.

Überall dieselben Abkürzungen verwenden

Menschen suchen nach Schlüsselwörtern, um relevanten Code zu finden. Unterstütze dies, indem Du die gleiche Abkürzung für die gleiche Sache verwenden. Kürze zum Beispiel „detection object type“ immer mit „dobjt“ ab, anstatt „dot“, „dotype“, „detobjtype“ usw. zu verwenden.

Substantive für Klassen und Verben für Methoden verwenden

Verwende Substantive oder Substantivphrasen, um Klassen, Schnittstellen und Objekte zu benennen:

CLASS /clean/account
CLASS /clean/user_preferences
INTERFACE /clean/customizing_reader

Benutze Verben oder Verbphrasen, um Methoden zu benennen:

METHODS withdraw
METHODS add_message
METHODS read_entries

Wenn man boolesche Methoden mit Verben wie is_ und has_ beginnt, ergibt sich ein schöner Lesefluss:

IF is_empty( table ).

Funktionen sollten wie Methoden benannt werden:

FUNCTION /clean/read_alerts

Vermeide Reizwörter wie „Daten“, „Info“, „Objekt“.

Störende Wörter weglassen

account  " anstatt account_data
alert    " anstatt alert_object

oder sie durch etwas Bestimmtes ersetzen, das wirklich einen Mehrwert bietet

user_preferences          " anstatt user_info
response_time_in_seconds  " anstatt response_time_variable

Wähle ein Wort pro Begriff

METHODS read_this.
METHODS read_that.
METHODS read_those.

Wähle einen Begriff für ein Konzept und bleibe bei diesem. Mische keine anderen Synonyme ein. Synonyme führen dazu, dass der Leser Zeit damit vergeudet, einen Unterschied zu finden, den es nicht gibt.

" Anti-Muster
METHODS read_this.
METHODS retrieve_that.
METHODS query_those.

Verwende Musternamen nur, wenn Du sie ernst meinen

Verwende die Namen von Software-Entwurfsmustern für Klassen und Schnittstellen nur dann, wenn Du sie wirklich meinst. Nenne z. B. Deine Klasse nicht file_factory, wenn sie nicht wirklich das Factory-Entwurfsmuster implementiert. Zu den häufigsten Mustern gehören: Singleton, Factory, Facade, Composite, Decorator, Iterator, Observer und Strategy.

Vermeide Kodierungen, insbesondere die ungarische Notation und Präfixe

Es wird empfohlen, alle Kodierungspräfixe zu vermeiden.

METHOD add_two_numbers.
  result = a + b.
ENDMETHOD.

statt der unnötig längeren

METHOD add_two_numbers.
  rv_result = iv_a + iv_b.
ENDMETHOD.

Vermeide es, eingebaute Funktionen zu verdecken

Innerhalb einer Klasse wird eine eingebaute Funktion immer durch Methoden der Klasse verdeckt, wenn sie denselben Namen haben, unabhängig von der Anzahl und dem Typ der Argumente in der Funktion. Die Funktion wird auch unabhängig von der Anzahl und dem Typ der Methodenparameter verdeckt. Eingebaute Funktionen sind z.B. condense( ), lines( ), line_exists( ), strlen( ), etc.

" Anti-Muster
METHODS lines RETURNING VALUE(result) TYPE i.    
METHODS line_exists RETURNING VALUE(result) TYPE i.
" Anti-Muster
CLASS-METHODS condense RETURNING VALUE(result) TYPE i.   
CLASS-METHODS strlen RETURNING VALUE(result) TYPE i.

Sprache

Die Hinterlassenschaft beachten

Wenn Du für ältere ABAP-Releases programmierst, solltest Du die Ratschläge in diesem Leitfaden mit Vorsicht behandeln: Viele der folgenden Empfehlungen verwenden eine relativ neue Syntax und Konstrukte, die in älteren ABAP-Releases möglicherweise nicht unterstützt werden. Überprüfe die Richtlinien, die Du befolgen willst, auf dem ältesten Release, das Du unterstützen musst. Verwerfe Clean Code nicht einfach als Ganzes – die große Mehrheit der Regeln (z. B. Benennung, Kommentierung) funktioniert in jeder ABAP-Version.

Achte auf die Leistung

Wenn Du leistungsstarke Komponenten programmierst, nimm die Ratschläge in diesem Leitfaden mit Bedacht: Einige Aspekte von „Clean Code“ können die Dinge verlangsamen (mehr Methodenaufrufe) oder mehr Speicher verbrauchen (mehr Objekte). ABAP hat einige Besonderheiten, die dies intensivieren können, zum Beispiel vergleicht es Datentypen beim Aufrufen einer Methode, so dass das Aufteilen einer einzigen großen Methode in viele Teilmethoden den Code verlangsamen kann.

Es wird jedoch dringend empfohlen, nicht vorschnell zu optimieren, basierend auf unklaren Ängsten. Die überwiegende Mehrheit der Regeln (z. B. Benennung, Kommentierung) hat überhaupt keine negativen Auswirkungen. Versuche, Dinge auf saubere, objektorientierte Weise zu entwickeln. Wenn etwas zu langsam ist, führe eine Leistungsmessung durch. Erst dann solltest Du eine auf Fakten basierende Entscheidung treffen, ausgewählte Regeln zu verwerfen.

In einer typischen Anwendung wird der Großteil der Laufzeit in einem sehr kleinen Teil des Codes verbracht. So kann bereits 10 % des Codes für 90 % der Laufzeit verantwortlich sein, und insbesondere in ABAP entfällt ein großer Teil der Laufzeit wahrscheinlich auf die Datenbankzeit.

Daher ist es nicht sinnvoll, erhebliche Anstrengungen darauf zu verwenden, den gesamten Code ständig super effizient zu gestalten. Wir schlagen nicht vor, die Leistung zu ignorieren, sondern vielmehr den Fokus während der anfänglichen Entwicklung auf sauberen und gut strukturierten Code zu legen und den Profiler zu verwenden, um kritische Bereiche zur Optimierung zu identifizieren.

Tatsächlich würden wir argumentieren, dass ein solcher Ansatz eine insgesamt positive Wirkung auf die Leistung haben wird, da es sich um eine gezieltere Optimierungsanstrengung handelt und es einfacher sein sollte, Leistungsengpässe zu identifizieren und gut strukturierten Code einfacher zu überarbeiten und zu optimieren.

Objektorientierung gegenüber prozeduraler Programmierung bevorzugen

Objektorientierte Programme (Klassen, Schnittstellen) sind besser segmentiert und lassen sich leichter umgestalten und testen als prozeduraler Code (Funktionen, Programme). Obwohl es Situationen gibt, in denen Du prozedurale Objekte bereitstellen musst (eine Funktion für einen RFC, ein Programm für eine Transaktion), sollten diese Objekte nicht viel mehr tun, als eine entsprechende Klasse aufzurufen, die die eigentliche Funktion bereitstellt:

FUNCTION check_business_partner [...].
  DATA(validator) = NEW /clean/biz_partner_validator( ).
  result = validator->validate( business_partners ).
ENDFUNCTION.

Bevorzugung funktionaler gegenüber prozeduralen Sprachkonstrukten

Sie sind in der Regel kürzer und werden von modernen Programmierern eher verstanden.

DATA(variable) = 'A'.
" MOVE 'A' TO variable.

DATA(uppercase) = to_upper( lowercase ).
" TRANSLATE lowercase TO UPPER CASE.

index += 1.         " >= NW 7.54
index = index + 1.  " < NW 7.54
" ADD 1 TO index.

DATA(object) = NEW /clean/my_class( ).
" CREATE OBJECT object TYPE /dirty/my_class.

result = VALUE #( FOR row IN input ( row-text ) ).
" LOOP AT input INTO DATA(row).
"  INSERT row-text INTO TABLE result.
" ENDLOOP.

DATA(line) = value_pairs[ name = 'A' ]. " entry must exist
DATA(line) = VALUE #( value_pairs[ name = 'A' ] OPTIONAL ). " entry can be missing
" READ TABLE value_pairs INTO DATA(line) WITH KEY name = 'A'.

DATA(exists) = xsdbool( line_exists( value_pairs[ name = 'A' ] ) ).
IF line_exists( value_pairs[ name = 'A' ] ).
" READ TABLE value_pairs TRANSPORTING NO FIELDS WITH KEY name = 'A'.
" DATA(exists) = xsdbool( sy-subrc = 0 ).

Viele der nachstehenden detaillierten Regeln sind lediglich spezifische Wiederholungen dieser allgemeinen Ratschläge.

Vermeide veraltete Sprachelemente

Achte beim Upgrade der ABAP-Version darauf, dass Du auf veraltete Sprachelemente achtest und diese nicht mehr verwendest.

Zum Beispiel machen die @-escaped „host“-Variablen in der folgenden Anweisung ein wenig deutlicher, was eine Programmvariable und was eine Spalte in der Datenbank ist,

SELECT *
  FROM spfli
  WHERE carrid = @carrid AND
        connid = @connid
  INTO TABLE @itab.

im Vergleich zu der veralteten, nicht verkürzten Form

SELECT *
  FROM spfli
  WHERE carrid = carrid AND
        connid = connid
  INTO TABLE itab.

Neuere Alternativen verbessern in der Regel die Lesbarkeit des Codes und verringern Designkonflikte mit modernen Programmierparadigmen, so dass ein Wechsel zu diesen Alternativen Deinen Code automatisch bereinigen kann.

Veraltete Elemente funktionieren zwar weiterhin, profitieren aber möglicherweise nicht mehr von den Optimierungen in Bezug auf Verarbeitungsgeschwindigkeit und Speicherverbrauch.

Mit modernen Sprachelementen kannst Du junge ABAP-Mitarbeiter, die mit den veralteten Konstrukten nicht mehr vertraut sind, weil sie nicht mehr in den SAP-Schulungen gelehrt werden, leichter an Bord holen.

Die SAP NetWeaver Dokumentation enthält einen stabilen Abschnitt, der veraltete Sprachelemente auflistet, zum Beispiel NW 7.50, NW 7.51, NW 7.52, NW 7.53, NW 7.54, NW 7.55, NW 7.56, NW 7.57.

Entwurfsmuster klug einsetzen

Wo sie angemessen sind und einen spürbaren Nutzen bringen. Wende Entwurfsmuster nicht überall an, nur um der Sache willen.

Konstanten

Konstanten statt magischer Zahlen verwenden

IF abap_type = cl_abap_typedescr=>typekind_date.

ist verständlicher als

" Anti-Muster
IF abap_type = 'D'.

ENUM gegenüber Konstanten bevorzugen Schnittstellen

ABAP-eigene Aufzählungen mit ENUM verwenden (verfügbar in Releases >= 7.51)

CLASS /clean/message_severity DEFINITION PUBLIC ABSTRACT FINAL.
  PUBLIC SECTION.
    TYPES: BEGIN OF ENUM type,
             warning,
             error,
           END OF ENUM type.
ENDCLASS.

anstatt Dinge zu vermischen, die nichts miteinander zu tun haben, oder die Leute zu dem Schluss zu verleiten, dass Konstantensammlungen „implementiert“ werden könnten:

" Anti-Muster
INTERFACE /dirty/common_constants.
  CONSTANTS:
    warning      TYPE symsgty VALUE 'W',
    transitional TYPE i       VALUE 1,
    error        TYPE symsgty VALUE 'E',
    persisted    TYPE i       VALUE 2.
ENDINTERFACE.

Enumerations beschreibt alternative Aufzählungsmuster (auch für ältere Versionen, die ENUM noch nicht unterstützen) und erörtert deren Vor- und Nachteile.

Wenn Du keine ENUM- oder Aufzählungsmuster verwendest, gruppiere Deine Konstanten

Wenn Du keine Aufzählungen verwenden kannst und Konstanten in loser Form sammeln musst , z. B. in einer Schnittstelle, gruppiere sie zumindest:

CONSTANTS:
  BEGIN OF message_severity,
    warning TYPE symsgty VALUE 'W',
    error   TYPE symsgty VALUE 'E',
  END OF message_severity,
  BEGIN OF message_lifespan,
    transitional TYPE i VALUE 1,
    persisted    TYPE i VALUE 2,
  END OF message_lifespan.

macht den Zusammenhang deutlicher als

" Anti-Muster
CONSTANTS:
  warning      TYPE symsgty VALUE 'W',
  transitional TYPE i       VALUE 1,
  error        TYPE symsgty VALUE 'E',
  persisted    TYPE i       VALUE 2,

Die Gruppe ermöglicht Dir auch einen gruppenweisen Zugriff, zum Beispiel für die Eingabevalidierung:

DO.
  ASSIGN message_severity-(sy-index) TO FIELD-SYMBOL(<constant>).
  IF sy-subrc IS INITIAL.
    IF input = <constant>.
      DATA(is_valid) = abap_true.
      RETURN.
    ENDIF.
  ELSE.
    RETURN.
  ENDIF.
ENDDO.

Variablen

Inline-Deklarationen gegenüber Up-Front-Deklarationen bevorzugen

Wenn Du diese Richtlinien befolgst, werden Deine Methoden so kurz (3-5 Anweisungen), dass die Deklaration von Variablen inline beim ersten Auftreten natürlicher aussieht

METHOD do_something.
  DATA(name) = 'something'.
  DATA(reader) = /clean/reader=>get_instance_for( name ).
  result = reader->read_it( ).
ENDMETHOD.

als die Deklaration von Variablen mit einem separaten DATA-Abschnitt am Anfang der Methode

" Anti-Muster
METHOD do_something.
  DATA:
    name   TYPE seoclsname,
    reader TYPE REF TO /dirty/reader.
  name = 'something'.
  reader = /dirty/reader=>get_instance_for( name ).
  result = reader->read_it( ).
ENDMETHOD.

Verwende Variablen nicht außerhalb des Anweisungsblocks, in dem sie deklariert sind

" Anti-Muster
IF has_entries = abap_true.
  DATA(value) = 1.
ELSE.
  value = 2.
ENDIF.

Eine in einem Anweisungsblock (z. B. in einem IF- oder LOOP-Block) deklarierte Variable ist auch außerhalb dieses Blocks im nachfolgenden Code verfügbar. Dies ist für den Leser verwirrend, insbesondere wenn die Methode länger ist und die Deklaration nicht sofort auffällt.

Wenn die Variable außerhalb des Anweisungsblocks, in dem sie deklariert ist, benötigt wird, deklariere sie vorher:

DATA value TYPE i.
IF has_entries = abap_true.
  value = 1.
ELSE.
  value = 2.
ENDIF.

Keine Verkettung von Vorab-Deklarationen

DATA name TYPE seoclsname.
DATA reader TYPE REF TO reader.

Die Verkettung deutet darauf hin, dass die definierten Variablen auf einer logischen Ebene miteinander verbunden sind. Um sie konsequent zu nutzen, müsstest Du sicherstellen, dass alle verketteten Variablen zusammengehören, und zusätzliche Kettengruppen einführen, um Variablen hinzuzufügen. Dies ist zwar möglich, aber in der Regel nicht der Mühe wert.

Die Verkettung erschwert auch unnötig die Neuformatierung und das Refactoring, da jede Zeile anders aussieht und das Ändern dieser Zeilen ein Herumhantieren mit Doppelpunkten, Punkten und Kommas erfordert, was den Aufwand nicht wert ist.

" Anti-Muster
DATA:
  name   TYPE seoclsname,
  reader TYPE REF TO reader.

Verwende keine Feldsymbole für den dynamischen Datenzugriff

Starting in ABAP Platform 2021, there are almost no places left where using a field symbol is necessary to perform access to generically typed variables or dynamic access to components of a variable.

Anstatt beispielsweise etwas wie:

" Anti-Muster
ASSIGN dref->* TO <fs>.
result = <fs>.

schreibe

result = dref->*.

Wähle die richtigen Ziele für deine Schleifen

Es gibt drei mögliche Ziele für eine ABAP-Schleife: Ein Feldsymbol (SCHLEIFE AN Tabelle ZUWEISEN FIELD-SYMBOL().), eine Referenzvariable (SCHLEIFE AN Tabelle REFERENCE IN DATA(line).) oder ein einfaches Datenobjekt (SCHLEIFE AN Tabelle IN DATA(line).). Jede von ihnen hat unterschiedliche beabsichtigte Anwendungsfälle:

  • Feldsymbole, wenn du die Daten, über die iteriert wird, lesen oder manipulieren möchtest.
  • Datenreferenzen, wenn du diese Verweise außerhalb der aktuellen Schleife zugreifen musst, z.B. um sie in Methoden zu übergeben, deren Eingabeparameter Verweise sind, oder um Verweise auf die Daten auch nach Abschluss der Schleife aufzubewahren.
  • Datenobjekte, wenn du eine Kopie der Daten selbst benötigst oder wenn der Zeilentyp der Tabelle bereits eine Referenz ist.

Beachte, dass Datenreferenzen auch zum Lesen oder Manipulieren der Daten verwendet werden können. Das bedeutet, dass fast alle Vorkommen von Feldsymbolen als Schleifenziele durch Referenzen als Schleifenziele ersetzt werden können.

Um die Konsistenz mit dem allgemeinen Muster von objektorientiertem ABAP zu wahren, kannst du Referenzen als Schleifenziele verwenden, wann immer möglich. Andererseits, wenn du auf den gesamten Wert des Zeilentyps zugreifen möchtest, führen die Datenreferenzen zusätzliche Dereferenzierungsoperationen ein, die bei der Verwendung von Feldsymbolen unnötig sind. Vergleiche:

LOOP AT table ASSIGNING FIELD-SYMBOL(<line>).
  obj->do_something( <line> ).
ENDLOOP.

mit

LOOP AT table REFERENCE INTO DATA(line).
  obj->do_something( line->* ).
ENDLOOP.

Zusätzlich ist der Datenzugriff über Feldsymbole etwas schneller als der Datenzugriff über Referenzen. Dies fällt nur auf, wenn Schleifen einen signifikanten Teil der Laufzeit des Programms ausmachen und ist oft nicht relevant, z.B. wenn Datenbankoperationen oder andere Ein-/Ausgabe-Prozesse die Laufzeit dominieren.

Aus diesen Gründen gibt es zwei mögliche konsistente Stile, abhängig vom spezifischen Anwendungskontext:

  • Wenn der Kontext hauptsächlich Objekte und Referenzen verwendet und kleine Leistungseinbußen in Schleifen im Allgemeinen nicht relevant sind, verwende Referenzen anstelle von Feldsymbolen als Schleifenziele, wann immer möglich.
  • Wenn der Kontext viele Manipulationen einfacher Daten und keine Referenzen oder Objekte durchführt oder wenn kleine Leistungseinbußen in Schleifen im Allgemeinen relevant sind, verwende Feldsymbole, um Daten in Schleifen zu lesen und zu manipulieren.

Tabellen

Verwende den richtigen Tabellentyp

  • Du verwendest typischerweise HASHED-Tabellen für große Tabellen, die in einem Schritt gefüllt werden, niemals geändert werden und häufig nach ihrem Schlüssel gelesen werden. Ihr inhärenter Speicher- und Verarbeitungsaufwand macht Hash-Tabellen nur für große Datenmengen und viele Lesezugriffe wertvoll. Jede Änderung am Inhalt der Tabelle erfordert eine teure Neuberechnung des Hash, daher sollte dies nicht für Tabellen verwendet werden, die zu oft geändert werden.
  • Du verwendest typischerweise SORTED-Tabellen für große Tabellen, die ständig sortiert werden müssen, die Stück für Stück gefüllt werden oder geändert werden müssen und häufig nach einem oder mehreren vollständigen oder teilweisen Schlüsseln gelesen oder in einer bestimmten Reihenfolge verarbeitet werden. Das Hinzufügen, Ändern oder Entfernen von Inhalten erfordert das Finden der richtigen Einfügestelle, erfordert jedoch keine Anpassung des restlichen Index der Tabelle. Sortierte Tabellen zeigen ihren Wert nur bei einer großen Anzahl von Lesezugriffen.
  • Verwende STANDARD-Tabellen für kleine Tabellen, bei denen die Indexierung mehr Aufwand als Nutzen verursacht, und für „Arrays“, bei denen dir entweder die Reihenfolge der Zeilen überhaupt nicht wichtig ist oder du sie genau in der Reihenfolge verarbeiten möchtest, in der sie hinzugefügt wurden. Außerdem, wenn unterschiedliche Zugriffe auf die Tabelle benötigt werden, z.B. indexierter Zugriff und sortierter Zugriff über SORT und BINARY SEARCH.

DEFAULT KEY vermeiden

" Anti-Muster
DATA itab TYPE STANDARD TABLE OF row_type WITH DEFAULT KEY.

Standardmäßige Schlüssel werden oft nur hinzugefügt, um die neueren Funktionsanweisungen zum Laufen zu bringen. Die Schlüssel selbst sind in der Tat in der Regel überflüssig und verschwenden Ressourcen für nichts. Sie können sogar zu obskuren Fehlern führen, da sie numerische Datentypen ignorieren. Die SORT- und DELETE ADJACENT-Anweisungen ohne explizite Feldliste werden sich auf den Primärschlüssel der internen Tabelle beziehen, was im Falle der Verwendung von DEFAULT KEY zu sehr unerwarteten Ergebnissen führen kann, wenn beispielsweise numerische Felder als Bestandteil des Schlüssels verwendet werden, insbesondere in Kombination mit READ TABLE … BINARY usw.

Gib die Schlüsselkomponenten entweder explizit an.

DATA itab2 TYPE STANDARD TABLE OF row_type WITH NON-UNIQUE KEY comp1 comp2.

oder auf EMPTY KEY zurückgreifen, wenn Du überhaupt keinen Schlüssel benötigst.

DATA itab1 TYPE STANDARD TABLE OF row_type WITH EMPTY KEY.

Achtung: SORT auf internen Tabellen mit EMPTY KEY (ohne explizite Sortierfelder) sortiert überhaupt nicht, sondern es werden Syntaxwarnungen ausgegeben, falls die Leere des Schlüssels statisch ermittelt werden kann.

INSERT INTO TABLE gegenüber APPEND TO bevorzugen

INSERT VALUE #( ... ) INTO TABLE itab.

INSERT INTO TABLE funktioniert mit allen Tabellen- und Schlüsseltypen, wodurch es für Dich einfacher wird, die Tabellenart und Schlüsseldefinitionen umzustrukturieren, wenn sich Deine Leistungsanforderungen ändern.

Verwende APPEND TO nur dann, wenn Du eine STANDARD-Tabelle auf array-ähnliche Weise verwendest und betonen möchtest, dass der hinzugefügte Eintrag die letzte Zeile sein soll.

LINE_EXISTS gegenüber READ TABLE oder LOOP AT bevorzugen

IF line_exists( my_table[ key = 'A' ] ).

drückt die Absicht klarer und kürzer aus als

" Anti-Muster
READ TABLE my_table TRANSPORTING NO FIELDS WITH KEY key = 'A'.
IF sy-subrc = 0.

oder sogar

" Anti-Muster
LOOP AT my_table REFERENCE INTO DATA(line) WHERE key = 'A'.
  line_exists = abap_true.
  EXIT.
ENDLOOP.

READ TABLE gegenüber LOOP AT bevorzugen

READ TABLE my_table REFERENCE INTO DATA(line) WITH KEY key = 'A'.

drückt die Absicht klarer und kürzer aus als

" Anti-Muster
LOOP AT my_table REFERENCE INTO DATA(line) WHERE key = 'A'.
  EXIT.
ENDLOOP.

oder sogar

" Anti-Muster
LOOP AT my_table REFERENCE INTO DATA(line).
  IF line->key = 'A'.
    EXIT.
  ENDIF.
ENDLOOP.

LOOP AT WHERE gegenüber verschachteltem IF vorziehen

LOOP AT my_table REFERENCE INTO DATA(line) WHERE key = 'A'.

drückt die Absicht klarer und kürzer aus als

LOOP AT my_table REFERENCE INTO DATA(line).
  IF line->key = 'A'.
    EXIT.
  ENDIF.
ENDLOOP.

Vermeide unnötige Tabellenlesungen

Wenn Du erwartest, dass eine Zeile vorhanden ist, lese einmal und reagiere auf die Ausnahme,

TRY.
    DATA(row) = my_table[ key = input ].
  CATCH cx_sy_itab_line_not_found.
    RAISE EXCEPTION NEW /clean/my_data_not_found( ).
ENDTRY.

anstatt den Hauptkontrollfluss mit einem doppelten Lesevorgang zu behindern und zu verlangsamen.

" Anti-Muster
IF NOT line_exists( my_table[ key = input ] ).
  RAISE EXCEPTION NEW /clean/my_data_not_found( ).
ENDIF.
DATA(row) = my_table[ key = input ].

Strings

Verwendung von ` zur Definition von Literalen

CONSTANTS some_constant TYPE string VALUE `ABC`.
DATA(some_string) = `ABC`.  " --> TYPE string

Verzichte auf die Verwendung von ‚, da dies eine überflüssige Typumwandlung bedeutet und den Leser verwirrt, ob er es mit einem CHAR oder STRING zu tun hat:

" Anti-Muster
DATA some_string TYPE string.
some_string = 'ABC'.

| ist im Allgemeinen in Ordnung, kann aber nicht für CONSTANTS verwendet werden und fügt unnötigen Overhead hinzu, wenn ein fester Wert angegeben wird:

" Anti-Muster
DATA(some_string) = |ABC|.

Verwende |, um Text zusammenzusetzen

DATA(message) = |Received HTTP code { status_code } with message { text }|.

String-Templates heben besser hervor, was literal und was variabel ist, insbesondere wenn Du mehrere Variablen in einen Text einbettest.

" Anti-Muster
DATA(message) = `Received an unexpected HTTP ` && status_code && ` with message ` && text.

Booleans

Boolesche Operatoren klug einsetzen

Wir stoßen häufig auf Fälle, in denen Boolesche Werte eine natürliche Wahl zu sein scheinen

" Anti-Muster
is_archived = abap_true.

bis eine Änderung des Blickwinkels nahelegt, dass wir eine Aufzählung hätten wählen sollen.

archiving_status = /clean/archivation_status=>archiving_in_process.

Im Allgemeinen sind Booleans eine schlechte Wahl, um zwischen verschiedenen Arten von Dingen zu unterscheiden, da man fast immer auf Fälle stößt, die nicht ausschließlich das eine oder das andere sind

assert_true( xsdbool( document->is_archived( ) = abap_true AND
                      document->is_partially_archived( ) = abap_true ) ).

ABAP_BOOL für Boolesche Werte verwenden

DATA has_entries TYPE abap_bool.

Verwende nicht den generischen Typ „char1“. Obwohl er technisch kompatibel ist, verschleiert er die Tatsache, dass es sich um eine Boolesche Variable handelt.

Vermeide auch andere Boolesche Typen, da sie oft seltsame Nebeneffekte haben. Zum Beispiel unterstützt „boolean“ einen dritten Wert „undefined“, der zu subtilen Programmfehlern führt.

In einigen Fällen benötigst Du möglicherweise ein Datenwörterbuchelement, zum Beispiel für DynPro-Felder. „abap_bool“ kann hier nicht verwendet werden, da es im Typ-Pool „abap“ definiert ist und nicht im Datenwörterbuch. In diesem Fall greife auf „abap_boolean“ zurück. Erstelle Dein eigenes Datenwörterbuchelement, wenn Du eine benutzerdefinierte Beschreibung benötigst.

ABAP ist möglicherweise die einzige Programmiersprache, die nicht über einen universellen Booleschen Datentyp verfügt. Dennoch ist das Vorhandensein eines solchen zwingend erforderlich. Diese Empfehlung basiert auf den ABAP-Programmierrichtlinien.

ABAP_TRUE und ABAP_FALSE für Vergleiche verwenden

has_entries = abap_true.
IF has_entries = abap_false.

Verwende nicht die Zeichenäquivalente ‚X‘ und ‚ ‚ oder Leerzeichen; sie erschweren es, zu erkennen, dass es sich um einen booleschen Ausdruck handelt:

" Anti-Muster
has_entries = 'X'.
IF has_entries = space.

Vermeide Vergleiche mit INITIAL – es zwingt Lesende dazu, sich daran zu erinnern, dass der Standardwert von „abap_bool“ „abap_false“ ist:

" Anti-Muster
IF has_entries IS NOT INITIAL.

ABAP ist möglicherweise die einzige Programmiersprache, die keine integrierten „Konstanten“ für „wahr“ und „falsch“ hat. Dennoch ist es unerlässlich, sie zu haben. Diese Empfehlung basiert auf den ABAP-Programmierrichtlinien.

Verwende „XSDBOOL“, um boolesche Variablen festzulegen.

DATA(has_entries) = xsdbool( line IS NOT INITIAL ).

Das äquivalente IF-THEN-ELSE ist viel länger und bringt keinen Mehrwert:

" Anti-Muster
IF line IS INITIAL.
  has_entries = abap_false.
ELSE.
  has_entries = abap_true.
ENDIF.

„XSDBOOL“ ist die beste Methode für unseren Zweck, da sie direkt ein „char1“ erzeugt, das am besten zu unserem booleschen Typ „abap_bool“ passt. Die äquivalenten Funktionen „boolc“ und „boolx“ erzeugen unterschiedliche Typen und führen eine unnötige implizite Typumwandlung durch.

Wir sind uns einig, dass der Name „xsdbool“ unglücklich und irreführend ist; schließlich interessieren wir uns überhaupt nicht für die Teile der „XML Schema Definition“, die das Präfix „xsd“ nahelegt.

Eine mögliche Alternative zu „xsdbool“ ist die COND-Ternärform. Ihre Syntax ist intuitiv, aber etwas länger, da sie unnötigerweise das „THEN abap_true“-Segment wiederholt und Kenntnisse über den impliziten Standardwert „abap_false“ erfordert. Aus diesem Grund schlagen wir sie nur als sekundäre Lösung vor.

DATA(has_entries) = COND abap_bool( WHEN line IS NOT INITIAL THEN abap_true ).

Bedingungen

Versuche, Bedingungen positiv zu formulieren.

IF has_entries = abap_true.

Zum Vergleich, sieh dir an, wie schwer verständlich die gleiche Aussage wird, wenn sie umgekehrt wird:

" Anti-Muster
IF has_no_entries = abap_false.

Das „Versuch“ im Abschnittstitel bedeutet, dass Du dies nicht so erzwingen sollst, dass Du am Ende mit leeren IF-Verzweigungen endest:

" Anti-Muster
IF has_entries = abap_true.
ELSE.
  " only do something in the ELSE block, IF remains empty
ENDIF.

IS NOT gegenüber NOT IS bevorzugen

IF variable IS NOT INITIAL.
IF variable NP 'TODO*'.
IF variable <> 42.

Die Verneinung ist logisch äquivalent, erfordert jedoch eine „mentale Umkehr“, die es schwerer verständlich macht.

" Anti-Muster
IF NOT variable IS INITIAL.
IF NOT variable CP 'TODO*'.
IF NOT variable = 42.

Erwäge die Verwendung prädikativer Methodenaufrufe für boolesche Methoden

Der prädikative Methodenaufruf für boolesche Methoden, z. B.

IF [ NOT ] condition_is_fulfilled( ).

Der prädikative Methodenaufruf für boolesche Methoden ist nicht nur sehr kompakt, sondern er ermöglicht es auch, den Code näher an die natürliche Sprache heranzuführen als der Vergleichsausdruck:

" Anti-Muster
IF condition_is_fulfilled( ) = abap_true / abap_false.

Beachte, dass der prädikative Methodenaufruf „… meth( ) …“ nur eine verkürzte Form von „… meth( ) IS NOT INITIAL …“ ist. Siehe „Prädikativer Methodenaufruf“ in der ABAP-Keyword-Dokumentation. Aus diesem Grund sollte die verkürzte Form nur für Methoden verwendet werden, die Typen zurückgeben, bei denen der nicht-initialisierte Wert die Bedeutung von „wahr“ hat und der initialisierte Wert die Bedeutung von „falsch“ hat.

Erwäge die Zerlegung komplexer Bedingungen

Bedingungen können einfacher werden, wenn Du sie in ihre elementaren Bestandteile zerlegst, die sie ausmachen:

DATA(example_provided) = xsdbool( example_a IS NOT INITIAL OR
                                  example_b IS NOT INITIAL ).

DATA(one_example_fits) = xsdbool( applies( example_a ) = abap_true OR
                                  applies( example_b ) = abap_true OR
                                  fits( example_b ) = abap_true ).

IF example_provided = abap_true AND
   one_example_fits = abap_true.

anstatt alles an Ort und Stelle zu belassen:

" Anti-Muster
IF ( example_a IS NOT INITIAL OR
     example_b IS NOT INITIAL ) AND
   ( applies( example_a ) = abap_true OR
     applies( example_b ) = abap_true OR
     fits( example_b ) = abap_true ).

Erwäge das Extrahieren komplexer Bedingungen

Es ist fast immer eine gute Idee, komplexe Bedingungen in eigene Methoden auszulagern:

IF is_provided( example ).

METHOD is_provided.
  DATA(is_filled) = xsdbool( example IS NOT INITIAL ).
  DATA(is_working) = xsdbool( applies( example ) = abap_true OR
                              fits( example ) = abap_true ).
  result = xsdbool( is_filled = abap_true AND
                    is_working = abap_true ).
ENDMETHOD.

Ifs

Keine leeren IF-Verzweigungen

IF has_entries = abap_false.
  " do some magic
ENDIF.

ist kürzer und klarer als:

" Anti-Muster
IF has_entries = abap_true.
ELSE.
  " do some magic
ENDIF.

Bevorzuge CASE gegenüber ELSE IF für mehrere alternative Bedingungen

CASE type.
  WHEN type-some_type.
    " ...
  WHEN type-some_other_type.
    " ...
  WHEN OTHERS.
    RAISE EXCEPTION NEW /clean/unknown_type_failure( ).
ENDCASE.

CASE erleichtert das Erkennen einer Gruppe von Alternativen, die sich gegenseitig ausschließen. Es kann schneller sein als eine Serie von IFs, da es sich in einen anderen Mikroprozessorbefehl übersetzen kann, anstatt eine Serie nacheinander ausgewerteter Bedingungen zu sein. Du kannst schnell neue Fälle einführen, ohne die unterscheidende Variable immer wieder wiederholen zu müssen. Die Anweisung verhindert sogar einige Fehler, die auftreten können, wenn versehentlich die IF-ELSEIFs verschachtelt werden.

" Anti-Muster
IF type = type-some_type.
  " ...
ELSEIF type = type-some_other_type.
  " ...
ELSE.
  RAISE EXCEPTION NEW /dirty/unknown_type_failure( ).
ENDIF.

Halte die Verschachtelungstiefe niedrig

" anti-pattern
IF <this>.
  IF <that>.
  ENDIF.
ELSE.
  IF <other>.
  ELSE.
    IF <something>.
    ENDIF.
  ENDIF.
ENDIF.

Verschachtelte IFs werden schnell schwer verständlich und erfordern eine exponentiell steigende Anzahl von Testfällen für eine vollständige Abdeckung.

Entscheidungsbäume können normalerweise durch Bildung von Untermethoden und Einführung von booleschen Hilfsvariablen aufgeteilt werden.

Andere Fälle können vereinfacht werden, indem IFs zusammengeführt werden, wie zum Beispiel:

IF <this> AND <that>.

anstatt der unnötig verschachtelten:

" Anti-Muster
IF <this>.
  IF <that>.

Reguläre Ausdrücke

Bevorzuge einfachere Methoden gegenüber regulären Ausdrücken

IF input IS NOT INITIAL.
" IF matches( val = input  regex = '.+' ).

WHILE contains( val = input  sub = 'abc' ).
" WHILE contains( val = input  regex = 'abc' ).

Reguläre Ausdrücke werden schnell schwer verständlich. Einfache Fälle sind in der Regel ohne sie leichter zu bewältigen.

Reguläre Ausdrücke verbrauchen in der Regel auch mehr Speicher und Verarbeitungszeit, da sie in einen Ausdrucksbaum analysiert und zur Laufzeit in einen ausführbaren Abgleicher kompiliert werden müssen. Einfache Lösungen kommen oft mit einer geradlinigen Schleife und einer temporären Variablen aus.

Bevorzuge grundlegende Prüfungen gegenüber regulären Ausdrücken

CALL FUNCTION 'SEO_CLIF_CHECK_NAME'
  EXPORTING
    cls_name = class_name
  EXCEPTIONS
    ...

anstatt Dinge neu zu erfinden.

" Anti-Muster
DATA(is_valid) = matches( val     = class_name
                          pattern = '[A-Z][A-Z0-9_]{0,29}' ).

Erwäge das Zusammenstellen komplexer regulärer Ausdrücke

CONSTANTS class_name TYPE string VALUE `CL\_.*`.
CONSTANTS interface_name TYPE string VALUE `IF\_.*`.
DATA(object_name) = |{ class_name }\|{ interface_name }|.

Einige komplexe reguläre Ausdrücke werden einfacher, wenn Du dem Leser zeigst, wie sie aus elementaren Bestandteilen aufgebaut sind.

Klassen

Klassen: Objektorientierung

Bevorzuge Objekte gegenüber statischen Klassen

Statische Klassen geben alle Vorteile auf, die durch die Objektorientierung im ersten Schritt erzielt wurden. Insbesondere machen sie es fast unmöglich, Abhängigkeiten in Unit-Tests durch Testdoubles zu ersetzen.

Wenn Du darüber nachdenkst, ob eine Klasse oder Methode statisch sein soll, wird die Antwort fast immer „nein“ sein.

Eine akzeptierte Ausnahme von dieser Regel sind einfache Typen-Utility-Klassen. Deren Methoden erleichtern die Interaktion mit bestimmten ABAP-Typen. Sie sind nicht nur völlig zustandslos, sondern so grundlegend, dass sie wie ABAP-Anweisungen oder eingebaute Funktionen aussehen. Der entscheidende Faktor ist, dass ihre Verwender sie so eng in ihren Code integrieren, dass sie sie in Unit-Tests tatsächlich nicht mocken möchten.

CLASS /clean/string_utils DEFINITION [...].
  CLASS-METHODS trim
   IMPORTING
     string        TYPE string
   RETURNING
     VALUE(result) TYPE string.
ENDCLASS.

METHOD retrieve.
  DATA(trimmed_name) = /clean/string_utils=>trim( name ).
  result = read( trimmed_name ).
ENDMETHOD.

Bevorzuge Zusammensetzung gegenüber Vererbung.

Vermeide den Aufbau von Klassenhierarchien mit Vererbung. Bevorzuge stattdessen die Zusammensetzung.

Eine saubere Vererbung ist schwer zu gestalten, weil Regeln wie das Liskov-Substitutionsprinzip eingehalten werden müssen. Sie ist auch schwer zu verstehen, weil Menschen die Leitprinzipien hinter der Hierarchie erkennen und verinnerlichen müssen. Vererbung reduziert die Wiederverwendung, da Methoden tendenziell nur für Unterklassen verfügbar gemacht werden. Sie erschwert auch das Refactoring, da das Verschieben oder Ändern von Mitgliedern oft Änderungen am gesamten Hierarchiebaum erfordert.

Zusammensetzung bedeutet, dass Du kleine, unabhängige Objekte entwirfst, von denen jedes einen bestimmten Zweck erfüllt. Diese Objekte können durch einfache Delegations- und Fassadenmuster in komplexere Objekte umgeformt werden. Die Zusammensetzung kann zwar mehr Klassen erzeugen, hat ansonsten jedoch keine weiteren Nachteile.

Lass diese Regel dich nicht davon abhalten, Vererbung an den richtigen Stellen zu verwenden. Es gibt gute Anwendungen für Vererbung, zum Beispiel das Composite-Designmuster. Frage dich einfach kritisch, ob Vererbung in deinem Fall wirklich mehr Vorteile als Nachteile bringt. Wenn du unsicher bist, ist die Zusammensetzung in der Regel die sicherere Wahl.

Mische nicht Zustandsbehaftetes (stateful) und Zustandsloses (stateless) in derselben Klasse

Vermische die paradigmen des zustandslosen und zustandsbehafteten Programmierens nicht in derselben Klasse.

Im zustandslosen Programmieren erhalten Methoden Eingabe und erzeugen Ausgabe, ohne Nebeneffekte, was zu Methoden führt, die unabhängig von Zeitpunkt und Reihenfolge der Aufrufe immer das gleiche Ergebnis produzieren.

CLASS /clean/xml_converter DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS convert
      IMPORTING
        file_content  TYPE xstring
      RETURNING
        VALUE(result) TYPE /clean/some_inbound_message.
ENDCLASS.

CLASS /clean/xml_converter IMPLEMENTATION.
  METHOD convert.
    cl_proxy_xml_transform=>xml_xstring_to_abap(
      EXPORTING
        xml       = file_content
        ext_xml   = abap_true
        svar_name = 'ROOT_NODE'
      IMPORTING
        abap_data = result ).
   ENDMETHOD.
ENDCLASS.

Im zustandsbehafteten Programmieren manipulieren wir den internen Zustand von Objekten durch ihre Methoden, was bedeutet, dass es voll von Nebeneffekten ist.

CLASS /clean/log DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS add_message IMPORTING message TYPE /clean/message.
  PRIVATE SECTION.
    DATA messages TYPE /clean/message_table.
ENDCLASS.

CLASS /clean/log IMPLEMENTATION.
  METHOD add_message.
    INSERT message INTO TABLE messages.
  ENDMETHOD.
ENDCLASS.

Beide Paradigmen sind in Ordnung und haben ihre Anwendungen. Das Mischen von ihnen in derselben Klasse führt jedoch zu Code, der schwer verständlich ist und mit undurchsichtigen Übertragungsfehlern und Synchronitätsproblemen sicher zum Scheitern verurteilt ist. Vermeide das.

Geltungsbereich

Standardmäßig global, ggf. nur lokal

Arbeite standardmäßig mit globalen Klassen. Verwende lokale Klassen nur, wenn es angemessen ist.

Globale Klassen sind diejenigen, die im Datenwörterbuch sichtbar sind. Lokale Klassen leben innerhalb eines Includes eines anderen Entwicklungsobjekts und sind nur für dieses andere Objekt sichtbar.

Lokale Klassen sind geeignet für:

  • Sehr spezifische private Datenstrukturen, beispielsweise einen Iterator für die Daten der globalen Klasse, der nur hier benötigt wird.
  • Das Extrahieren eines komplexen privaten Algorithmus, um beispielsweise den speziellen, mehrzweckigen Sortier-Aggregationsalgorithmus von anderen Teilen deines Klassen-Codes zu trennen.
  • Das ermöglicht das Mocking bestimmter Aspekte der globalen Klasse, z.B. durch Extrahieren aller Datenbankzugriffe in eine separate lokale Klasse, die in den Unit-Tests durch einen Testdoppelgänger ersetzt werden kann.

Lokale Klassen behindern die Wiederverwendung, da sie anderswo nicht verwendet werden können. Obwohl sie leicht extrahiert werden können, werden Menschen in der Regel Schwierigkeiten haben, sie überhaupt zu finden, was zu unerwünschter Code-Duplizierung führt. Die Orientierung, Navigation und das Debugging in sehr langen lokalen Klassen-Include-Dateien sind mühsam und ärgerlich. Da ABAP auf der Include-Ebene sperrt, können Menschen nicht gleichzeitig an verschiedenen Teilen des lokalen Includes arbeiten (was möglich wäre, wenn es separate globale Klassen wären).

Überdenke Deinen Einsatz von lokalen Klassen, wenn:

  • Dein lokales Include Dutzende von Klassen und Tausende von Zeilen Code umfasst.
  • Du globale Klassen als „Pakete“ betrachtest, die andere Klassen enthalten.
  • Deine globalen Klassen zu leeren Hüllen werden.
  • Du doppelten Code in separaten lokalen Includes wiederfindest.
  • Deine Entwickler sich gegenseitig ausschließen und nicht parallel arbeiten können.
  • Deine Aufwandsschätzungen in die Höhe schnellen, weil deine Teams Schwierigkeiten haben, die lokalen Teilbäume der anderen zu verstehen.

FINAL, wenn nicht zur Vererbung bestimmt

Mache Klassen, die nicht explizit für die Vererbung entworfen sind, FINAL.

Bei der Gestaltung der Zusammenarbeit von Klassen sollte deine erste Wahl die Zusammensetzung und nicht die Vererbung sein. Die Aktivierung der Vererbung ist keine leichte Entscheidung, da sie dich dazu zwingt, über Dinge wie PROTECTED vs. PRIVATE und das Liskov-Substitutionsprinzip nachzudenken und viele Design-Details festlegt. Wenn du diese Dinge in deinem Klassendesign nicht berücksichtigt hast, solltest du daher versehentliche Vererbung verhindern, indem du deine Klasse als FINAL markierst.

Es gibt natürlich einige gute Anwendungen für Vererbung, wie beispielsweise das Entwurfsmuster Composite. Business Add-Ins können auch nützlicher werden, indem sie Unterklassen ermöglichen, sodass der Kunde den Großteil des ursprünglichen Codes wiederverwenden kann. Beachte jedoch, dass in all diesen Fällen die Vererbung von Anfang an im Design eingebaut ist.

Unsaubere Klassen, die keine Schnittstellen implementieren, sollten als nicht-FINAL belassen werden, um Verbrauchern das Mocken in ihren Unit-Tests zu ermöglichen.

Mitglieder standardmäßig PRIVATE, nur bei Bedarf PROTECTED

Setze Attribute, Methoden und andere Klassenmitglieder standardmäßig auf PRIVATE.

Markiere sie nur als PROTECTED, wenn du Unterklassen ermöglichen möchtest, sie zu überschreiben.

Die internen Details von Klassen sollten anderen nur auf Bedarfsbasis zur Verfügung gestellt werden. Dies betrifft nicht nur externe Aufrufer, sondern auch Unterklassen. Informationen übermäßig verfügbar zu machen, kann zu subtilen Fehlern durch unerwartete Neudifinitionen führen und das Refactoring behindern, da Außenstehende Mitglieder an Ort und Stelle fixieren, die immer noch veränderbar sein sollten.

Erwäge die Verwendung von Unveränderlichkeitsmuster (Immutable Objects) anstelle von Gettern

Ein unveränderliches Objekt ist ein Objekt, das sich nach seiner Konstruktion nicht mehr verändert. Für diese Art von Objekten solltest Du öffentliche schreibgeschützte Attribute anstelle von Getter-Methoden verwenden.

CLASS /clean/some_data_container DEFINITION.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        a TYPE i
        b TYPE c
        c TYPE d.
    DATA a TYPE i READ-ONLY.
    DATA b TYPE c READ-ONLY.
    DATA c TYPE d READ-ONLY.
ENDCLASS.

anstelle von

CLASS /dirty/some_data_container DEFINITION.
  PUBLIC SECTION.
    METHODS get_a ...
    METHODS get_b ...
    METHODS get_c ...
  PRIVATE SECTION.
    DATA a TYPE i.
    DATA b TYPE c.
    DATA c TYPE d.
ENDCLASS.

Achtung: Verwende für Objekte, die veränderliche Werte haben, keine öffentlichen schreibgeschützten Attribute. Andernfalls müssen diese Attribute immer auf dem neuesten Stand gehalten werden, unabhängig davon, ob ihr Wert von einem anderen Code benötigt wird oder nicht.

Verwende „READ-ONLY“ sparsam

Viele moderne Programmiersprachen, insbesondere Java, empfehlen, Klassenmember, wo immer möglich, als schreibgeschützt zu deklarieren, um versehentliche Seiteneffekte zu verhindern.

Obwohl ABAP die READ-ONLY-Erweiterung für Datenanweisungen bietet, empfehlen wir, sie sparsam zu verwenden.

Erstens steht diese Erweiterung nur im PUBLIC SECTION zur Verfügung, was ihre Anwendbarkeit drastisch einschränkt. Sie kann weder zu geschützten oder privaten Membern noch zu lokalen Variablen in einer Methode hinzugefügt werden.

Zweitens funktioniert die Erweiterung subtil anders, als man es von anderen Programmiersprachen erwarten könnte: READ-ONLY-Daten können immer noch frei von jeder Methode innerhalb der Klasse selbst, ihrer befreundeten Klassen und deren Unterklassen geändert werden. Dies widerspricht dem in anderen Sprachen weit verbreiteten Verhalten „einmal schreiben, niemals ändern“. Diese Unterschiede können zu unangenehmen Überraschungen führen.

Um Missverständnisse zu vermeiden: Das Schützen von Variablen vor versehentlicher Änderung ist eine bewährte Praxis. Wir würden empfehlen, sie auch auf ABAP anzuwenden, wenn es eine entsprechende Anweisung gäbe.

Konstruktor

Bevorzuge „NEW“ gegenüber „CREATE OBJECT“

DATA object TYPE REF TO /clean/some_number_range.
object = NEW #( '/CLEAN/CXTGEN' )
...
DATA(object) = NEW /clean/some_number_range( '/CLEAN/CXTGEN' ).
...
DATA(object) = CAST /clean/number_range( NEW /clean/some_number_range( '/CLEAN/CXTGEN' ) ).

anstatt des unnötig längeren:

" Anti-Muster
DATA object TYPE REF TO /dirty/some_number_range.
CREATE OBJECT object
  EXPORTING
    number_range = '/DIRTY/CXTGEN'.

Außer wenn du natürlich dynamische Typen benötigst.

CREATE OBJECT number_range TYPE (dynamic_type)
  EXPORTING
    number_range = '/CLEAN/CXTGEN'.

Wenn Deine globale Klasse CREATE PRIVATE ist, lasse den CONSTRUCTOR öffentlich.

CLASS /clean/some_api DEFINITION PUBLIC FINAL CREATE PRIVATE.
  PUBLIC SECTION.
    METHODS constructor.

Wir sind einverstanden, dass dies sich widerspricht. Gemäß dem Artikel „Instanzkonstruktor“ in der ABAP-Hilfe ist es jedoch erforderlich, den CONSTRUCTOR im PUBLIC SECTION anzugeben, um eine korrekte Kompilierung und Syntaxvalidierung sicherzustellen.

Dies gilt nur für globale Klassen. In lokalen Klassen sollte der Konstruktor privat sein, so wie es sein sollte.

Bevorzuge mehrere statische Erstellungsmethoden anstelle optionaler Parameter.

CLASS-METHODS describe_by_data IMPORTING data TYPE any [...]
CLASS-METHODS describe_by_name IMPORTING name TYPE any [...]
CLASS-METHODS describe_by_object_ref IMPORTING object_ref TYPE REF TO object [...]
CLASS-METHODS describe_by_data_ref IMPORTING data_ref TYPE REF TO data [...]

ABAP unterstützt keine Überladung. Verwende Namensvariationen und keine optionalen Parameter, um die gewünschte Semantik zu erreichen.

" Anti-Muster
METHODS constructor
  IMPORTING
    data       TYPE any OPTIONAL
    name       TYPE any OPTIONAL
    object_ref TYPE REF TO object OPTIONAL
    data_ref   TYPE REF TO data OPTIONAL
  [...]

Die allgemeine Richtlinie „Teile Methoden anstelle von Hinzufügen optionaler Parameter“ erläutert die Gründe dafür.

Erwäge die Auflösung von komplexen Konstruktionen zu einer mehrstufigen Konstruktion mit dem Builder-Designmuster.

Verwende beschreibende Namen für mehrere Erstellungsmethoden

Gute Wörter, um Erstellungsmethoden zu beginnen, sind new_, create_ und construct_. Menschen verbinden sie intuitiv mit der Erstellung von Objekten. Sie fügen sich auch gut zu Verbphrasen wie new_from_template, create_as_copy oder create_by_name hinzu.

CLASS-METHODS new_describe_by_data IMPORTING p_data TYPE any [...]
CLASS-METHODS new_describe_by_name IMPORTING p_name TYPE any [...]
CLASS-METHODS new_describe_by_object_ref IMPORTING p_object_ref TYPE REF TO object [...]
CLASS-METHODS new_describe_by_data_ref IMPORTING p_data_ref TYPE REF TO data [...]

anstatt etwas Bedeutungsloses wie:

" Anti-Muster
CLASS-METHODS create_1 IMPORTING p_data TYPE any [...]
CLASS-METHODS create_2 IMPORTING p_name TYPE any [...]
CLASS-METHODS create_3 IMPORTING p_object_ref TYPE REF TO object [...]
CLASS-METHODS create_4 IMPORTING p_data_ref TYPE REF TO data [...]

Erstelle Singleton-Instanzen nur dann, wenn mehrere Instanzen keinen Sinn ergeben

METHOD new.
  IF singleton IS NOT BOUND.
    singleton = NEW /clean/my_class( ).
  ENDIF.
  result = singleton.
ENDMETHOD.

Wende das Singleton-Muster an, wenn dein objektorientiertes Design besagt, dass das Vorhandensein einer zweiten Instanz keinen Sinn ergibt. Verwende es, um sicherzustellen, dass jeder Verbraucher mit demselben Objekt in demselben Zustand und denselben Daten arbeitet.

Verwende das Singleton-Muster nicht aus Gewohnheit oder weil eine Performance-Regel es vorschreibt. Es handelt sich um das am häufigsten überstrapazierte und falsch angewandte Muster, das unerwartete Nebeneffekte erzeugt und das Testen unnötig kompliziert macht. Wenn es keine designbedingten Gründe für ein einheitliches Objekt gibt, überlasse diese Entscheidung dem Verbraucher – er kann immer noch dasselbe Ergebnis erzielen, beispielsweise mit einer Factory, durch Mittel außerhalb des Konstruktors.

Methoden

Diese Regeln gelten für Methoden in Klassen und Funktionsmodulen.

Aufrufe

Rufe keine statischen Methoden über Instanzvariablen auf.

Um eine statische Methode aufzurufen, verwende:

cl_my_class=>static_method( ).

Anstatt sie über eine Instanzvariable zu cl_my_class aufzurufen.

" Anti-Muster
lo_my_instance->static_method( ).

Eine statische Methode ist an die Klasse selbst angehängt, und sie über eine Instanzvariable aufzurufen, kann potenziell zu Verwirrung führen.

Es ist in Ordnung, eine statische Methode derselben Klasse in einer anderen statischen Methode ohne Qualifizierung aufzurufen.

METHOD static_method.
  another_static_method( ).
  yet_another( ).
ENDMETHOD.

Innerhalb einer Instanzmethode, auch wenn du eine statische Methode derselben Klasse aufrufst, solltest du den Aufruf dennoch mit dem Klassennamen qualifizieren:

CLASS cl_my_class IMPLEMENTATION.

  METHOD instance_method.
    cl_my_class=>a_static_method( ).
    another_instance_method( ).
  ENDMETHOD.

  ...

Greife nicht auf Typen über Instanzvariablen zu

Wenn Du einen Datentyp verwendest, der in einer Klasse oder einem Interface definiert ist, greife auf die Typdefinition über die Klasse/das Interface zu und nicht über eine Instanz der Klasse/des Interface.

CLASS lcl DEFINITION.
  PUBLIC SECTION.
    TYPES foo TYPE i.
ENDCLASS.
CLASS lcl IMPLEMENTATION.
ENDCLASS.

INTERFACE lif.
  TYPES blah TYPE lcl=>foo.  
ENDINTERFACE.

Die Verwendung der Instanz für den Datentyp wäre verwirrend, da sie darauf hindeutet, dass der Typ instanzspezifisch ist.

" Anti-Muster
CLASS lcl DEFINITION.
  PUBLIC SECTION.
    TYPES foo TYPE i.
ENDCLASS.
CLASS lcl IMPLEMENTATION.
ENDCLASS.

INTERFACE lif.
  DATA(ref) = new lcl( ).
  TYPES blah TYPE ref->foo.
ENDINTERFACE.

Bevorzuge funktionale Aufrufe gegenüber prozeduralen Aufrufen.

modify->update( node           = /clean/my_bo_c=>node-item
                key            = item->key
                data           = item
                changed_fields = changed_fields ).

anstatt des unnötig längeren:

" Anti-Muster
CALL METHOD modify->update
  EXPORTING
    node           = /dirty/my_bo_c=>node-item
    key            = item->key
    data           = item
    changed_fields = changed_fields.

Wenn die dynamische Typisierung funktionale Aufrufe verbietet, weiche auf den prozeduralen Stil aus.

CALL METHOD modify->(method_name)
  EXPORTING
    node           = /clean/my_bo_c=>node-item
    key            = item->key
    data           = item
    changed_fields = changed_fields.

Viele der detaillierten Regeln unten sind nur spezifischere Variationen dieses Ratschlags.

Omit RECEIVING

DATA(sum) = aggregate_values( values ).

anstatt des unnötig längeren:

" Anti-Muster
aggregate_values(
  EXPORTING
    values = values
  RECEIVING
    result = DATA(sum) ).

Lasse das optionale Schlüsselwort EXPORTING weg

modify->update( node           = /clean/my_bo_c=>node-item
                key            = item->key
                data           = item
                changed_fields = changed_fields ).

anstatt des unnötig längeren:

" Anti-Muster
modify->update(
  EXPORTING
    node           = /dirty/my_bo_c=>node-item
    key            = item->key
    data           = item
    changed_fields = changed_fields ).

Lasse den Parametername bei Ein-Parameter-Aufrufen weg

DATA(unique_list) = remove_duplicates( list ).

anstatt des unnötig längeren:

" Anti-Muster
DATA(unique_list) = remove_duplicates( list = list ).

Es gibt jedoch Fälle, in denen allein der Methodenname nicht ausreichend klar ist und die Wiederholung des Parameternamens die Verständlichkeit weiter erhöhen kann:

car->drive( speed = 50 ).
update( asynchronous = abap_true ).

Lasse die Selbstreferenz „me“ weg, wenn Du auf ein Instanzattribut oder eine Methode zugreifst

Da die Selbstreferenz „me->“ implizit vom System gesetzt wird, lasse sie weg, wenn du auf ein Instanzattribut oder eine Methode zugreifst.

DATA(sum) = aggregate_values( values ).

anstatt des unnötig längeren:

" Anti-Muster
DATA(sum) = aggregate_values( me->values ).
" Anti-Muster
DATA(sum) = me->aggregate_values( values ).

Es sei denn, es besteht ein Sichtbarkeitskonflikt zwischen einer lokalen Variable oder einem Importparameter und einem Instanzattribut.

me->logger = logger.

Methoden: Objektorientierung

Bevorzuge Instanzmethoden gegenüber statischen Methoden

Methoden sollten standardmäßig Instanzmember sein. Instanzmethoden spiegeln besser die „Objektorientierung“ der Klasse wider. Sie können in Unit-Tests leichter gemockt werden.

METHODS publish.

Methods should be static only in exceptional cases, such as static creation methods.

CLASS-METHODS create_instance
  RETURNING
    VALUE(result) TYPE REF TO /clean/blog_post.

Öffentliche Instanzmethoden sollten Teil eines Interfaces sein.

Öffentliche Instanzmethoden sollten immer Teil eines Interfaces sein. Dies entkoppelt Abhängigkeiten und vereinfacht das Mocking in Unit-Tests.

METHOD /clean/blog_post~publish.

In einer sauberen objektorientierten Programmierung macht es wenig Sinn, eine Methode öffentlich zu haben, ohne dass sie Teil eines Interfaces ist – mit wenigen Ausnahmen wie Aufzählungsklassen, die niemals eine alternative Implementierung haben werden und niemals in Testfällen gemockt werden.

Parameteranzahl

Strebe nach wenigen IMPORTING-Parametern, idealerweise weniger als drei

FUNCTION seo_class_copy
  IMPORTING
    clskey      TYPE seoclskey
    new_clskey  TYPE seoclskey
    config      TYPE class_copy_config
  EXPORTING
    ...

wäre viel verständlicher als:

" Anti-Muster
FUNCTION seo_class_copy
  IMPORTING
    clskey                 TYPE seoclskey
    new_clskey             TYPE seoclskey
    access_permission      TYPE seox_boolean DEFAULT seox_true
    VALUE(save)            TYPE seox_boolean DEFAULT seox_true
    VALUE(suppress_corr)   TYPE seox_boolean DEFAULT seox_false
    VALUE(suppress_dialog) TYPE seox_boolean DEFAULT seox_false
    VALUE(authority_check) TYPE seox_boolean DEFAULT seox_true
    lifecycle_manager      TYPE REF TO if_adt_lifecycle_manager OPTIONAL
    lock_handle            TYPE REF TO if_adt_lock_handle OPTIONAL
    VALUE(suppress_commit) TYPE seox_boolean DEFAULT seox_false
  EXPORTING
    ...

Zu viele Eingabeparameter lassen die Komplexität einer Methode explodieren, da sie eine exponentielle Anzahl von Kombinationen behandeln muss. Viele Parameter sind ein Hinweis darauf, dass die Methode möglicherweise mehr als eine Aufgabe erfüllt.

Du kannst die Anzahl der Parameter reduzieren, indem du sie in sinnvolle Gruppen mit Strukturen und Objekten kombinierst.

Teile Methoden auf, anstatt optionale Parameter hinzuzufügen

METHODS do_one_thing IMPORTING what_i_need TYPE string.
METHODS do_another_thing IMPORTING something_else TYPE i.

um die gewünschte Semantik zu erreichen, da ABAP keine Überladung unterstützt.

" Anti-Muster
METHODS do_one_or_the_other
  IMPORTING
    what_i_need    TYPE string OPTIONAL
    something_else TYPE i OPTIONAL.

Optionale Parameter verwirren Aufrufer:

  • Welche sind wirklich erforderlich?
  • Welche Kombinationen sind gültig?
  • Welche schließen sich gegenseitig aus?

Mehrere Methoden mit spezifischen Parametern für den Anwendungsfall vermeiden diese Verwirrung, indem sie klare Anleitung dazu geben, welche Parameterkombinationen gültig und erwartet werden.

Verwende PREFERRED PARAMETER sparsam.

Die Hinzufügung PREFERRED PARAMETER macht es schwer zu erkennen, welcher Parameter tatsächlich übergeben wird, was die Codeverständlichkeit erschwert. Die Minimierung der Anzahl von Parametern, insbesondere optionaler Parameter, reduziert automatisch die Notwendigkeit von PREFERRED PARAMETER.

Gib RETURN, EXPORT oder CHANGE genau einen Parameter zurück.

Eine gute Methode tut eine Sache, und das sollte sich in der Methode auch dadurch widerspiegeln, dass die Methode genau eine Sache zurückgibt. Wenn die Ausgabeparameter deiner Methode keine logische Einheit bilden, tut deine Methode mehr als eine Sache und du solltest sie aufteilen.

Es gibt Fälle, in denen die Ausgabe eine logische Einheit bildet, die aus mehreren Dingen besteht. Diese werden am besten durch die Rückgabe einer Struktur oder eines Objekts repräsentiert:

TYPES:
  BEGIN OF check_result,
    result      TYPE result_type,
    failed_keys TYPE /bobf/t_frw_key,
    messages    TYPE /bobf/t_frw_message,
  END OF check_result.

METHODS check_business_partners
  IMPORTING
    business_partners TYPE business_partners
  RETURNING
    VALUE(result)     TYPE check_result.

anstelle von

" Anti-Muster
METHODS check_business_partners
  IMPORTING
    business_partners TYPE business_partners
  EXPORTING
    result            TYPE result_type
    failed_keys       TYPE /bobf/t_frw_key
    messages          TYPE /bobf/t_frw_message.

Besonders im Vergleich zu mehreren EXPORTING-Parametern ermöglicht dies den Menschen, den funktionalen Aufrufstil zu verwenden, erspart dir das Nachdenken über IS SUPPLIED und verhindert, dass die Menschen versehentlich vergessen, wichtige ERROR_OCCURRED-Informationen abzurufen.

Anstelle von mehreren optionalen Ausgabeparametern solltest du die Methode entsprechend sinnvoller Aufrufmuster aufteilen:

TYPES:
  BEGIN OF check_result,
    result      TYPE result_type,
    failed_keys TYPE /bobf/t_frw_key,
    messages    TYPE /bobf/t_frw_message,
  END OF check_result.

METHODS check
  IMPORTING
    business_partners TYPE business_partners
  RETURNING
    VALUE(result)     TYPE result_type.

METHODS check_and_report
  IMPORTING
    business_partners TYPE business_partners
  RETURNING
    VALUE(result)     TYPE check_result.

Parameterarten

Bevorzuge RETURNING gegenüber EXPORTING

METHODS square
  IMPORTING
    number        TYPE i
  RETURNING
    VALUE(result) TYPE i.

DATA(result) = square( 42 ).

Anstatt des unnötig längeren:

" Anti-Muster
METHODS square
  IMPORTING
    number TYPE i
  EXPORTING
    result TYPE i.

square(
  EXPORTING
    number = 42
  IMPORTING
    result = DATA(result) ).

RETURNING macht den Aufruf nicht nur kürzer, sondern ermöglicht auch Methodenverkettung und verhindert Fehler, bei denen Eingabe und Ausgabe identisch sind.

Das Zurückgeben großer Tabellen mit RETURNING ist in der Regel in Ordnung

Obwohl die ABAP-Sprachdokumentation und die Leistungshandbücher etwas anderes sagen, stoßen wir selten auf Fälle, in denen das Übergeben einer großen oder tief verschachtelten Tabelle in einem VALUE-Parameter wirklich Leistungsprobleme verursacht. Daher empfehlen wir, im Allgemeinen zu verwenden:

METHODS get_large_table
  RETURNING
    VALUE(result) TYPE /clean/some_table_type.

METHOD get_large_table.
  result = large_table.
ENDMETHOD.

DATA(my_table) = get_large_table( ).

Nur wenn es konkreten Beweis gibt (eine schlechte Leistungsmessung) für Deinen individuellen Fall, solltest Du auf den umständlicheren prozeduralen Stil zurückgreifen.

" Anti-Muster
METHODS get_large_table
  EXPORTING
    result TYPE /dirty/some_table_type.

METHOD get_large_table.
  result = large_table.
ENDMETHOD.

get_large_table( IMPORTING result = DATA(my_table) ).

Diese Sektion steht im Widerspruch zu den ABAP-Programmierrichtlinien und den Code Inspector-Checks, die beide empfehlen, dass große Tabellen per Referenz EXPORTIERT werden sollten, um Leistungseinbußen zu vermeiden. Wir konnten konsistent keine Leistungs- und Speicherprobleme reproduzieren und haben Hinweise auf Kernel-Optimierungen erhalten, die die RETURNING-Leistung im Allgemeinen verbessern, siehe „Sharing Between Dynamic Data Objects“ in der ABAP-Sprachhilfe.

Verwende entweder RETURNING, EXPORTING oder CHANGING, aber nicht eine Kombination von ihnen.

METHODS copy_class
  IMPORTING
    old_name      TYPE seoclsname
    new name      TYPE secolsname
  RETURNING
    VALUE(result) TYPE copy_result
  RAISING
    /clean/class_copy_failure.

anstatt verwirrende Mischungen wie:

" Anti-Muster
METHODS copy_class
  ...
  RETURNING
    VALUE(result)      TYPE vseoclass
  EXPORTING
    error_occurred     TYPE abap_bool
  CHANGING
    correction_request TYPE trkorr
    package            TYPE devclass.

Unterschiedliche Arten von Ausgabeparametern deuten darauf hin, dass die Methode mehr als eine Aufgabe erfüllt. Dies verwirrt den Leser und macht das Aufrufen der Methode unnötig kompliziert.

Eine akzeptable Ausnahme von dieser Regel könnten Builder sein, die ihre Eingabe beim Erstellen ihrer Ausgabe verbrauchen:

METHODS build_tree
  CHANGING
    tokens        TYPE tokens
  RETURNING
    VALUE(result) TYPE REF TO tree.

Dennoch können auch diese durch die Objektivierung der Eingabe verständlicher gestaltet werden:

METHODS build_tree
  IMPORTING
    tokens        TYPE REF TO token_stack
  RETURNING
    VALUE(result) TYPE REF TO tree.

Verwende CHANGING sparsam, wenn es geeignet ist

CHANGING sollte für Fälle reserviert werden, in denen eine vorhandene lokale Variable, die bereits gefüllt ist, nur an einigen Stellen aktualisiert wird:

METHODS update_references
  IMPORTING
    new_reference TYPE /bobf/conf_key
  CHANGING
    bo_nodes      TYPE root_nodes.

METHOD update_references.
  LOOP AT bo_nodes REFERENCE INTO DATA(bo_node).
    bo_node->reference = new_reference.
  ENDLOOP.
ENDMETHOD.

Zwinge Deine Aufrufer nicht, unnötige lokale Variablen einzuführen, nur um Deine CHANGING-Parameter zu liefern. Verwende CHANGING-Parameter nicht, um eine zuvor leere Variable zu füllen.

Split-Methode anstelle eines booleschen Eingabeparameters

Boolesche Eingabeparameter sind oft ein Indikator dafür, dass eine Methode zwei Dinge statt eines tut.

" Anti-Muster
METHODS update
  IMPORTING
    do_save TYPE abap_bool.

Außerdem neigen Methodenaufrufe mit einem einzigen – und damit unbenannten – booleschen Parameter dazu, die Bedeutung des Parameters zu verschleiern.

" Anti-Muster
update( abap_true ).  " Was bedeutet "true"? synchron? simulieren? übertragen?

Die Aufteilung der Methode kann den Code der Methoden vereinfachen und die verschiedenen Absichten besser beschreiben

update_without_saving( ).
update_and_save( ).

Die allgemeine Wahrnehmung legt nahe, dass Setter für boolesche Variablen in Ordnung sind:

METHODS set_is_deleted
  IMPORTING
    new_value TYPE abap_bool.

Parameter-Namen

Erwäge den Aufruf des RETURNING-Parameters RESULT

Gute Methodennamen sind normalerweise so gut, dass der RETURNING-Parameter keinen eigenen Namen braucht. Der Name würde kaum mehr tun, als den Methodennamen nachzuplappern oder etwas Offensichtliches zu wiederholen.

Die Wiederholung eines Mitgliedsnamens kann sogar zu Konflikten führen, die durch das Hinzufügen eines überflüssigen me-> aufgelöst werden müssen.

" Anti-Muster
METHODS get_name
  RETURNING
    VALUE(name) TYPE string.

METHOD get_name.
  name = me->name.
ENDMETHOD.

In diesen Fällen nennst Du den Parameter einfach RESULT, oder etwas wie RV_RESULT, wenn Du die ungarische Schreibweise bevorzugst.

Benenne den RETURNING-Parameter, wenn es nicht offensichtlich ist, wofür er steht, z. B. in Methoden, die me für die Methodenverkettung zurückgeben, oder in Methoden, die etwas erstellen, aber nicht die erstellte Entität zurückgeben, sondern nur ihren Schlüssel oder so.

Initialisierung der Parameter

Löschen oder Überschreiben der EXPORTING-Referenzparameter

Referenzparameter beziehen sich auf vorhandene Speicherbereiche, die bereits gefüllt sein können. Lösche oder überschreibe diese, um zuverlässige Daten zu erhalten:

METHODS square
  EXPORTING
    result TYPE i.

" clear
METHOD square.
  CLEAR result.
  " ...
ENDMETHOD.

" overwrite
METHOD square.
  result = cl_abap_math=>square( 2 ).
ENDMETHOD.

Code inspector und Checkman weisen auf EXPORTING-Variablen hin, die nie geschrieben werden. Verwende diese statischen Überprüfungen, um diese ansonsten eher obskure Fehlerquelle zu vermeiden.

Achte darauf, ob Eingabe und Ausgabe identisch sein können

Im Allgemeinen ist es eine gute Idee, den Parameter als erstes in der Methode nach den Typ- und Datendeklarationen zu löschen. Dadurch ist die Anweisung leicht zu erkennen und es wird vermieden, dass der noch enthaltene Wert versehentlich von späteren Anweisungen verwendet wird.

Einige Parameterkonfigurationen könnten jedoch dieselbe Variable als Ein- und Ausgabe verwenden. In diesem Fall würde ein frühes CLEAR den Eingabewert löschen, bevor er verwendet werden kann, was zu falschen Ergebnissen führt.

" Anti-Muster
DATA value TYPE i.

square_dirty(
  EXPORTING
    number = value
  IMPORTING
    result = value ).

METHOD square_dirty.
  CLEAR result.
  result = number * number.
ENDMETHOD.

Ziehe in Erwägung, solche Methoden umzugestalten und EXPORTING durch RETURNING zu ersetzen. Erwäge auch das Überschreiben des EXPORTING-Parameters in einer einzigen Ergebnisberechnungsanweisung. Wenn beides nicht passt, greife auf ein spätes CLEAR zurück.

VALUE-Parameter nicht löschen

Parameter, die mit VALUE arbeiten, werden als neue, separate Speicherbereiche übergeben, die per Definition leer sind. Sie dürfen nicht wieder gelöscht werden:

METHODS square
  EXPORTING
    VALUE(result) TYPE i.

METHOD square.
  " keine Notwendigkeit, das Ergebnis zu CLEARen
ENDMETHOD.

RETURNING-Parameter sind immer VALUE-Parameter, so dass Du sie nie löschen musst:

METHODS square
  RETURNING
    VALUE(result) TYPE i.

METHOD square.
  " keine Notwendigkeit, das Ergebnis zu CLEARen
ENDMETHOD.

Methode Körper

Mach eine Sache, mach sie gut, mach sie nur

Eine Methode sollte eine Sache tun, und nur eine. Sie sollte es auf die bestmögliche Weise tun.

Eine Methode tut wahrscheinlich nur eine Sache, wenn

  • sie nur wenige Eingabeparameter hat
  • sie keine booleschen Parameter enthält
  • sie genau einen Ausgabeparameter hat
  • sie klein ist
  • sie eine Abstraktionsebene herabsteigt
  • sie löst nur eine Art von Ausnahme aus
  • Du kannst keine sinnvollen anderen Methoden extrahieren
  • Du kannst die Anweisungen nicht sinnvoll in Abschnitte gruppieren

Konzentriere Dich auf den glücklichen Weg oder die Fehlerbehandlung, aber nicht auf beides

Als Spezialisierung der Regel „Mach eine Sache, mach sie gut, mach sie nur“ sollte eine Methode entweder dem glücklichen Weg folgen, für den sie gebaut wurde, oder dem Fehlerbehandlungsweg, falls sie es nicht kann, aber wahrscheinlich nicht beides.

" Anti-Muster
METHOD append_xs.
  IF input > 0.
    DATA(remainder) = input.
    WHILE remainder > 0.
      result = result && `X`.
      remainder = remainder - 1.
    ENDWHILE.
  ELSEIF input = 0.
    RAISE EXCEPTION /dirty/sorry_cant_do( ).
  ELSE.
    RAISE EXCEPTION cx_sy_illegal_argument( ).
  ENDIF.
ENDMETHOD.

kann zerlegt werden in

METHOD append_xs.
  validate( input ).
  DATA(remainder) = input.
  WHILE remainder > 0.
    result = result && `X`.
    remainder = remainder - 1.
  ENDWHILE.
ENDMETHOD.

METHOD validate.
  IF input = 0.
    RAISE EXCEPTION /dirty/sorry_cant_do( ).
  ELSEIF input < 0.
    RAISE EXCEPTION cx_sy_illegal_argument( ).
  ENDIF.
ENDMETHOD.

oder, um den Teil der Validierung zu betonen

METHOD append_xs.
  IF input > 0.
    result = append_xs_without_check( input ).
  ELSEIF input = 0.
    RAISE EXCEPTION /dirty/sorry_cant_do( ).
  ELSE.
    RAISE EXCEPTION cx_sy_illegal_argument( ).
  ENDIF.
ENDMETHOD.

METHOD append_xs_without_check.
  DATA(remainder) = input.
  WHILE remainder > 0.
    result = result && `X`.
    remainder = remainder - 1.
  ENDWHILE.
ENDMETHOD.

Eine Abstraktionsebene herabsteigen

Die Anweisungen in einer Methode sollten eine Abstraktionsebene unter der Methode selbst liegen. Dementsprechend sollten sie alle auf der gleichen Abstraktionsebene liegen.

METHOD create_and_publish.
  post = create_post( user_input ).
  post->publish( ).
ENDMETHOD.

statt verwirrender Mischungen von Konzepten auf niedriger Ebene (trim, to_upper, …) und hoher Ebene (publish, …) wie

" Anti-Muster
METHOD create_and_publish.
  post = NEW blog_post( ).
  DATA(user_name) = trim( to_upper( sy-uname ) ).
  post->set_author( user_name ).
  post->publish( ).
ENDMETHOD.

Eine zuverlässige Methode, um herauszufinden, welches Abstraktionsniveau das richtige ist, ist folgende: Lasse Dich vom Autor der Methode in wenigen, kurzen Worten erklären, was die Methode tut, ohne sich den Code anzusehen. Die Aufzählungspunkte, die er nennt, sind die Untermethoden, die die Methode aufrufen soll, oder die Anweisungen, die sie ausführen soll.

Methoden klein halten

Die Methoden sollten weniger als 20 Aussagen enthalten, optimal sind etwa 3 bis 5 Aussagen.

METHOD read_and_parse_version_filters.
  DATA(active_model_version) = read_random_version_under( model_guid ).
  DATA(filter_json) = read_model_version_filters( active_model_version-guid ).
  result = parse_model_version_filters( filter_json ).
ENDMETHOD.

Die folgende DATA-Deklaration allein reicht aus, um zu sehen, dass die umgebende Methode weit mehr als nur eine Sache tut:

" Anti-Muster
DATA:
  class           TYPE vseoclass,
  attributes      TYPE seoo_attributes_r,
  methods         TYPE seoo_methods_r,
  events          TYPE seoo_events_r,
  types           TYPE seoo_types_r,
  aliases         TYPE seoo_aliases_r,
  implementings   TYPE seor_implementings_r,
  inheritance     TYPE vseoextend,
  friendships     TYPE seof_friendships_r,
  typepusages     TYPE seot_typepusages_r,
  clsdeferrds     TYPE seot_clsdeferrds_r,
  intdeferrds     TYPE seot_intdeferrds_r,
  attribute       TYPE vseoattrib,
  method          TYPE vseomethod,
  event           TYPE vseoevent,
  type            TYPE vseotype,
  alias           TYPE seoaliases,
  implementing    TYPE vseoimplem,
  friendship      TYPE seofriends,
  typepusage      TYPE vseotypep,
  clsdeferrd      TYPE vseocdefer,
  intdeferrd      TYPE vseoidefer,
  new_clskey_save TYPE seoclskey.

Natürlich gibt es Fälle, in denen es nicht sinnvoll ist, eine größere Methode weiter zu reduzieren. Das ist völlig in Ordnung, solange die Methode auf eine Sache konzentriert bleibt:

METHOD decide_what_to_do.
  CASE temperature.
    WHEN burning.
      result = air_conditioning.
    WHEN hot.
      result = ice_cream.
    WHEN moderate.
      result = chill.
    WHEN cold.
      result = skiing.
    WHEN freezing.
      result = hot_cocoa.
  ENDCASE.
ENDMETHOD.

Dennoch ist es sinnvoll, zu prüfen, ob sich hinter dem ausführlichen Code ein geeigneteres Muster verbirgt:

METHOD decide_what_to_do.
  result = VALUE #( spare_time_activities[ temperature = temperature ] OPTIONAL ).
ENDMETHOD.

Wenn Methoden sehr klein geschnitten werden, kann sich dies negativ auf die Leistung auswirken, da die Anzahl der Methodenaufrufe steigt.

Kontrollfluss

Schnell scheitern

Validiere und scheitere so früh wie möglich:

METHOD do_something.
  IF input IS INITIAL.
    RAISE EXCEPTION cx_sy_illegal_argument( ).
  ENDIF.
  DATA(massive_object) = build_expensive_object_from( input ).
  result = massive_object->do_some_fancy_calculation( ).
ENDMETHOD.

Spätere Validierungen sind schwieriger zu erkennen und zu verstehen und haben möglicherweise bereits Ressourcen vergeudet, um dorthin zu gelangen.

" Anti-Muster
METHOD do_something.
  DATA(massive_object) = build_expensive_object_from( input ).
  IF massive_object IS NOT BOUND. " happens if input is initial
    RAISE EXCEPTION cx_sy_illegal_argument( ).
  ENDIF.
  result = massive_object->do_some_fancy_calculation( ).
ENDMETHOD.

CHECK vs. RETURN

Es besteht kein Konsens darüber, ob Du CHECK oder RETURN verwenden solltest, um eine Methode zu beenden, wenn die Eingabe nicht den Erwartungen entspricht.

CHECK bietet zwar eindeutig die kürzere Syntax

METHOD read_customizing.
  CHECK keys IS NOT INITIAL.
  " tun, was immer getan werden muss
ENDMETHOD.

Der Name der Anweisung verrät nicht, was passiert, wenn die Bedingung nicht erfüllt wird, so dass die Langform wahrscheinlich besser verstanden wird:

METHOD read_customizing.
  IF keys IS INITIAL.
    RETURN.
  ENDIF.
  " tun, was immer getan werden muss
ENDMETHOD.

Du könntest die Frage vollständig vermeiden, indem Du die Validierung umkehrst und einen Kontrollfluss mit einmaliger Rückgabe verwendest. Dies wird als Anti-Muster betrachtet, weil es eine unnötige Verschachtelungstiefe einführt.

METHOD read_customizing.
  " Anti-Muster
  IF keys IS NOT INITIAL.
    " tun, was immer getan werden muss
  ENDIF.
ENDMETHOD.

Überlege in jedem Fall, ob es wirklich angemessen ist, nichts zurückzugeben. Methoden sollten ein sinnvolles Ergebnis liefern, also entweder einen gefüllten Rückgabeparameter oder eine Ausnahme. Nichts zurückzugeben, ist in vielen Fällen ähnlich wie die Rückgabe von Null, was vermieden werden sollte.

Vermeide CHECK in anderen Positionen

Verwende CHECK nicht außerhalb des Initialisierungsabschnitts einer Methode. Die Anweisung verhält sich an verschiedenen Stellen unterschiedlich und kann zu unklaren, unerwarteten Auswirkungen führen.

Beispielsweise beendet CHECK in einer LOOP-Schleife die aktuelle Iteration und fährt mit der nächsten fort; man könnte fälschlicherweise erwarten, dass es die Methode beendet oder die Schleife verlässt. Verwende stattdessen lieber eine IF-Anweisung in Kombination mit CONTINUE, da CONTINUE nur in Schleifen verwendet werden kann.

Fehlerbehandlung

Nachrichten

Nachrichten leicht auffindbar machen

Um Nachrichten über eine Verwendungsnachweis-Suche aus der Transaktion SE91 leicht auffindbar zu machen, verwende das folgende Muster:

MESSAGE e001(ad) INTO DATA(message).

Falls die variable Nachricht nicht benötigt wird, füge das Pragma ##NEEDED hinzu:

MESSAGE e001(ad) INTO DATA(message) ##NEEDED.

Vermeide Folgendes:

" Anti-Muster
IF 1 = 2. MESSAGE e001(ad). ENDIF.

Dies ist ein Anti-Muster, da:

  • Es enthält unerreichbaren Code.
  • Es testet eine Bedingung, die für Gleichheit niemals wahr sein kann.

Rückgabe-Codes

Ausnahmen gegenüber Rückgabecodes bevorzugen

METHOD try_this_and_that.
  RAISE EXCEPTION NEW cx_failed( ).
ENDMETHOD.

anstelle von

" Anti-Muster
METHOD try_this_and_that.
  error_occurred = abap_true.
ENDMETHOD.

Ausnahmen haben mehrere Vorteile gegenüber Rückgabecodes:

  • Ausnahmen halten Ihre Methodensignaturen sauber: Du kannst das Ergebnis der Methode als RETURNING-Parameter zurückgeben und daneben noch Ausnahmen auslösen. Rückgabecodes verschmutzen Deine Signaturen mit zusätzlichen Parametern für die Fehlerbehandlung.
  • Der Aufrufer muss nicht sofort auf sie reagieren. Er kann einfach den glücklichen Weg seines Codes aufschreiben. Das CATCH zur Behandlung von Ausnahmen kann ganz am Ende der Methode stehen oder auch ganz außerhalb.
  • Ausnahmen können in ihren Attributen und durch Methoden Details über den Fehler liefern. Für Rückgabewerte muss man sich selbst eine andere Lösung einfallen lassen, wie z. B. die Rückgabe eines Protokolls.
  • Die Umgebung erinnert den Aufrufer mit Syntaxfehlern daran, Ausnahmen zu behandeln. Rückgabecodes können versehentlich ignoriert werden, ohne dass es jemand merkt.

Misserfolge nicht durchgehen lassen

Wenn Du Rückgabecodes verwenden musst, z. B. weil Du Funktionen und ältere Codes aufrufst, die nicht unter Deiner Kontrolle stehen, stelle sicher, dass Du keine Fehler durchgehen lässt.

DATA:
  current_date TYPE string,
  response     TYPE bapiret2.

CALL FUNCTION 'BAPI_GET_CURRENT_DATE'
  IMPORTING
    current_date = current_date
  CHANGING
    response     = response.

IF response-type = 'E'.
  RAISE EXCEPTION NEW /clean/some_error( ).
ENDIF.

Ausnahmen

Ausnahmen sind für Fehler, nicht für reguläre Fälle

" Anti-Muster
METHODS entry_exists_in_db
  IMPORTING
    key TYPE char10
  RAISING
    cx_not_found_exception.

Wenn etwas ein regulärer, gültiger Fall ist, sollte es mit regulären Ergebnisparametern behandelt werden.

METHODS entry_exists_in_db
  IMPORTING
    key           TYPE char10
  RETURNING
    VALUE(result) TYPE abap_bool.

Ausnahmen sollten für Fälle reserviert werden, die Du nicht erwartest und die Fehlersituationen widerspiegeln.

METHODS assert_user_input_is_valid
  IMPORTING
    user_input TYPE string
  RAISING
    cx_bad_user_input.

Die missbräuchliche Verwendung von Ausnahmen verleitet den Leser zu der Annahme, dass etwas schief gelaufen ist, obwohl in Wirklichkeit alles in Ordnung ist. Ausnahmen sind auch viel langsamer als normaler Code, weil sie konstruiert werden müssen und oft eine Menge Kontextinformationen sammeln.

Klassenbasierte Ausnahmen verwenden

TRY.
    get_component_types( ).
  CATCH cx_has_deep_components_error.
ENDTRY.

Die veralteten nicht klassenbasierten Ausnahmen haben die gleichen Eigenschaften wie Rückgabecodes und sollten nicht mehr verwendet werden.

" Anti-Muster
get_component_types(
  EXCEPTIONS
    has_deep_components = 1
    OTHERS              = 2 ).

Werfen

Eigene Superklassen verwenden

CLASS cx_fra_static_check DEFINITION ABSTRACT INHERITING FROM cx_static_check.
CLASS cx_fra_no_check DEFINITION ABSTRACT INHERITING FROM cx_no_check.

Ziehe in Erwägung, abstrakte Superklassen für jeden Ausnahmetyp Deiner Anwendung zu erstellen, anstatt die Basisklassen direkt zu unterklassifizieren. Ermöglicht es Dir, alle Deine Ausnahmen zu ERFASSEN. Ermöglicht es Dir, allen Ausnahmen gemeinsame Funktionen hinzuzufügen, wie z. B. eine spezielle Textbehandlung. ABSTRACT verhindert, dass diese nicht beschreibenden Fehler versehentlich direkt verwendet werden.

Eine Art von Ausnahme auslösen

METHODS generate
  RAISING
    cx_generation_error.

In den allermeisten Fällen hat das Auslösen mehrerer Arten von Ausnahmen keinen Sinn. Der Aufrufer ist in der Regel weder interessiert noch in der Lage, die Fehlersituationen zu unterscheiden. Er wird sie daher in der Regel alle auf die gleiche Weise behandeln – und wenn das der Fall ist, warum sollte er sie dann überhaupt unterscheiden?

" Anti-Muster
METHODS generate
  RAISING
    cx_abap_generation
    cx_hdbr_access_error
    cx_model_read_error.

Eine bessere Lösung, um verschiedene Fehlersituationen zu erkennen, ist die Verwendung eines Ausnahmetyps, aber die Hinzufügung von Unterklassen, die eine Reaktion auf einzelne Fehlersituationen ermöglichen – aber nicht erfordern -, wie in Unterklassen verwenden, um dem Aufrufer die Unterscheidung von Fehlersituationen zu ermöglichen beschrieben.

Verwende Unterklassen, damit der Aufrufer Fehlersituationen unterscheiden kann

CLASS cx_bad_generation_variable DEFINITION INHERITING FROM cx_generation_error.
CLASS cx_bad_code_composer_template DEFINITION INHERITING FROM cx_generation_error.

METHODS generate RAISING cx_generation_error.

TRY.
    generator->generate( ).
  CATCH cx_bad_generation_variable.
    log_failure( ).
  CATCH cx_bad_code_composer_template INTO DATA(bad_template_exception).
    show_error_to_user( bad_template_exception ).
  CATCH cx_generation_error INTO DATA(other_exception).
    RAISE EXCEPTION NEW cx_application_error( previous =  other_exception ).
ENDTRY.

Wenn es viele verschiedene Fehlersituationen gibt, verwende stattdessen Fehlercodes:

CLASS cx_generation_error DEFINITION ...
  PUBLIC SECTION.
    TYPES error_code_type TYPE i.
    CONSTANTS:
      BEGIN OF error_code_enum,
        bad_generation_variable    TYPE error_code_type VALUE 1,
        bad_code_composer_template TYPE error_code_type VALUE 2,
        ...
      END OF error_code_enum.
    DATA error_code TYPE error_code_type.

TRY.
    generator->generate( ).
  CATCH cx_generation_error INTO DATA(exception).
    CASE exception->error_code.
      WHEN cx_generation_error=>error_code_enum-bad_generation_variable.
      WHEN cx_generation_error=>error_code_enum-bad_code_composer_variable.
      ...
    ENDCASE.
ENDTRY.

CX_STATIC_CHECK für verwaltbare Ausnahmen auslösen

Wenn eine Ausnahme zu erwarten ist und vom Empfänger vernünftig behandelt werden kann, werfe eine geprüfte Ausnahme, die von CX_STATIC_CHECK geerbt wurde: fehlende Benutzereingabevalidierung, fehlende Ressource, für die es Fallbacks gibt, usw.

CLASS cx_file_not_found DEFINITION INHERITING FROM cx_static_check.

METHODS read_file
  IMPORTING
    file_name_enterd_by_user TYPE string
  RAISING
    cx_file_not_found.

Dieser Ausnahmetyp muss in den Methodensignaturen angegeben werden und muss abgefangen oder weitergeleitet werden, um Syntaxfehler zu vermeiden. Sie ist daher für den Verbraucher klar ersichtlich und stellt sicher, dass er nicht von einer unerwarteten Ausnahme überrascht wird und auf die Fehlersituation reagieren kann.

Wirf CX_NO_CHECK für normalerweise nicht wiederherstellbare Situationen

Wenn eine Ausnahme so schwerwiegend ist, dass es unwahrscheinlich ist, dass der Empfänger sich davon erholen kann, verwende CX_NO_CHECK: Fehler beim Lesen einer Must-Have-Ressource, Fehler beim Auflösen der angeforderten Abhängigkeit, usw.

CLASS cx_out_of_memory DEFINITION INHERITING FROM cx_no_check.

METHODS create_guid
  RETURNING
    VALUE(result) TYPE /bobf/conf_key.

CX_NO_CHECK kann nicht in Methodensignaturen deklariert werden, so dass sein Auftreten für den Konsumenten eine böse Überraschung darstellt. Im Falle von nicht behebbaren Situationen ist dies in Ordnung, da der Verbraucher ohnehin nichts Nützliches dagegen unternehmen kann.

Es kann jedoch Fälle geben, in denen der Verbraucher diese Art von Fehlern tatsächlich erkennen und darauf reagieren möchte. Zum Beispiel könnte ein Abhängigkeitsmanager einen CX_NO_CHECK auslösen, wenn er nicht in der Lage ist, eine Implementierung für eine angeforderte Schnittstelle bereitzustellen, da der reguläre Anwendungscode nicht fortgesetzt werden kann. Es könnte jedoch einen Testbericht geben, der versucht, alle möglichen Dinge zu instanziieren, nur um zu sehen, ob es funktioniert, und der einen Fehler einfach als roten Eintrag in einer Liste meldet – dieser Dienst sollte in der Lage sein, die Ausnahme abzufangen und zu ignorieren, anstatt gezwungen zu sein, sie zu löschen.

CX_DYNAMIC_CHECK für vermeidbare Ausnahmen in Betracht ziehen

Anwendungsfälle für CX_DYNAMIC_CHECK sind selten, und im Allgemeinen empfehlen wir, auf die anderen Ausnahmetypen zurückzugreifen. Du kannst diese Art von Ausnahme jedoch als Ersatz für CX_STATIC_CHECK in Betracht ziehen, wenn der Aufrufer die volle, bewusste Kontrolle darüber hat, ob eine Ausnahme auftreten kann.

DATA value TYPE decfloat.
value = '7.13'.
cl_abap_math=>get_db_length_decs(
  EXPORTING
    in     = value
  IMPORTING
    length = DATA(length) ).

Ein Beispiel ist die Methode get_db_length_decs der Klasse cl_abap_math, die die Anzahl der Ziffern und Dezimalstellen einer dezimalen Fließkommazahl angibt. Diese Methode löst die dynamische Ausnahme cx_parameter_invalid_type aus, wenn der Eingabeparameter keine dezimale Fließkommazahl darstellt. Normalerweise wird diese Methode für eine vollständig und statisch typisierte Variable aufgerufen, so dass der Entwickler weiß, ob diese Ausnahme jemals auftreten kann oder nicht. In diesem Fall würde die dynamische Ausnahme den Aufrufer in die Lage versetzen, die unnötige CATCH-Klausel wegzulassen.

Dump für völlig unwiederbringliche Situationen

Wenn eine Situation so schwerwiegend ist, dass Du absolut sicher bist, dass der Empfänger sich nicht davon erholen kann, oder wenn sie eindeutig auf einen Programmierfehler hinweist, sollten Sie einen Dump durchführen, anstatt eine Exception auszulösen: fehlgeschlagene Speichererfassung, fehlgeschlagene Indexlesungen in einer Tabelle, die gefüllt werden muss, usw.

RAISE SHORTDUMP TYPE cx_sy_create_object_error.  " >= NW 7.53
MESSAGE x666(general).                           " < NW 7.53

Dieses Verhalten verhindert, dass der Verbraucher danach etwas Sinnvolles tun kann. Verwende dies nur, wenn Du Dir sicher bist.

RAISE EXCEPTION NEW gegenüber RAISE EXCEPTION TYPE bevorzugen

Hinweis: Verfügbar ab NW 7.52.

RAISE EXCEPTION NEW cx_generation_error( previous = exception ).

ist im Allgemeinen kürzer als der unnötig längere

RAISE EXCEPTION TYPE cx_generation_error
  EXPORTING
    previous = exception.

Wenn Du jedoch den Zusatz MESSAGE häufig verwendest, solltest Du Dich für die Variante TYPE entscheiden:

RAISE EXCEPTION TYPE cx_generation_error
  MESSAGE e136(messages)
  EXPORTING
    previous = exception.

Fangen

Umhülle fremde Ausnahmen, anstatt sie in Deinen Code eindringen zu lassen

METHODS generate RAISING cx_generation_failure.

METHOD generate.
  TRY.
      generator->generate( ).
    CATCH cx_amdp_generation_failure INTO DATA(exception).
      RAISE EXCEPTION NEW cx_generation_failure( previous = exception ).
  ENDTRY.
ENDMETHOD.

Das Gesetz von Demeter empfiehlt, Dinge zu entkoppeln. Die Weiterleitung von Ausnahmen aus anderen Komponenten verstößt gegen dieses Prinzip. Mache Dich unabhängig vom fremden Code, indem Du diese Ausnahmen abfängst und in einen eigenen Ausnahmetyp verpackst.

" Anti-Muster
METHODS generate RAISING cx_sy_gateway_failure.

METHOD generate.
  generator->generate( ).
ENDMETHOD.

Kommentare

Drücke Dich im Code aus, nicht in Kommentaren

METHOD correct_day_to_last_in_month.
  WHILE is_invalid( date ).
    reduce_day_by_one( CHANGING date = date ).
  ENDWHILE.
ENDMETHOD.

METHOD is_invalid.
  DATA zero_if_invalid TYPE i.
  zero_if_invalid = date.
  result = xsdbool( zero_if_invalid = 0 ).
ENDMETHOD.

METHOD reduce_day_by_one.
  date+6(2) = date+6(2) - 1.
ENDMETHOD.

anstelle von

" Anti-Muster
" korrekt wäre z.B. der 29.02. in Nicht-Schaltjahren sowie das Ergebnis einer Datumsberechnung
" etwas wie z.B. den 31.06. Dieses Beispiel muss auf 30.06. korrigiert werden.
METHOD fix_day_overflow.
  DO 3 TIMES.
    " 31 - 28 = 3 => diese Korrektur ist höchstens 3 Mal erforderlich
    lv_dummy = cv_date.
    " lv_dummy ist 0, wenn der Datumswert ein nicht vorhandenes Datum ist - ABAP-spezifische Implementierung
    IF ( lv_dummy EQ 0 ).
      cv_date+6(2) = cv_date+6(2) - 1. " 1 Tag vom angegebenen Datum subtrahieren
    ELSE.
      " Datum vorhanden => keine Korrektur erforderlich
      EXIT.
    ENDIF.
  ENDDO.
ENDMETHOD.

Clean Code verbietet Dir nicht, Deinen Code zu kommentieren – er ermutigt Dich, bessere Mittel zu nutzen und nur dann auf Kommentare zurückzugreifen, wenn dies nicht möglich ist.

Kommentare sind keine Entschuldigung für schlechte Namen

DATA(input_has_entries) = has_entries( input ).

Verbessere Deine Namen, anstatt zu erklären, was sie wirklich bedeuten oder warum Du schlechte Namen gewählt hast.

" Anti-Muster
" prüft, ob die Tabelleneingabe Einträge enthält
DATA(result) = check_table( input ).

Verwende Methoden anstelle von Kommentaren, um Deinen Code zu segmentieren

DATA(statement) = build_statement( ).
DATA(data) = execute_statement( statement ).

Dadurch werden nicht nur die Absicht, die Struktur und die Abhängigkeiten des Codes deutlicher, sondern es werden auch Übertragungsfehler vermieden, wenn temporäre Variablen zwischen den Abschnitten nicht ordnungsgemäß gelöscht werden.

" Anti-Muster
" -----------------
" Build statement
" -----------------
DATA statement TYPE string.
statement = |SELECT * FROM d_document_roots|.

" -----------------
" Execute statement
" -----------------
DATA(result_set) = adbc->execute_sql_query( statement ).
result_set->next_package( IMPORTING data = data ).

Schreibe Kommentare, um das Warum zu erklären, nicht das Was

" kann nicht scheitern, Existenz von >= 1 Zeile oben behauptet
DATA(first_line) = table[ 1 ].

Niemand braucht den Code in natürlicher Sprache zu wiederholen

" Anti-Muster
" select alert root from database by key
SELECT * FROM d_alert_root WHERE key = key.

Design gehört in die Designdokumente, nicht in den Code

" Anti-Muster
" Diese Klasse erfüllt einen doppelten Zweck. Zuerst macht sie eine Sache. Dann tut sie noch etwas anderes.
" Sie tut dies, indem sie eine Menge Code ausführt, der über die lokalen Hilfsklassen verteilt ist.
" Um zu verstehen, was hier vor sich geht, sollten wir zunächst über die Natur des Universums als solches nachdenken.
" Schauen Sie sich dies und jenes an, um die Details zu erfahren.

Niemand liest das – im Ernst. Wenn die Leute ein Lehrbuch lesen müssen, um Deinen Code verwenden zu können, kann dies ein Indikator dafür sein, dass Dein Code schwerwiegende Designprobleme aufweist, die Du anderweitig lösen solltest. Manche Codes benötigen eine Erklärung, die über eine einzelne Kommentarzeile hinausgeht; erwäge in diesen Fällen, das Entwurfsdokument zu verlinken.

Kommentar mit „, nicht mit *

Kommentare werden zusammen mit den Aussagen, die sie kommentieren, eingerückt

METHOD do_it.
  IF input IS NOT INITIAL.
    " Delegationsmuster
    output = calculate_result( input ).
  ENDIF.
ENDMETHOD.

Kommentare mit Sternchen neigen dazu, an seltsamen Stellen einzurücken

" Anti-Muster
METHOD do_it.
  IF input IS NOT INITIAL.
* Delegationsmuster
    output = calculate_result( input ).
  ENDIF.
ENDMETHOD.

Setze Kommentare vor die Aussage, auf die sie sich beziehen

" Delegationsmuster
output = calculate_result( input ).

besser als

" Anti-Muster
output = calculate_result( input ).
" Delegationsmuster

Und weniger invasiv als

output = calculate_result( input ).  " Delegationsmuster

Code löschen, statt ihn zu kommentieren

" Anti-Muster
* output = calculate_result( input ).

Wenn Du so etwas findest, lösche es. Der Code ist offensichtlich nicht erforderlich, da Deine Anwendung funktioniert und alle Tests erfolgreich sind. Gelöschter Code kann später aus der Versionshistorie wiederhergestellt werden. Wenn Du einen Code dauerhaft erhalten möchtest, kopiere ihn in eine Datei oder ein $TMP- oder HOME-Objekt.

Führe keine manuelle Versionierung durch.

" Anti-Muster
* ticket 800034775 ABC ++ Start
output = calculate_result( input ).
* ticket 800034775 ABC ++ End

Kommentare zur Zuordnung tendieren dazu, den Code zu verschmutzen und bieten nicht viele Vorteile, da die Versionierung bereits vom Quellcode-Management durchgeführt wird. Texte in Transportaufträgen eignen sich viel besser, um zu beschreiben, warum etwas angepasst wurde.

Verwende FIXME, TODO und XXX und füge deine ID hinzu

METHOD do_something.
  " XXX FH Lösche diese Methode - Sie macht nichts
ENDMETHOD.
  • FIXME weist auf Fehler hin, die entweder zu klein sind oder sich noch in der Entstehung befinden, um als interne Vorfälle behandelt zu werden.
  • TODOs sind Stellen, an denen Du in der nahen(!) Zukunft etwas vervollständigen möchtest.
  • XXX markiert Code, der funktioniert, aber verbessert werden könnte.

Wenn Du einen solchen Kommentar eingibst, füge Deinen Nickname, Deine Initialen oder Deinen Benutzernamen hinzu, damit Deine Mitentwickler Dich kontaktieren und Fragen stellen können, falls der Kommentar unklar ist.

Füge keine Methode-Signatur und End-of-Kommentare hinzu

Methode-Signatur-Kommentare helfen niemandem.

" Anti-Muster
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method CALIBRATION_KPIS=>CALCULATE_KPI
* +-------------------------------------------------------------------------------------------------+
* | [--->] STRATEGY_ID                 TYPE        STRATEGY_ID
* | [--->] THRESHOLD                   TYPE        STRATEGY_THRESHOLD
* | [--->] DETECTION_OBJECT_SCORE      TYPE        T_HIT_RESULT
* | [<---] KPI                         TYPE        T_SIMULATED_KPI
* +--------------------------------------------------------------------------------------</SIGNATURE>

Jahrzehnte zuvor, als Du die Methodensignatur nicht sehen konntest, wenn Du ihren Code inspiziert hast, oder mit Ausdrucken gearbeitet hast, die Dutzende von Seiten hatten, ergaben diese Kommentare vielleicht Sinn. Aber alle modernen ABAP-IDEs (SE24, SE80, ADT) zeigen die Methodensignatur leicht, so dass diese Kommentare nichts weiter als störendes Rauschen geworden sind.Im Formular-Editor von SE24/SE80 drücke die Schaltfläche „Signature“. In den ABAP Development Tools markiere den Methodennamen und drücke F2 oder füge die Ansicht „ABAP Element Info“ deiner Perspektive hinzu.Ebenso sind End-of-Kommentare überflüssig. Diese Kommentare mögen vor Jahrzehnten hilfreich gewesen sein, als Programme und Funktionen sowie die verschachtelten IFs darin hunderte Zeilen Code lang waren. Aber unser moderner Codierstil führt zu Methoden, die kurz genug sind, um leicht zu erkennen, zu welcher öffnenden Anweisung ein ENDIF oder ENDMETHOD gehört:

" Anti-Muster
METHOD get_kpi_calc.
  IF has_entries = abap_false.
    result = 42.
  ENDIF.  " IF has_entries = abap_false
ENDMETHOD.   " get_kpi_calc

Dupliziere Meldungstexte nicht als Kommentare

" Anti-Muster
" Warnkategorie nicht ausgefüllt
MESSAGE e003 INTO dummy.

Meldungen ändern sich unabhängig von Deinem Code, und niemand wird sich daran erinnern, den Kommentar anzupassen. Dies führt schnell dazu, dass der Kommentar veraltet und sogar irreführend wird, ohne dass es jemand bemerkt.

Moderne IDEs bieten einfache Möglichkeiten, den Text hinter einer Meldung anzuzeigen. In den ABAP Development Tools beispielsweise kannst du die Meldungs-ID markieren und Umschalt + F2 drücken.

Wenn Du es expliziter möchtest, erwäge, die Meldung in eine eigene Methode auszulagern.

METHOD create_alert_not_found_message.
  MESSAGE e003 INTO dummy.
ENDMETHOD.

ABAP Doc nur für öffentliche APIs

Schreibe ABAP-Dokumentation, um öffentliche APIs zu dokumentieren, also APIs, die für Entwickler in anderen Teams oder Anwendungen gedacht sind. Schreibe keine ABAP-Dokumentation für interne Elemente.

ABAP-Dokumentation leidet unter denselben Schwächen wie alle Kommentare, das heißt, sie veraltet schnell und wird dann irreführend. Daher solltest du sie nur dort einsetzen, wo es sinnvoll ist, und nicht verlangen, dass für alles und jedes ABAP-Dokumentation geschrieben wird.

Verwende Pragmas anstelle von Pseudokommentaren

Verwende Pragmas anstelle von Pseudokommentaren, um irrelevante Warnungen und Fehler, die von der ATC identifiziert wurden, zu unterdrücken. Pseudokommentare sind größtenteils veraltet und wurden durch Pragmas ersetzt.

" Muster
MESSAGE e001(ad) INTO DATA(message) ##NEEDED.

" Anti-Muster
MESSAGE e001(ad) INTO DATA(message). "#EC NEEDED

Verwende das Programm ABAP_SLIN_PRAGMAS oder die Tabelle SLIN_DESC, um die Zuordnung zwischen veralteten Pseudokommentaren und den Pragmas, die sie ersetzt haben, zu finden.

Formatierung

Die folgenden Vorschläge sind darauf optimiert, den Code zu lesen, nicht zu schreiben. Da der ABAP-Formatter sie nicht abdeckt, erfordern einige von ihnen zusätzliche manuelle Arbeit, um Anweisungen neu zu formatieren, wenn sich Namenlängen usw. ändern. Wenn Du dies vermeiden möchtest, erwäge, Regeln wie „Zuweisungen zur gleichen Variable ausrichten, aber nicht zu verschiedenen“ zu verwerfen.

Sei konsistent

Formatiere den gesamten Code eines Projekts auf die gleiche Weise. Lass alle Teammitglieder den gleichen Formatierungsstil verwenden.

Wenn Du fremden Code bearbeitest, halte Dich an den Formatierungsstil dieses Projekts, anstatt auf Deinem persönlichen Stil zu beharren.

Wenn Du im Laufe der Zeit Deine Formatierungsregeln änderst, verwende bewährte Refactoring-Methoden, um Deinen Code im Laufe der Zeit zu aktualisieren.

Optimiere für das Lesen, nicht für das Schreiben

Entwickler verbringen die meiste Zeit damit, Code zu lesen. Das tatsächliche Schreiben von Code nimmt einen viel kleineren Teil des Tages ein.

Daher solltest Du Deine Code-Formatierung für das Lesen und Debuggen optimieren, nicht für das Schreiben.

Beispielsweise solltest Du bevorzugen

DATA:
  a TYPE b,
  c TYPE d,
  e TYPE f.

anstelle

" Anti-Muster
DATA:
  a TYPE b
  ,c TYPE d
  ,e TYPE f.

ABAP-Formatierer bevor Aktivierung verwenden

Wende den ABAP-Formatter – Shift+F1 in SE80, SE24 und ADT – vor der Aktivierung eines Objekts an. Hinweis: Der ABAP-Formatter wird in der SAP GUI als Pretty Printer bezeichnet.

Wenn Du eine größere, unformatierte Altcode-Basis änderst, möchtest Du möglicherweise den ABAP-Formatter nur auf ausgewählte Zeilen anwenden, um große Änderungslisten und Transportabhängigkeiten zu vermeiden. Erwäge, das gesamte Entwicklungselement in einem separaten Transportauftrag oder Hinweis zu formatieren.

Verwende immer die ABAP-Formatter-Einstellungen Deines Teams

Verwende immer die ABAP-Formatter-Einstellungen Deines Teams. Stelle sie unter

  • Eclipse: Menü > Fenster > Einstellungen > ABAP-Entwicklung > Editoren > Quellcode-Editoren > ABAP Formatter
  • SAP GUI: Menü > Dienstprogramme > Einstellungen … > ABAP-Editor > Pretty Printer

Setze Einzug und Konvertierung von Großbuchstaben/Kleinbuchstaben > Schlüsselwort in Großbuchstaben wie in deinem Team vereinbart.

Nicht mehr als eine Anweisung pro Zeile

DATA do_this TYPE i.
do_this = input + 3.

Auch wenn einige Vorkommnisse dich in die Irre führen könnten und den Eindruck vermitteln, dass dies lesbar war:

" Anti-Muster
DATA do_this TYPE i. do_this = input + 3.

Halte dich an eine vernünftige Zeilenlänge

Beachte eine maximale Zeilenlänge von 120 Zeichen.

Das menschliche Auge liest Text angenehmer, wenn die Zeilen nicht zu breit sind – frage einen UI-Designer oder einen Augenbewegungsforscher deiner Wahl. Du wirst auch die schmalere Codeansicht beim Debuggen oder Vergleichen von zwei Quellen nebeneinander schätzen.

Die Begrenzung auf 80 oder sogar 72 Zeichen, die in den alten Terminalgeräten ihren Ursprung hat, ist ein wenig zu restriktiv. Während 100 Zeichen oft empfohlen werden und eine tragfähige Wahl darstellen, scheinen 120 Zeichen für ABAP etwas besser zu funktionieren, vielleicht aufgrund der allgemeinen Verbosität der Sprache.

Zur Erinnerung kannst du in ADT die Druckmarke auf 120 Zeichen konfigurieren, die dann in der Codeansicht als vertikale Linie dargestellt wird. Konfiguriere dies unter Menü > Fenster > Einstellungen > Allgemein > Editoren > Texteditoren.

Verdichte deinen Code

DATA(result) = calculate( items ).

Verdichte deinen Code, anstatt unnötige Leerzeichen hinzuzufügen

" Anti-Muster
DATA(result)        =      calculate(    items =   items )   .

Füge eine einzelne Leerzeile hinzu, um Dinge zu trennen, aber nicht mehr

DATA(result) = do_something( ).

DATA(else) = calculate_this( result ).

zu betonen, dass die beiden Anweisungen verschiedene Dinge tun. Aber es gibt keinen Grund dafür, viele Leerzeilen zwischen den Codeabschnitten hinzuzufügen.

" Anti-Muster
DATA(result) = do_something( ).



DATA(else) = calculate_this( result ).

Das Verlangen, trennende Leerzeilen hinzuzufügen, kann ein Hinweis darauf sein, dass Deine Methode nicht nur eine Aufgabe erfüllt.

Übertreibe es nicht mit den trennenden Leerzeilen.

METHOD do_something.
  do_this( ).
  then_that( ).
ENDMETHOD.

Kein Grund, die schlechte Angewohnheit zu haben, Deinen Code mit Leerzeilen auseinanderzureißen.

" Anti-Muster
METHOD do_something.

  do_this( ).

  then_that( ).

ENDMETHOD.

Das gilt auch innerhalb einer Anweisung, da dies beim Durchblättern des Codes leicht als neue Anweisung missverstanden werden kann.

" Anti-Muster
DATA(result) = merge_structures( a = VALUE #( field_1 = 'X'
                                              field_2 = 'A' )

                                 b = NEW /clean/structure_type( field_3 = 'C'
                                                                field_4 = 'D' ) ).

Leerzeilen machen eigentlich nur Sinn, wenn Du Anweisungen hast, die sich über mehrere Zeilen erstrecken.

METHOD do_something.

  do_this( ).

  then_that(
    EXPORTING
      variable = 'A'
    IMPORTING
      result   = result ).

ENDMETHOD.

Richte Zuweisungen an dasselbe Objekt aus, aber nicht an unterschiedliche Objekte

Um zu verdeutlichen, dass diese Dinge irgendwie zusammengehören.

structure-type = 'A'.
structure-id   = '4711'.

oder noch besser

structure = VALUE #( type = 'A'
                     id   = '4711' ).

Aber lass Dinge unordentlich, die nichts miteinander zu tun haben:

customizing_reader = fra_cust_obj_model_reader=>s_get_instance( ).
hdb_access = fra_hdbr_access=>s_get_instance( ).

Schließe Klammern am Zeilenende

modify->update( node           = if_fra_alert_c=>node-item
                key            = item->key
                data           = item
                changed_fields = changed_fields ).

anstatt der unnötig längeren Schreibweise

" Anti-Muster
modify->update( node           = if_fra_alert_c=>node-item
                key            = item->key
                data           = item
                changed_fields = changed_fields
).

Halte Aufrufe mit einem einzelnen Parameter in einer Zeile

DATA(unique_list) = remove_duplicates( list ).
remove_duplicates( CHANGING list = list ).

anstatt der unnötig längeren Schreibweise

" Anti-Muster
DATA(unique_list) = remove_duplicates(
                           list ).
DATA(unique_list) = remove_duplicates(
                         CHANGING
                           list = list ).

Behalte die Parameter hinter dem Aufruf

DATA(sum) = add_two_numbers( value_1 = 5
                             value_2 = 6 ).

Wenn dies die Zeilen sehr lang macht, kannst Du die Parameter in die nächste Zeile aufteilen.

DATA(sum) = add_two_numbers(
                value_1 = round_up( input DIV 7 ) * 42 + round_down( 19 * step_size )
                value_2 = VALUE #( ( `Fehler in der Berechnung mit einem seltsamen Ergebnis` ) ) ).

Wenn Du sie aufteilst, rücke die Parameter unter dem Aufruf ein

DATA(sum) = add_two_numbers(
                value_1 = 5
                value_2 = 6 ).

Die Parameter an anderer Stelle auszurichten, erschwert es, zuzuordnen, wozu sie gehören:

" Anti-Muster
DATA(sum) = add_two_numbers(
    value_1 = 5
    value_2 = 6 ).

Jedoch handelt es sich hierbei um das beste Muster, wenn Du möchtest, dass die Formatierung nicht durch eine Änderung der Namenlänge unterbrochen wird.

Zeilenumbruch bei mehreren Parametern

DATA(sum) = add_two_numbers( value_1 = 5
                             value_2 = 6 ).

Ja, das verschwendet Platz. Andernfalls ist es jedoch schwierig zu erkennen, wo ein Parameter endet und der nächste beginnt:

" Anti-Muster
DATA(sum) = add_two_numbers( value_1 = 5 value_2 = 6 ).

Richte die Parameter aus

modify->update( node           = if_fra_alert_c=>node-item
                key            = item->key
                data           = item
                changed_fields = changed_fields ).

Unregelmäßige Ränder erschweren das Erkennen, wo der Parameter endet und sein Wert beginnt:

" Anti-Muster
modify->update( node = if_fra_alert_c=>node-item
                key = item->key
                data = item
                changed_fields = changed_fields ).

Auf der anderen Seite handelt es sich um das beste Muster, wenn du möchtest, dass die Formatierung nicht durch eine Änderung der Namenlänge unterbrochen wird.

Breche den Aufruf in eine neue Zeile, wenn die Zeile zu lang wird

DATA(some_super_long_param_name) =
  if_some_annoying_interface~add_two_numbers_in_a_long_name(
      value_1 = 5
      value_2 = 6 ).

Rücke ein und richte am Tab aus

Rücke die Schlüsselwörter der Parameter um 2 Leerzeichen und die Parameter selbst um 4 Leerzeichen ein:

DATA(sum) = add_two_numbers(
              EXPORTING
                value_1 = 5
                value_2 = 6
              CHANGING
                errors  = errors ).

Wenn Du keine Schlüsselwörter hast, rücke die Parameter um 4 Leerzeichen ein.

DATA(sum) = add_two_numbers(
                value_1 = 5
                value_2 = 6 ).

Verwende die Tab-Taste zum Einrücken. Es ist in Ordnung, wenn dadurch eine zusätzliche Leerstelle hinzugefügt wird, falls das „DATA(sum) =“ am linken Rand eine ungerade Anzahl von Zeichen hat.

Rücke Inline-Deklarationen wie Methodenaufrufe ein

Rücke Inline-Deklarationen mit „VALUE“ oder „NEW“ genauso ein, als wären es Methodenaufrufe:

DATA(result) = merge_structures( a = VALUE #( field_1 = 'X'
                                              field_2 = 'A' )
                                 b = NEW /clean/structure_type( field_3 = 'C'
                                                                field_4 = 'D' ) ).

Richte die Typklauseln nicht aus

DATA name TYPE seoclsname.
DATA reader TYPE REF TO /clean/reader.

Eine Variable und ihr Typ gehören zusammen und sollten daher visuell nahe beieinander gruppiert sein. Das Ausrichten der TYPE-Klauseln lenkt von diesem Grundsatz ab und legt nahe, dass die Variablen eine vertikale Gruppe bilden und ihre Typen eine andere. Die Ausrichtung erzeugt auch unnötigen Bearbeitungsaufwand, da du alle Einrückungen anpassen musst, wenn sich die Länge des längsten Variablennamens ändert.

" Anti-Muster
DATA name   TYPE seoclsname.
DATA reader TYPE REF TO /clean/reader.

Verkette keine Zuweisungen.

" Anti-Muster
var1 = var2 = var3.

Verkettete Zuweisungen verwirren normalerweise den Leser. Außerdem funktioniert die Inline-Deklaration nicht an jeder Stelle in einer Mehrfachzuweisung.

var2 = var3.
var1 = var3.

Außerdem sieht das Anti-Muster unklar aus, da „=“ in ABAP sowohl für Vergleiche als auch für Zuweisungen verwendet wird. Es ähnelt der Art und Weise, wie andere Programmiersprachen Vergleiche implementieren, zum Beispiel „a = (b == c)“ in JavaScript. Verwende „xsdbool“ für Vergleiche.

Testen

Prinzipien

Schreibe testbaren Code

Schreibe Deinen Code so, dass Du ihn automatisch testen kannst. Falls dies eine Umstrukturierung Deines Codes erfordert, tue dies. Mache das zuerst, bevor Du andere Funktionen hinzufügst.

Wenn Du zu einem vorhandenen Code hinzufügst, der zu schlecht strukturiert ist, um getestet zu werden, strukturiere ihn zumindest so um, dass Du Deine Ergänzungen testen kannst.

Ermögliche anderen, Dich zu simulieren

Wenn Du Code schreibst, der von anderen verwendet wird, ermögliche ihnen, Unit-Tests für ihren eigenen Code zu schreiben, indem du Schnittstellen an allen nach außen gerichteten Stellen hinzufügst, hilfreiche Testdoubles bereitstellst, die Integrationstests erleichtern, oder Dependency Inversion anwendest, um ihnen zu ermöglichen, die Konfiguration durch eine Testkonfiguration zu ersetzen.

Regeln zur Lesbarkeit

Mache deinen Testcode noch lesbarer als deinen Produktionscode. Schlechten Produktionscode kannst du mit guten Tests angehen, aber wenn du die Tests nicht verstehst, bist du verloren.

Halte deinen Testcode so einfach und dumm, dass du ihn auch noch in einem Jahr verstehst.

Halte dich an Standards und Muster, um es deinen Kollegen zu ermöglichen, schnell in den Code einzusteigen.

Mache keine Kopien und schreibe keine Testberichte

Beginne nicht mit der Erstellung einer Kopie eines Entwicklungselements und spiele damit herum, wenn Du an einem Backlog-Element arbeitest. Andere werden diese Objekte nicht bemerken und daher den Status Deiner Arbeit nicht kennen. Wahrscheinlich verschwendest du auch viel Zeit, indem du die Arbeitskopie überhaupt erstellst. Du wirst wahrscheinlich auch vergessen, die Kopie später zu löschen, was dein System und seine Abhängigkeiten verschmutzt. (Glaubst du das nicht? Geh jetzt in dein Entwicklungssystem und überprüfe dein $TMP.)

Beginne auch nicht damit, einen Testbericht zu schreiben, der etwas auf eine bestimmte Weise aufruft, und wiederhole das, um zu überprüfen, ob die Dinge funktionieren, wenn du daran arbeitest. Das ist schlechtes Testen: Das manuelle Wiederholen eines Testberichts und das Überprüfen mit bloßem Auge, ob alles noch in Ordnung ist. Gehe den nächsten Schritt und automatisiere diesen Bericht in einem Unit-Test mit einer automatischen Überprüfung, die dir sagt, ob der Code noch in Ordnung ist. Erstens sparst du dir so die Mühe, die Unit-Tests später schreiben zu müssen. Zweitens sparst du eine Menge Zeit für die manuellen Wiederholungen und vermeidest Langeweile und Ermüdung.

Teste öffentliche Schnittstellen, nicht private interne Teile

Schreibe deinen Code so, dass du ihn automatisch testen kannst. Falls dies eine Umstrukturierung deines Codes erfordert, tue dies. Mache das zuerst, bevor du andere Funktionen hinzufügst.

Wenn du zu einem vorhandenen Code hinzufügst, der zu schlecht strukturiert ist, um getestet zu werden, strukturiere ihn zumindest so um, dass du deine Ergänzungen testen kannst.

Ermögliche anderen, dich zu simulieren. Wenn du Code schreibst, der von anderen verwendet wird, ermögliche ihnen, Unit-Tests für ihren eigenen Code zu schreiben, indem du Schnittstellen an allen nach außen gerichteten Stellen hinzufügst, hilfreiche Testdoubles bereitstellst, die Integrationstests erleichtern, oder Dependency Inversion anwendest, um ihnen zu ermöglichen, die Konfiguration durch eine Testkonfiguration zu ersetzen.

Mach dir keine allzu großen Sorgen um die Testabdeckung

Die Codeabdeckung ist dazu da, Dir zu helfen, vergessenen Code zu finden, den Du vergessen hast zu testen, und nicht, um irgendwelche zufälligen KPIs zu erfüllen:

Erfinde keine Tests ohne oder mit Dummy-Assertions, nur um die Abdeckung zu erreichen. Lasse lieber Dinge ungetestet, um transparent zu machen, dass Du sie nicht sicher refaktorieren kannst. Du kannst weniger als 100% Abdeckung haben und trotzdem perfekte Tests haben. Es gibt Fälle – wie IFs im Konstruktor, um Testdoubles einzufügen -, bei denen es möglicherweise unpraktisch ist, 100% zu erreichen. Gute Tests neigen dazu, die gleiche Anweisung mehrmals abzudecken, für verschiedene Zweige und Bedingungen. Sie werden tatsächlich eine imaginäre > 100%ige Abdeckung haben.

Testklassen

Benenne lokale Testklassen nach ihrem Zweck

Benenne lokale Testklassen entweder nach dem „when“-Teil der Geschichte.

CLASS ltc_<public method name> DEFINITION FOR TESTING ... ."

oder nach dem „given“-Teil der Geschichte.

CLASS ltc_<common setup semantics> DEFINITION FOR TESTING ... .
" Anti-Muster
CLASS ltc_fra_online_detection_api DEFINITION FOR TESTING ... . " Wir wissen, dass es sich um die zu testende Klasse handelt - warum wiederholen?
CLASS ltc_test DEFINITION FOR TESTING ....                      " Natürlich handelt es sich um einen Test, was sollte es auch sonst sein?

Tests in lokale Klassen

Platziere Unit-Tests in der lokalen Test-Include der zu testenden Klasse. Dies stellt sicher, dass Personen diese Tests beim Refactoring der Klasse finden und ermöglicht es ihnen, alle zugehörigen Tests mit einem einzigen Tastendruck auszuführen, wie in „Wie führt man Testklassen aus“ beschrieben.

Lege Komponenten-, Integrations- und Systemtests in die lokale Test-Include einer separaten globalen Klasse. Diese Tests beziehen sich nicht direkt auf eine einzelne Klasse unter Test, daher sollten sie nicht willkürlich in einer der beteiligten Klassen platziert werden, sondern in einer separaten Klasse.

Markiere diese globale Testklasse als „FOR TESTING“ und „ABSTRACT“, um zu verhindern, dass sie versehentlich im Produktionscode referenziert wird.

Tests in anderen Klassen zu platzieren, birgt die Gefahr, dass sie übersehen werden und vergessen werden, sie auszuführen, wenn die beteiligten Klassen refaktoriert werden.

Daher ist es sinnvoll, Testbeziehungen zu verwenden, um zu dokumentieren, welche Objekte durch den Test getestet werden. Mit dem untenstehenden Beispiel könnte die Testklasse „hiring_test“ ausgeführt werden, während sich die Person in der Klasse „recruiting“ oder „candidate“ befindet, mithilfe der Tastenkombination Umschalt-Strg-F12 (Windows) oder Cmd-Umschalt-F12 (macOS).

"! @testing recruting
"! @testing candidate
class hiring_test definition
  for testing risk level dangerous duration medium
  abstract.
  ...
endclass.

Hilfsmethoden in Hilfsklassen

Platziere Hilfsmethoden, die von mehreren Testklassen verwendet werden, in einer Hilfsklasse. Stelle die Hilfsmethoden durch Vererbung (Is-A-Beziehung) oder Delegation (Has-A-Beziehung) zur Verfügung.

" Beispiel Vererbung

CLASS lth_unit_tests DEFINITION ABSTRACT.

  PROTECTED SECTION.
    CLASS-METHODS assert_activity_entity
      IMPORTING
        actual_activity_entity TYPE REF TO zcl_activity_entity
        expected_activity_entity TYPE REF TO zcl_activity_entity.
    ...
ENDCLASS.

CLASS lth_unit_tests IMPLEMENTATION.

  METHOD assert_activity_entity.
    ...
  ENDMETHOD.

ENDCLASS.

CLASS ltc_unit_tests DEFINITION INHERITING FROM lth_unit_tests FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.
  ...
ENDCLASS.

Testklassen ausführen

In den ABAP Development Tools drücke Strg + Umschalt + F10, um alle Tests in einer Klasse auszuführen. Drücke Strg + Umschalt + F11, um Abdeckungsmessungen einzubeziehen. Drücke Strg + Umschalt + F12, um auch Tests in anderen Klassen auszuführen, die als Testbeziehungen gepflegt werden.

Unter macOS, Cmd anstelle von Strg verwenden.

Code unter Test

Benenne den Code unter Test aussagekräftig oder verwende standardmäßig „CUT“ (Code unter Test).

Gib der Variable, die den Code unter Test darstellt, einen aussagekräftigen Namen:

DATA blog_post TYPE REF TO ...

Wiederhole nicht einfach den Klassennamen mit all seinen nicht wertvollen Namensräumen und Präfixen:

" Anti-Muster
DATA clean_fra_blog_post TYPE REF TO ...

Wenn Du verschiedene Testaufbauten hast, kann es hilfreich sein, den sich ändernden Zustand des Objekts zu beschreiben:

DATA empty_blog_post TYPE REF TO ...
DATA simple_blog_post TYPE REF TO ...
DATA very_long_blog_post TYPE REF TO ...

Wenn es schwierig ist, einen aussagekräftigen Namen zu finden, verwende standardmäßig „CUT“ als Abkürzung für „Code unter Test“.

DATA cut TYPE REF TO ...

Besonders in unübersichtlichen und verwirrenden Tests kann das Benennen der Variablen als „cut“ dem Leser vorübergehend helfen zu erkennen, was tatsächlich getestet wird. Allerdings ist das Aufräumen der Tests der eigentliche Weg für langfristigen Erfolg.

Teste gegen Schnittstellen, nicht gegen Implementierungen

Eine praktische Konsequenz des „Teste öffentliche Methoden, nicht private Interna“-Ansatzes ist, Deinen Code unter Test mit einer Schnittstelle zu typisieren.

DATA code_under_test TYPE REF TO some_interface.

statt einer Klasse

" Anti-Muster
DATA code_under_test TYPE REF TO some_class.

Extrahiere den Aufruf des Codes unter Test in eine eigene Methode

Wenn die zu testende Methode viele Parameter oder vorbereitete Daten erfordert, kann es hilfreich sein, den Aufruf in eine eigene Hilfsmethode auszulagern, die die unwichtigen Parameter standardmäßig setzt.

METHODS map_xml_to_itab
  IMPORTING
    xml_string TYPE string
    config     TYPE /clean/xml2itab_config DEFAULT default_config
    format     TYPE /clean/xml2itab_format DEFAULT default_format.

METHOD map_xml_to_itab.
  result = cut->map_xml_to_itab( xml_string = xml_string
                                 config     = config
                                 format     = format ).
ENDMETHOD.

DATA(itab) = map_xml_to_itab( '<xml></xml>' ).

Das direkte Aufrufen der ursprünglichen Methode kann deinen Test mit vielen bedeutungslosen Details überfluten:

" Anti-Muster
DATA(itab) = cut->map_xml_to_itab( xml_string = '<xml></xml>'
                                   config     = VALUE #( 'some meaningless stuff' )
                                   format     = VALUE #( 'more meaningless stuff' ) ).

Injection

Verwende die Dependency Inversion, um Testdoubles einzuführen.

Dependency Inversion bedeutet, dass Du alle Abhängigkeiten dem Konstruktor übergibst:

METHODS constructor
  IMPORTING
    customizing_reader TYPE REF TO if_fra_cust_obj_model_reader.

METHOD constructor.
  me->customizing_reader = customizing_reader.
ENDMETHOD.

Verwende keine Setter-Injektion. Sie ermöglicht die Verwendung des Produktionscodes auf Arten, die nicht beabsichtigt sind:

" Anti-Muster
METHODS set_customizing_reader
  IMPORTING
    customizing_reader TYPE REF TO if_fra_cust_obj_model_reader.

METHOD do_something.
  object->set_customizing_reader( a ).
  object->set_customizing_reader( b ). " would you expect that somebody does this?
ENDMETHOD.

Verwende keine FRIENDS-Injektion. Sie initialisiert Abhängigkeiten, bevor sie ersetzt werden, mit wahrscheinlich unerwarteten Konsequenzen. Sie wird brechen, sobald Du die internen Komponenten umbenennst. Außerdem umgeht sie Initialisierungen im Konstruktor.

" Anti-Muster
METHOD setup.
  cut = NEW fra_my_class( ). " <- builds the customizing_reader for the production use case first - what will it break with that?
  cut->customizing_reader ?= cl_abap_testdouble=>create( 'if_fra_cust_obj_model_reader' ).
ENDMETHOD.

METHOD constructor.
  customizing_reader = fra_cust_obj_model_reader=>s_get_instance( ).
  customizing_reader->fill_buffer( ). " <- won't be called on your test double, so no chance to test this
ENDMETHOD.

In Betracht ziehen, das ABAP-Testdoppelwerkzeug zu verwenden

DATA(customizing_reader) = CAST /clean/customizing_reader( cl_abap_testdouble=>create( '/clean/default_custom_reader' ) ).
cl_abap_testdouble=>configure_call( customizing_reader )->returning( sub_claim_customizing ).
customizing_reader->read( 'SOME_ID' ).

Kürzer und einfacher zu verstehen als individuell erstellte Testdoubles:

" Anti-Muster
CLASS /dirty/default_custom_reader DEFINITION FOR TESTING CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES /dirty/customizing_reader.
    DATA customizing TYPE /dirty/customizing_table.
ENDCLASS.

CLASS /dirty/default_custom_reader IMPLEMENTATION.
  METHOD /dirty/customizing_reader~read.
    result = customizing.
  ENDMETHOD.
ENDCLASS.

METHOD test_something.
  DATA(customizing_reader) = NEW /dirty/customizing_reader( ).
  customizing_reader->customizing = sub_claim_customizing.
ENDMETHOD.

Nutze die Testwerkzeuge aus.

Im Allgemeinen ermöglicht ein sauberer Programmierstil das meiste der Arbeit mit standardmäßigen ABAP-Unit-Tests und Testdoubles. Es gibt jedoch Tools, mit denen du trickreichere Fälle auf elegante Weise angehen kannst:

  • Verwende die OSQL-Testumgebung (CL_OSQL_TEST_ENVIRONMENT), um komplexe OpenSQL-Anweisungen zu testen, indem du sie auf Testdaten umleitest, die im Unit-Test definiert sind, ohne den Rest des Systems zu beeinträchtigen.
  • Nutze die CDS-Testumgebung (CL_CDS_TEST_ENVIRONMENT), um deine CDS-Sichten zu testen.
  • Verwende die AUTHORITY-CHECK-Testhilfs-API (CL_AUNIT_AUTHORITY_CHECK), um Berechtigungen während der Laufzeit des Tests zu entziehen. Dies hilft, das Verhalten des Codes für verschiedene Berechtigungen zu testen. Hier ist ein Beispielcode:
DATA(user_role_authorizations) = value cl_aunit_auth_check_types_def=>user_role_authorizations(
  ( role_authorizations = value #(
    ( object = 'S_DEVELOP' authorizations = value #(
      ( value #(
        ( fieldname = 'ACTVT'
          fieldvalues = value #(
            ( lower_value = '02' )
          )
        )
      )
      )
    )
    )
  )
).

DATA(authority_check_controller) = cl_aunit_authority_check=>get_controller( ).
DATA(authority_object_set) = cl_aunit_authority_check=>create_auth_object_set( user_role_authorizations ).

authority_check_controller->restrict_authorizations_to( authority_object_set ).

Verwende Testnähte als temporäre Lösung

Wenn alle anderen Techniken versagen oder wenn Du Dich in gefährlichem flachen Gewässer von Legacy-Code befindest, solltest Du darauf verzichten, Testnähte zu verwenden, um die Dinge testbar zu machen.

Obwohl sie auf den ersten Blick bequem erscheinen, sind Testnähte invasiv und neigen dazu, sich in private Abhängigkeiten zu verstricken, sodass sie schwer aufrechtzuerhalten und stabil im langfristigen Betrieb zu halten sind.

Daher wird empfohlen, Testnähte nur als vorübergehende Lösung zu verwenden, um Dir die Refakturierung des Codes in eine testbarere Form zu ermöglichen.

Verwende LOCAL FRIENDS, um auf den konstruktorabhängigen Konstruktor zuzugreifen

CLASS /clean/unit_tests DEFINITION.
  PRIVATE SECTION.
    DATA cut TYPE REF TO /clean/interface_under_test.
    METHODS setup.
ENDCLASS.

CLASS /clean/class_under_test DEFINITION LOCAL FRIENDS unit_tests.

CLASS unit_tests IMPLEMENTATION.
  METHOD setup.
    DATA(mock) = cl_abap_testdouble=>create( '/clean/some_mock' ).
    " /clean/class_under_test is CREATE PRIVATE
     " so this only works because of the LOCAL FRIENDS
    cut = NEW /clean/class_under_test( mock ).
  ENDMETHOD.
ENDCLASS.

Missbrauche LOCAL FRIENDS nicht, um den getesteten Code zu durchbrechen

Unit-Tests, die auf private und geschützte Mitglieder zugreifen, um Mock-Daten einzufügen, sind fragil: Sie brechen, wenn sich die interne Struktur des getesteten Codes ändert.

" Anti-Muster
CLASS /dirty/class_under_test DEFINITION LOCAL FRIENDS unit_tests.
CLASS unit_tests IMPLEMENTATION.
  METHOD returns_right_result.
    cut->some_private_member = 'AUNIT_DUMMY'.
  ENDMETHOD.
ENDCLASS.

Füge keine Funktionen zum Produktionscode hinzu, die nur für die Verwendung während automatischer Tests gedacht sind

Wie bereits unter Testnähte beschrieben, sollte vermieden werden, dem Produktionscode etwas hinzuzufügen, das ausschließlich für die Verwendung während automatischer Tests gedacht ist.

" Anti-Muster
IF is_unit_test_running = abap_true.
  "Hier ist eine Logik, die nur während der Unit-Tests ausgeführt wird.
ENDIF.

Beachte, dass Testfunktionen, die von einem Endbenutzer ausgeführt werden sollen, z. B. simuliertes Buchen oder das Ausführen eines Berichts im Testmodus, Teil des Anwendungsbereichs sind und weiterhin einen gültigen Anwendungsfall darstellen.

Verwende keine Unterklassen, um Methoden zu simulieren

Verwende keine Unterklassen und das Überschreiben von Methoden, um sie in deinen Unit-Tests zu simulieren. Obwohl dies funktioniert, ist es brüchig, da die Tests leicht brechen, wenn der Code refaktoriert wird. Es ermöglicht auch echten Verbrauchern, deine Klasse zu erben, was dich unvorbereitet treffen kann, wenn du nicht ausdrücklich dafür entwirfst.

" Anti-Muster
CLASS unit_tests DEFINITION INHERITING FROM /dirty/real_class FOR TESTING [...].
  PROTECTED SECTION.
    METHODS needs_to_be_mocked REDEFINITION.

Um Legacy-Code testbar zu machen, verwende anstelle von Unterklassen und dem Überschreiben von Methoden Testnähte. Sie sind genauso brüchig, aber immer noch der sauberere Weg, da sie zumindest das Verhalten der Klasse in der Produktion nicht ändern, wie es geschieht, wenn Du die vorherige FINAL-Flagge entfernst oder die Methodenberechtigung von PRIVATE auf PROTECTED änderst.

Wenn Du neuen Code schreibst, berücksichtige dieses Testbarkeitsproblem direkt bei der Gestaltung der Klasse und finde einen anderen, besseren Weg. Gängige Best Practices umfassen die Verwendung anderer Testwerkzeuge und das Auslagern der Problem-Methode in eine separate Klasse mit ihrer eigenen Schnittstelle.

Dies ist eine spezifischere Variante von „Ändere den Produktionscode nicht, um den Code testbar zu machen“.

Simuliere nicht Dinge, die nicht benötigt werden

cut = NEW /clean/class_under_test( db_reader = db_reader
                                   config    = VALUE #( )
                                   writer    = VALUE #( ) ).

Definiere Deine „Given“-Bedingungen so genau wie möglich: Setze keine Daten, die Dein Test nicht benötigt, und simuliere keine Objekte, die nie aufgerufen werden. Diese Dinge lenken den Leser von dem ab, was tatsächlich passiert.

" Anti-Muster
cut = NEW /dirty/class_under_test( db_reader = db_reader
                                   config    = config
                                   writer    = writer ).

Es gibt auch Fälle, in denen es überhaupt nicht notwendig ist, etwas zu simulieren. Dies ist in der Regel der Fall bei Datenstrukturen und Datencontainern. Zum Beispiel können deine Unit-Tests gut mit der Produktionsversion eines transient_log arbeiten, da es nur Daten speichert, ohne Nebenwirkungen.

Erstelle keine Testframeworks

Unit-Tests – im Gegensatz zu Integrationstests – sollten Daten-ein-Daten-aus sein, wobei alle Testdaten bei Bedarf spontan definiert werden.

cl_abap_testdouble=>configure_call( test_double )->returning( data ).

Beginne nicht damit, Frameworks zu erstellen, die „Testfall-IDs“ unterscheiden, um zu entscheiden, welche Daten bereitgestellt werden sollen. Der resultierende Code wird so lang und verwickelt sein, dass Du diese Tests auf lange Sicht nicht aufrechterhalten kannst.

" Anti-Muster

test_double->set_test_case( 1 ).

CASE test_case.
  WHEN 1.
  WHEN 2.
ENDCASE.

Testmethoden

Testmethodennamen: Spiegeln wider, was gegeben ist und erwartet wird

Gute Namen spiegeln wider, was gegeben ist und was erwartet wird:

METHOD reads_existing_entry.
METHOD throws_on_invalid_key.
METHOD detects_invalid_input.

Schlechte Namen spiegeln das „Wenn“ wider, wiederholen bedeutungslose Fakten oder sind kryptisch:

" Anti-Muster

"Was wird erwartet, Erfolg oder Fehler?
METHOD get_conversion_exits.

" Es ist eine Testmethode, was sollte sie sonst tun, als "testen"?
METHOD test_loop.

" Es ist also parametrisiert, aber was ist sein Ziel?
METHOD parameterized_test.

" Was soll "_wo_w" bedeuten, und wirst du dich in einem Jahr noch daran erinnern?
METHOD get_attributes_wo_w.

Da ABAP nur 30 Zeichen in Methodennamen zulässt, ist es fair, einen erläuternden Kommentar hinzuzufügen, wenn der Name zu kurz ist, um ausreichend Bedeutung zu vermitteln. ABAP Doc oder die erste Zeile in der Testmethode können eine geeignete Wahl für den Kommentar sein.

Wenn Du viele Testmethoden mit zu langen Namen hast, kann dies ein Anzeichen dafür sein, dass Du Deine einzelne Testklasse in mehrere aufteilen und die Unterschiede in den gegebenen Bedingungen in den Klassennamen ausdrücken solltest.

Verwende „Given-When-Then“

Organisiere Deinen Testcode entlang des „Given-When-Then“-Paradigmas: Zuerst initialisiere Dinge im „Given“-Abschnitt, dann rufe die tatsächlich getestete Methode im „When“-Abschnitt auf, und schließlich validiere das Ergebnis im „Then“-Abschnitt.

Wenn die „Given“- oder „Then“-Abschnitte so lang werden, dass Du die drei Abschnitte nicht mehr visuell voneinander trennen kannst, extrahiere Unter-Methoden. Leerzeilen oder Kommentare als Trennungen mögen auf den ersten Blick gut aussehen, reduzieren jedoch nicht wirklich das visuelle Durcheinander. Dennoch sind sie hilfreich, um die Abschnitte für den Leser und den unerfahrenen Test-Schreiber zu trennen.

„When“ ist genau ein Aufruf

Stelle sicher, dass der „When“-Abschnitt Deiner Testmethode genau einen Aufruf an die zu testende Klasse enthält:

METHOD rejects_invalid_input.
  " when
  DATA(is_valid) = cut->is_valid_input( 'SOME_RANDOM_ENTRY' ).
  " then
  cl_abap_unit_assert=>assert_false( is_valid ).
ENDMETHOD.

Das Aufrufen mehrerer Dinge zeigt, dass die Methode keinen klaren Fokus hat und zu viel testet. Dies erschwert die Ursachenfindung, wenn der Test fehlschlägt: War es der erste, zweite oder dritte Aufruf, der das Versagen verursacht hat? Es verwirrt auch den Leser, weil er nicht sicher ist, was das genaue Feature unter Test ist.

Füge kein TEARDOWN hinzu, es sei denn, Du benötigst es wirklich

Teardown-Methoden werden normalerweise nur benötigt, um Datenbankeinträge oder andere externe Ressourcen in Integrationstests aufzuräumen.

Das Zurücksetzen von Elementen der Testklasse, insbesondere cut und der verwendeten Testdoubles, ist überflüssig; sie werden von der Setup-Methode vor dem Start der nächsten Testmethode überschrieben.

Testdaten

Mache es einfach, die Bedeutung zu erkennen

In Unit-Tests möchtest Du schnell erkennen können, welche Daten und Mock-Objekte wichtig sind und welche nur dazu dienen, den Code vor Abstürzen zu bewahren. Unterstütze dies, indem du Dingen, die keine Bedeutung haben, offensichtliche Namen und Werte gibst, zum Beispiel:

DATA(alert_id) = '42'.                             " Bekannte bedeutungslose Zahlen.
DATA(detection_object_type) = '?=/"&'.             " 'Tastatur Unfall'
CONSTANTS some_random_number TYPE i VALUE 782346.  " Aussagekräftige Variablennamen.

Don’t trick people into believing something connects to real objects or real customizing if it doesn’t:

" Anti-Muster
DATA(alert_id) = '00000001223678871'.        " Diese Alarm-ID existiert tatsächlich
DATA(detection_object_type) = 'FRA_SCLAIM'.  " Dieser Erkennungsobjekttyp auch
CONSTANTS memory_limit TYPE i VALUE 4096.    " Diese Zahl scheint sorgfältig ausgewählt zu sein

Mache es einfach, Unterschiede zu erkennen

exp_parameter_in = VALUE #( ( parameter_name = '45678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789END1' )
                            ( parameter_name = '45678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789END2' ) ).

Zwinge die Leser nicht dazu, lange bedeutungslose Zeichenfolgen zu vergleichen, um winzige Unterschiede zu erkennen.

Verwende Konstanten, um Zweck und Bedeutung der Testdaten zu beschreiben

CONSTANTS some_nonsense_key TYPE char8 VALUE 'ABCDEFGH'.

METHOD throws_on_invalid_entry.
  TRY.
      " when
      cut->read_entry( some_nonsense_key ).
      cl_abap_unit_assert=>fail( ).
    CATCH /clean/customizing_reader_error.
      " then
  ENDTRY.
ENDMETHOD.

Aussagen (Assertions)

Wenige, gezielte Aussagen

Behaupte nur genau das, worum es in der Testmethode geht, und das mit einer geringen Anzahl von Aussagen.

METHOD rejects_invalid_input.
  " when
  DATA(is_valid) = cut->is_valid_input( 'SOME_RANDOM_ENTRY' ).
  " then
  cl_abap_unit_assert=>assert_false( is_valid ).
ENDMETHOD.

Zu viele Behauptungen anzustellen, ist ein Anzeichen dafür, dass die Methode keinen klaren Fokus hat. Dies koppelt Produktions- und Testcode an zu vielen Stellen: Eine Änderung an einem Feature erfordert das Neuschreiben einer großen Anzahl von Tests, obwohl sie nicht wirklich mit dem geänderten Feature zu tun haben. Es verwirrt auch den Leser mit einer Vielzahl von Behauptungen und verschleiert die eine wichtige und unterscheidende Behauptung unter ihnen.

" Anti-Muster
METHOD rejects_invalid_input.
  " when
  DATA(is_valid) = cut->is_valid_input( 'SOME_RANDOM_ENTRY' ).
  " then
  cl_abap_unit_assert=>assert_false( is_valid ).
  cl_abap_unit_assert=>assert_not_initial( log->get_messages( ) ).
  cl_abap_unit_assert=>assert_equals( act = sy-langu
                                      exp = 'E' ).
ENDMETHOD.

Verwende den richtigen Typ der Behauptung (Assertion)

cl_abap_unit_assert=>assert_equals( act = table
                                    exp = test_data ).

Behauptungen (Assertions) tun oft mehr, als auf den ersten Blick ersichtlich ist. Zum Beispiel enthält assert_equals Typabgleich und bietet präzise Beschreibungen, wenn sich Werte unterscheiden. Die Verwendung der falschen, zu häufigen Behauptungen zwingt dich sofort in den Debugger, anstatt dir zu ermöglichen, anhand der Fehlermeldung zu sehen, was falsch ist.

" Anti-Muster
cl_abap_unit_assert=>assert_true( xsdbool( act = exp ) ).

Überprüfe den Inhalt, nicht die Menge

assert_contains_exactly( actual   = table
                         expected = VALUE string_table( ( `ABC` ) ( `DEF` ) ( `GHI` ) ) ).

Schreibe keine Assertions mit „Magischen Zahlen“ oder quantitativen Werten, wenn Du den tatsächlichen erwarteten Inhalt ausdrücken kannst. Zahlen können variieren, obwohl die Erwartungen erfüllt sind. Umgekehrt können die Zahlen übereinstimmen, obwohl der Inhalt völlig unerwartet ist.

" Anti-Muster
assert_equals( act = lines( log_messages )
               exp = 3 ).

Überprüfe die Qualität, nicht den Inhalt.

Wenn Du an einer Metaqualität des Ergebnisses interessiert bist, aber nicht am tatsächlichen Inhalt selbst, drücke dies mit einer geeigneten Behauptung aus:

assert_all_lines_shorter_than( actual_lines        = table
                               expected_max_length = 80 ).

Das genaue Überprüfen des Inhalts verschleiert, was Du tatsächlich testen möchtest. Es ist auch brüchig, da eine Refaktorisierung möglicherweise ein anderes, aber vollkommen akzeptables Ergebnis erzeugt, obwohl es alle Deine zu präzisen Unit-Tests kaputt macht.

" Anti-Muster
assert_equals( act = table
               exp = VALUE string_table( ( `ABC` ) ( `DEF` ) ( `GHI` ) ) ).

Verwende FAIL, um auf erwartete Ausnahmen zu prüfen

METHOD throws_on_empty_input.
  TRY.
      " when
      cut->do_something( '' ).
      cl_abap_unit_assert=>fail( ).
    CATCH /clean/some_exception.
      " then
  ENDTRY.
ENDMETHOD.

Leite unerwartete Ausnahmen weiter, anstatt sie abzufangen und zu einem Fehler zu machen

METHODS reads_entry FOR TESTING RAISING /clean/some_exception.

METHOD reads_entry.
  "when
  DATA(entry) = cut->read_something( ).
  "then
  cl_abap_unit_assert=>assert_not_initial( entry ).
ENDMETHOD.

Dein Testcode bleibt auf dem „Happy Path“ konzentriert und ist daher viel einfacher zu lesen und zu verstehen, im Vergleich zu:

" Anti-Muster
METHOD reads_entry.
  TRY.
      DATA(entry) = cut->read_something( ).
    CATCH /clean/some_exception INTO DATA(unexpected_exception).
      cl_abap_unit_assert=>fail( unexpected_exception->get_text( ) ).
  ENDTRY.
  cl_abap_unit_assert=>assert_not_initial( entry ).
ENDMETHOD.

Schreibe benutzerdefinierte Behauptungen, um den Code zu verkürzen und Duplikate zu vermeiden

METHODS assert_contains
  IMPORTING
    actual_entries TYPE STANDARD TABLE OF entries_tab
    expected_key   TYPE key_structure.

METHOD assert_contains.
  TRY.
      actual_entries[ key = expected_key ].
    CATCH cx_sy_itab_line_not_found.
      cl_abap_unit_assert=>fail( |Couldn't find the key { expected_key }| ).
  ENDTRY.
ENDMETHOD.

Anstatt dies immer wieder zu kopieren und einzufügen.

Über den Autor

Andreas Geiger

Mein Name ist Andreas Geiger und ich bin ein erfahrener Senior SAP Berater. Mit mehr als 10 Jahren Berufserfahrung habe ich mehrere SAP-Projekte erfolgreich abgeschlossen und umfangreiche Kenntnisse in verschiedenen Bereichen wie SAP FI, SAP MM und ABAP erworben. Nun möchte ich mein Wissen mit Dir teilen, um Dir einen Mehrwert zu bieten und Dich bei Deiner täglichen Arbeit mit dem SAP-System zu unterstützen.

Mehr zu ERP UP

ERP UP unterstützen

Wenn Du mit ERP UP zufrieden bist, kannst Du mich gerne unterstützen. Dabei gibt es unzählige Möglichkeiten, wie Du mich einfach und schnell unterstützen kannst. Wie Du genau ERP UP unterstützen kannst, erfährst Du hier. Vielen Dank.

1 Gedanke zu „Clean ABAP – Clean Code in ABAP“

Schreibe einen Kommentar