Clean ABAP ist die Anwendung von Clean Code Prinzipien auf ABAP-Entwicklung. Das Ziel: Code, der leicht zu lesen, zu verstehen, zu warten und zu testen ist.
Warum Clean ABAP?
| Problem | Clean ABAP Lösung |
|---|---|
| Code schwer verständlich | Aussagekräftige Namen |
| Methoden mit 500+ Zeilen | Kleine, fokussierte Methoden |
| Copy-Paste Fehler | DRY-Prinzip (Don’t Repeat Yourself) |
| Schwer zu testen | Dependency Injection |
| Unklare Fehlerbehandlung | Exceptions statt Returncodes |
Naming Conventions
Gute Namen machen Kommentare überflüssig und Code selbsterklärend.
Variablen und Attribute
" SCHLECHT: Unklare AbkürzungenDATA: lv_c TYPE i, lv_t TYPE string, lt_d TYPE TABLE OF mara.
" GUT: Aussagekräftige NamenDATA: lv_customer_count TYPE i, lv_travel_status TYPE string, lt_materials TYPE TABLE OF mara.Methoden
" SCHLECHT: Unklar was die Methode tutMETHODS process.METHODS do_it.METHODS handle.
" GUT: Verb + Objekt beschreibt die AktionMETHODS calculate_total_price.METHODS validate_customer_data.METHODS send_confirmation_email.Boolesche Variablen und Methoden
" SCHLECHT: Keine Frage erkennbarDATA lv_status TYPE abap_bool.METHODS check_customer.
" GUT: Fragen mit is_, has_, can_DATA lv_is_active TYPE abap_bool.DATA lv_has_items TYPE abap_bool.DATA lv_can_be_deleted TYPE abap_bool.
METHODS is_customer_valid RETURNING VALUE(rv_valid) TYPE abap_bool.METHODS has_open_orders RETURNING VALUE(rv_has_orders) TYPE abap_bool.Klassen und Interfaces
" SCHLECHT: Unklare VerantwortungCLASS zcl_helper DEFINITION.CLASS zcl_utils DEFINITION.CLASS zcl_manager DEFINITION.
" GUT: Klare Verantwortung im NamenCLASS zcl_invoice_calculator DEFINITION.CLASS zcl_email_sender DEFINITION.CLASS zcl_customer_validator DEFINITION.
" Interfaces mit IF_ PräfixINTERFACE zif_payment_provider DEFINITION.INTERFACE zif_logger DEFINITION.Methoden-Design
Klein und fokussiert
Eine Methode sollte eine Aufgabe erfüllen und auf einen Bildschirm passen.
" SCHLECHT: Methode macht zu vielMETHOD process_order. " 1. Validierung (50 Zeilen) IF customer_id IS INITIAL. " ... Fehlerbehandlung ENDIF. " ... weitere Validierungen
" 2. Preisberechnung (80 Zeilen) SELECT * FROM pricing_conditions... LOOP AT lt_items... " ... komplexe Berechnung ENDLOOP.
" 3. Datenbank-Update (40 Zeilen) MODIFY ztable FROM TABLE lt_data. " ... weitere Updates
" 4. E-Mail versenden (30 Zeilen) " ... E-Mail-LogikENDMETHOD.
" GUT: Aufgeteilt in fokussierte MethodenMETHOD process_order. validate_order( is_order ). DATA(lv_total) = calculate_total_price( is_order-items ). save_order( is_order ). send_order_confirmation( is_order ).ENDMETHOD.
METHOD validate_order. IF is_order-customer_id IS INITIAL. RAISE EXCEPTION TYPE zcx_invalid_order EXPORTING textid = zcx_invalid_order=>customer_missing. ENDIF. " ... weitere ValidierungenENDMETHOD.
METHOD calculate_total_price. rv_total = REDUCE #( INIT sum = 0 FOR item IN it_items NEXT sum = sum + item-quantity * item-price ).ENDMETHOD.Parameter Design
" SCHLECHT: Zu viele ParameterMETHODS create_invoice IMPORTING iv_customer_id TYPE kunnr iv_customer_name TYPE string iv_street TYPE string iv_city TYPE string iv_postal_code TYPE string iv_country TYPE land1 iv_invoice_date TYPE dats iv_due_date TYPE dats iv_currency TYPE waers iv_payment_terms TYPE string.
" GUT: Struktur verwendenTYPES: BEGIN OF ty_invoice_data, customer TYPE ty_customer, invoice TYPE ty_invoice_header, items TYPE tt_invoice_items, END OF ty_invoice_data.
METHODS create_invoice IMPORTING is_invoice_data TYPE ty_invoice_data.RETURNING vs. EXPORTING
" SCHLECHT: EXPORTING für einzelne WerteMETHODS get_customer_name IMPORTING iv_customer_id TYPE kunnr EXPORTING ev_name TYPE string.
" Aufruf umständlichget_customer_name( EXPORTING iv_customer_id = lv_id IMPORTING ev_name = lv_name).
" GUT: RETURNING für einzelne WerteMETHODS get_customer_name IMPORTING iv_customer_id TYPE kunnr RETURNING VALUE(rv_name) TYPE string.
" Aufruf elegantDATA(lv_name) = get_customer_name( lv_customer_id ).Bedingte Logik
Positive Bedingungen bevorzugen
" SCHLECHT: Doppelte NegationIF NOT is_invalid = abap_true. " ...ENDIF.
IF NOT has_no_items( ). " ...ENDIF.
" GUT: Positive FormulierungIF is_valid = abap_true. " ...ENDIF.
IF has_items( ). " ...ENDIF.Guard Clauses statt verschachtelte IF
" SCHLECHT: Tiefe VerschachtelungMETHOD calculate_discount. IF customer IS NOT INITIAL. IF customer-is_active = abap_true. IF order-total > 1000. IF customer-loyalty_level >= 3. rv_discount = 15. ELSE. rv_discount = 10. ENDIF. ELSE. rv_discount = 5. ENDIF. ENDIF. ENDIF.ENDMETHOD.
" GUT: Guard Clauses (frühe Returns)METHOD calculate_discount. " Vorbedingungen prüfen IF customer IS INITIAL. RETURN. ENDIF.
IF customer-is_active = abap_false. RETURN. ENDIF.
" Hauptlogik - flach und lesbar IF order-total > 1000 AND customer-loyalty_level >= 3. rv_discount = 15. RETURN. ENDIF.
IF order-total > 1000. rv_discount = 10. RETURN. ENDIF.
rv_discount = 5.ENDMETHOD.CASE statt IF-Ketten
" SCHLECHT: Lange IF-ELSEIF-KetteIF lv_status = 'N'. lv_text = 'Neu'.ELSEIF lv_status = 'P'. lv_text = 'In Bearbeitung'.ELSEIF lv_status = 'C'. lv_text = 'Abgeschlossen'.ELSEIF lv_status = 'X'. lv_text = 'Storniert'.ENDIF.
" GUT: CASE ist klarerlv_text = SWITCH #( lv_status WHEN 'N' THEN 'Neu' WHEN 'P' THEN 'In Bearbeitung' WHEN 'C' THEN 'Abgeschlossen' WHEN 'X' THEN 'Storniert' ELSE 'Unbekannt').Fehlerbehandlung
Exceptions statt Returncodes
" SCHLECHT: Returncode-basierte FehlerbehandlungMETHODS validate_order IMPORTING is_order TYPE ty_order EXPORTING ev_error TYPE string RETURNING VALUE(rv_success) TYPE abap_bool.
" Aufruf mit komplizierter PrüfungIF validate_order( EXPORTING is_order = ls_order IMPORTING ev_error = lv_error ) = abap_false. MESSAGE lv_error TYPE 'E'.ENDIF.
" GUT: Exception-basierte FehlerbehandlungMETHODS validate_order IMPORTING is_order TYPE ty_order RAISING zcx_order_validation.
" Aufruf mit TRY-CATCHTRY. validate_order( ls_order ). CATCH zcx_order_validation INTO DATA(lx_error). MESSAGE lx_error TYPE 'E'.ENDTRY.Exception-Klassen
" Eigene Exception-Klasse mit NachrichtentextenCLASS zcx_order_validation DEFINITION INHERITING FROM cx_static_check CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_t100_message.
CONSTANTS: BEGIN OF customer_not_found, msgid TYPE symsgid VALUE 'ZORDER', msgno TYPE symsgno VALUE '001', attr1 TYPE scx_attrname VALUE 'CUSTOMER_ID', attr2 TYPE scx_attrname VALUE '', attr3 TYPE scx_attrname VALUE '', attr4 TYPE scx_attrname VALUE '', END OF customer_not_found,
BEGIN OF invalid_quantity, msgid TYPE symsgid VALUE 'ZORDER', msgno TYPE symsgno VALUE '002', attr1 TYPE scx_attrname VALUE 'QUANTITY', attr2 TYPE scx_attrname VALUE '', attr3 TYPE scx_attrname VALUE '', attr4 TYPE scx_attrname VALUE '', END OF invalid_quantity.
DATA customer_id TYPE kunnr READ-ONLY. DATA quantity TYPE i READ-ONLY.
METHODS constructor IMPORTING textid LIKE if_t100_message=>t100key OPTIONAL previous TYPE REF TO cx_root OPTIONAL customer_id TYPE kunnr OPTIONAL quantity TYPE i OPTIONAL.ENDCLASS.Fehler sinnvoll behandeln
" SCHLECHT: Fehler verschluckenTRY. do_something( ). CATCH cx_root. " Nichts tun - Fehler ignoriert!ENDTRY.
" SCHLECHT: Alle Fehler gleich behandelnCATCH cx_root INTO DATA(lx_error). WRITE: 'Ein Fehler ist aufgetreten'.
" GUT: Spezifische FehlerbehandlungTRY. send_email( ls_email ). CATCH zcx_email_invalid_address INTO DATA(lx_invalid). " Benutzer informieren - behebbar MESSAGE lx_invalid TYPE 'W'. CATCH zcx_email_server_error INTO DATA(lx_server). " Logging für Admin - Retry später log_error( lx_server ). RAISE EXCEPTION lx_server.ENDTRY.Kommentare und Dokumentation
Code sollte selbsterklärend sein
" SCHLECHT: Kommentar erklärt offensichtlichen Code" Addiere 1 zu counterADD 1 TO lv_counter.
" Prüfe ob Kunde aktiv istIF customer-is_active = abap_true.
" Schleife über alle ItemsLOOP AT lt_items INTO DATA(ls_item).
" GUT: Kommentar erklärt WARUM, nicht WAS" Temporärer Fix bis Release 2.0 - siehe Issue #4711lv_offset = lv_offset + 1.
" Archivierte Kunden werden separat in Batch-Job verarbeitetIF customer-is_active = abap_true.ABAP Doc für öffentliche Methoden
"! <p class="shorttext synchronized">Berechnet den Gesamtpreis inkl. Rabatt</p>"!"! @parameter it_items | <p class="shorttext synchronized">Bestellpositionen</p>"! @parameter iv_discount_percent | <p class="shorttext synchronized">Rabatt in Prozent (0-100)</p>"! @parameter rv_total | <p class="shorttext synchronized">Gesamtpreis nach Rabatt</p>"! @raising zcx_calculation | <p class="shorttext synchronized">Bei ungültigen Eingabewerten</p>METHODS calculate_total IMPORTING it_items TYPE tt_order_items iv_discount_percent TYPE i DEFAULT 0 RETURNING VALUE(rv_total) TYPE netwr RAISING zcx_calculation.Testbarkeit
Dependency Injection
" SCHLECHT: Direkte Abhängigkeit - nicht testbarCLASS zcl_order_processor DEFINITION. PUBLIC SECTION. METHODS process_order IMPORTING is_order TYPE ty_order.ENDCLASS.
CLASS zcl_order_processor IMPLEMENTATION. METHOD process_order. " Direkte Instanzierung - schwer zu testen DATA(lo_email_sender) = NEW zcl_email_sender( ). lo_email_sender->send( ... ).
" Direkte DB-Zugriffe - Test verändert echte Daten MODIFY ztable FROM ls_data. ENDMETHOD.ENDCLASS.
" GUT: Dependency Injection - testbarCLASS zcl_order_processor DEFINITION. PUBLIC SECTION. METHODS constructor IMPORTING io_email_sender TYPE REF TO zif_email_sender io_repository TYPE REF TO zif_order_repository.
METHODS process_order IMPORTING is_order TYPE ty_order.
PRIVATE SECTION. DATA mo_email_sender TYPE REF TO zif_email_sender. DATA mo_repository TYPE REF TO zif_order_repository.ENDCLASS.
CLASS zcl_order_processor IMPLEMENTATION. METHOD constructor. mo_email_sender = io_email_sender. mo_repository = io_repository. ENDMETHOD.
METHOD process_order. " Verwendet injizierte Abhängigkeiten mo_repository->save( is_order ). mo_email_sender->send_confirmation( is_order ). ENDMETHOD.ENDCLASS.Test Double (Mock)
" Test mit Mock-ObjektenCLASS ltc_order_processor DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION. DATA mo_cut TYPE REF TO zcl_order_processor. " Class Under Test DATA mo_email_mock TYPE REF TO ltd_email_sender. DATA mo_repo_mock TYPE REF TO ltd_order_repository.
METHODS setup. METHODS test_process_sends_email FOR TESTING.ENDCLASS.
CLASS ltc_order_processor IMPLEMENTATION. METHOD setup. " Test Doubles erstellen mo_email_mock = NEW ltd_email_sender( ). mo_repo_mock = NEW ltd_order_repository( ).
" Class Under Test mit Mocks instanzieren mo_cut = NEW zcl_order_processor( io_email_sender = mo_email_mock io_repository = mo_repo_mock ). ENDMETHOD.
METHOD test_process_sends_email. " Given DATA(ls_order) = VALUE ty_order( order_id = '12345' ).
" When mo_cut->process_order( ls_order ).
" Then cl_abap_unit_assert=>assert_true( act = mo_email_mock->was_send_called msg = 'E-Mail sollte versendet werden' ). ENDMETHOD.ENDCLASS.
" Test Double für Email SenderCLASS ltd_email_sender DEFINITION FOR TESTING. PUBLIC SECTION. INTERFACES zif_email_sender. DATA was_send_called TYPE abap_bool.ENDCLASS.
CLASS ltd_email_sender IMPLEMENTATION. METHOD zif_email_sender~send_confirmation. was_send_called = abap_true. ENDMETHOD.ENDCLASS.Vorher/Nachher: Komplettes Beispiel
Vorher: Schwer wartbarer Code
METHOD calc. DATA: lt_d TYPE TABLE OF mara, lv_t TYPE p DECIMALS 2, lv_c TYPE i.
SELECT * FROM mara INTO TABLE lt_d WHERE matnr IN @s_matnr.
LOOP AT lt_d INTO DATA(ls_d). IF ls_d-mtart = 'FERT' OR ls_d-mtart = 'HALB'. SELECT SINGLE netpr FROM a] INTO @DATA(lv_p) WHERE matnr = @ls_d-matnr. IF sy-subrc = 0. lv_t = lv_t + lv_p. lv_c = lv_c + 1. ENDIF. ENDIF. ENDLOOP.
IF lv_c > 0. rv_result = lv_t / lv_c. ENDIF.ENDMETHOD.Nachher: Clean ABAP
"! <p class="shorttext synchronized">Berechnet Durchschnittspreis für Fertigprodukte</p>"!"! @parameter it_material_range | <p class="shorttext synchronized">Materialnummern-Selektion</p>"! @parameter rv_average_price | <p class="shorttext synchronized">Durchschnittspreis</p>METHOD calculate_average_price_for_finished_goods. DATA(lt_materials) = get_finished_goods( it_material_range ).
IF lt_materials IS INITIAL. RETURN. ENDIF.
DATA(lt_prices) = get_prices_for_materials( lt_materials ).
rv_average_price = calculate_average( lt_prices ).ENDMETHOD.
METHOD get_finished_goods. SELECT matnr FROM mara WHERE matnr IN @it_material_range AND mtart IN ('FERT', 'HALB') INTO TABLE @rt_materials.ENDMETHOD.
METHOD get_prices_for_materials. SELECT matnr, netpr FROM a] FOR ALL ENTRIES IN @it_materials WHERE matnr = @it_materials-matnr INTO TABLE @rt_prices.ENDMETHOD.
METHOD calculate_average. CHECK it_prices IS NOT INITIAL.
DATA(lv_total) = REDUCE netwr( INIT sum = CONV netwr( 0 ) FOR price IN it_prices NEXT sum = sum + price-netpr ).
rv_average = lv_total / lines( it_prices ).ENDMETHOD.Checkliste für Clean ABAP
| Bereich | Prüffrage |
|---|---|
| Naming | Kann ich den Zweck ohne Kommentar verstehen? |
| Methoden | Passt die Methode auf einen Bildschirm? |
| Methoden | Hat die Methode nur eine Aufgabe? |
| Parameter | Sind es maximal 3-4 Parameter? |
| Bedingungen | Sind IF-Verschachtelungen max. 2 Ebenen tief? |
| Fehler | Werden Exceptions statt Returncodes verwendet? |
| Tests | Kann die Klasse mit Mocks getestet werden? |
| Kommentare | Erklärt der Kommentar das WARUM, nicht das WAS? |
Weiterführende Ressourcen
- SAP Clean ABAP Guide - Offizieller SAP Style Guide
- ABAP Test Cockpit (ATC) - Automatische Code-Prüfung
- RAP Basics - Modernes ABAP-Entwicklungsmodell
- Inline-Deklarationen - Moderne ABAP-Syntax