Die Hexagonale Architektur (auch Ports & Adapters genannt) ist ein Architekturmuster, das die Kerngeschaeftslogik von externen Abhaengigkeiten isoliert. In ABAP Cloud ermoeglicht dieses Muster testbare, wartbare und flexible Anwendungen.
Was ist die Hexagonale Architektur?
Die Hexagonale Architektur wurde von Alistair Cockburn entwickelt und loest ein fundamentales Problem: Wie trenne ich Geschaeftslogik von technischen Details?
┌─────────────────────────────────────────────┐ │ DRIVING ADAPTERS │ │ (UI, REST API, Tests, Batch Jobs) │ └──────────────────┬──────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────┐ │ INBOUND PORTS │ │ (Interfaces / Use Cases) │ └──────────────────┬───────────────────────────┘ │ ┌──────────────────▼───────────────────────────┐ │ │ │ DOMAIN CORE │ │ (Business Logic) │ │ │ └──────────────────┬───────────────────────────┘ │ ┌──────────────────▼───────────────────────────┐ │ OUTBOUND PORTS │ │ (Repository, Service Interfaces) │ └──────────────────┬───────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────┐ │ DRIVEN ADAPTERS │ │ (Database, External APIs, File System) │ └──────────────────────────────────────────────┘Kernkonzepte
| Konzept | Beschreibung | ABAP Cloud Mapping |
|---|---|---|
| Domain Core | Reine Geschaeftslogik ohne Abhaengigkeiten | ABAP Klassen mit Interfaces |
| Inbound Port | Schnittstelle fuer eingehende Anfragen | Interface fuer Use Cases |
| Outbound Port | Schnittstelle fuer ausgehende Aufrufe | Interface fuer Repositories/Services |
| Driving Adapter | Ruft die Anwendung auf | RAP Behavior Handler, HTTP Handler |
| Driven Adapter | Wird von der Anwendung aufgerufen | Repository Implementation, HTTP Client |
Warum Hexagonale Architektur in ABAP Cloud?
Vorteile
- Testbarkeit: Domain-Logik kann ohne Datenbank oder externe Services getestet werden
- Austauschbarkeit: Adapter koennen ohne Aenderung der Geschaeftslogik ersetzt werden
- Unabhaengigkeit: Die Domain kennt keine technischen Details (RAP, HTTP, DB)
- Wartbarkeit: Klare Grenzen erleichtern Aenderungen
- Framework-Unabhaengigkeit: Migration auf neue Technologien wird einfacher
Wann lohnt sich der Aufwand?
| Szenario | Empfehlung |
|---|---|
| Komplexe Geschaeftslogik mit vielen Regeln | Hexagonale Architektur |
| Einfache CRUD-Operationen | Standard RAP (Managed) |
| Integration mehrerer externer Services | Hexagonale Architektur |
| Prototyp oder MVP | Standard RAP |
| Langlebige Enterprise-Anwendung | Hexagonale Architektur |
| Hohe Testabdeckung erforderlich | Hexagonale Architektur |
Mapping auf ABAP Cloud Konzepte
┌─────────────────────────────────────────────────────────────────┐│ ABAP CLOUD HEXAGONAL ││ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ DRIVING ADAPTERS │ ││ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ ││ │ │ RAP │ │ HTTP │ │ Background Job │ │ ││ │ │ Behavior │ │ Handler │ │ (Application Job) │ │ ││ │ └─────┬──────┘ └─────┬──────┘ └──────────┬─────────────┘ │ ││ └───────┼──────────────┼───────────────────┼───────────────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ INBOUND PORTS (Interfaces) │ ││ │ zif_order_service, zif_invoice_service │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ APPLICATION LAYER (Use Cases) │ ││ │ zcl_create_order_use_case, zcl_cancel_order_use_case │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ DOMAIN LAYER │ ││ │ zcl_order (Entity), zcl_order_line (Entity) │ ││ │ zcl_price_calculator (Domain Service) │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ OUTBOUND PORTS (Interfaces) │ ││ │ zif_order_repository, zif_customer_service │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ DRIVEN ADAPTERS │ ││ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ ││ │ │ DB │ │ HTTP │ │ SAP ERP │ │ ││ │ │ Repository │ │ Client │ │ Integration │ │ ││ │ └────────────┘ └────────────┘ └────────────────────────┘ │ ││ └──────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘Domain Layer: Die Geschaeftslogik
Der Domain Layer enthaelt die reine Geschaeftslogik ohne jede Abhaengigkeit zu technischen Frameworks.
Domain Entity
" Domain Entity: Order" Keine Abhaengigkeiten zu RAP, Datenbank oder externen ServicesCLASS zcl_order DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. TYPES: BEGIN OF ty_order_data, order_id TYPE sysuuid_x16, customer_id TYPE sysuuid_x16, status TYPE string, total_amount TYPE decfloat34, currency TYPE waers, created_at TYPE timestampl, END OF ty_order_data,
BEGIN OF ty_order_line, line_id TYPE i, product_id TYPE sysuuid_x16, quantity TYPE i, unit_price TYPE decfloat34, amount TYPE decfloat34, END OF ty_order_line, ty_order_lines TYPE STANDARD TABLE OF ty_order_line WITH KEY line_id.
" Factory Method CLASS-METHODS create IMPORTING iv_customer_id TYPE sysuuid_x16 iv_currency TYPE waers RETURNING VALUE(ro_order) TYPE REF TO zcl_order RAISING zcx_order_error.
" Reconstruct from persistence CLASS-METHODS reconstitute IMPORTING is_data TYPE ty_order_data it_lines TYPE ty_order_lines RETURNING VALUE(ro_order) TYPE REF TO zcl_order.
" Business Methods METHODS add_line IMPORTING iv_product_id TYPE sysuuid_x16 iv_quantity TYPE i iv_unit_price TYPE decfloat34 RAISING zcx_order_error.
METHODS submit RAISING zcx_order_error.
METHODS cancel IMPORTING iv_reason TYPE string RAISING zcx_order_error.
" Getters METHODS get_data RETURNING VALUE(rs_data) TYPE ty_order_data.
METHODS get_lines RETURNING VALUE(rt_lines) TYPE ty_order_lines.
METHODS is_editable RETURNING VALUE(rv_editable) TYPE abap_bool.
PRIVATE SECTION. DATA ms_data TYPE ty_order_data. DATA mt_lines TYPE ty_order_lines. DATA mv_next_line_id TYPE i.
METHODS calculate_total.
CONSTANTS: c_status_draft TYPE string VALUE 'DRAFT', c_status_submitted TYPE string VALUE 'SUBMITTED', c_status_cancelled TYPE string VALUE 'CANCELLED'.ENDCLASS.
CLASS zcl_order IMPLEMENTATION.
METHOD create. " Business Rule: Customer ID is mandatory IF iv_customer_id IS INITIAL. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e001(zorder) WITH 'Customer ID is required'. ENDIF.
ro_order = NEW zcl_order( ). ro_order->ms_data = VALUE #( order_id = cl_system_uuid=>create_uuid_x16_static( ) customer_id = iv_customer_id currency = iv_currency status = c_status_draft created_at = utclong_current( ) ). ro_order->mv_next_line_id = 1. ENDMETHOD.
METHOD reconstitute. ro_order = NEW zcl_order( ). ro_order->ms_data = is_data. ro_order->mt_lines = it_lines. ro_order->mv_next_line_id = REDUCE #( INIT max = 0 FOR line IN it_lines NEXT max = COND #( WHEN line-line_id > max THEN line-line_id ELSE max ) ) + 1. ENDMETHOD.
METHOD add_line. " Business Rule: Can only add lines to draft orders IF ms_data-status <> c_status_draft. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e002(zorder) WITH 'Cannot add lines to non-draft order'. ENDIF.
" Business Rule: Quantity must be positive IF iv_quantity <= 0. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e003(zorder) WITH 'Quantity must be positive'. ENDIF.
APPEND VALUE #( line_id = mv_next_line_id product_id = iv_product_id quantity = iv_quantity unit_price = iv_unit_price amount = iv_quantity * iv_unit_price ) TO mt_lines.
mv_next_line_id = mv_next_line_id + 1. calculate_total( ). ENDMETHOD.
METHOD submit. " Business Rule: Cannot submit empty order IF mt_lines IS INITIAL. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e004(zorder) WITH 'Cannot submit empty order'. ENDIF.
" Business Rule: Only draft orders can be submitted IF ms_data-status <> c_status_draft. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e005(zorder) WITH 'Only draft orders can be submitted'. ENDIF.
ms_data-status = c_status_submitted. ENDMETHOD.
METHOD cancel. " Business Rule: Cannot cancel already cancelled orders IF ms_data-status = c_status_cancelled. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e006(zorder) WITH 'Order is already cancelled'. ENDIF.
ms_data-status = c_status_cancelled. ENDMETHOD.
METHOD calculate_total. ms_data-total_amount = REDUCE #( INIT sum = CONV decfloat34( 0 ) FOR line IN mt_lines NEXT sum = sum + line-amount ). ENDMETHOD.
METHOD get_data. rs_data = ms_data. ENDMETHOD.
METHOD get_lines. rt_lines = mt_lines. ENDMETHOD.
METHOD is_editable. rv_editable = xsdbool( ms_data-status = c_status_draft ). ENDMETHOD.
ENDCLASS.Domain Service
Fuer Logik, die nicht zu einer einzelnen Entity gehoert:
" Domain Service: Price CalculatorCLASS zcl_price_calculator DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. TYPES: BEGIN OF ty_price_result, net_amount TYPE decfloat34, tax_amount TYPE decfloat34, gross_amount TYPE decfloat34, tax_rate TYPE decfloat34, END OF ty_price_result.
METHODS calculate IMPORTING iv_base_price TYPE decfloat34 iv_quantity TYPE i iv_country TYPE land1 iv_product_type TYPE string RETURNING VALUE(rs_result) TYPE ty_price_result.
PRIVATE SECTION. METHODS get_tax_rate IMPORTING iv_country TYPE land1 iv_product_type TYPE string RETURNING VALUE(rv_rate) TYPE decfloat34.ENDCLASS.
CLASS zcl_price_calculator IMPLEMENTATION.
METHOD calculate. DATA(lv_tax_rate) = get_tax_rate( iv_country = iv_country iv_product_type = iv_product_type ).
rs_result-net_amount = iv_base_price * iv_quantity. rs_result-tax_rate = lv_tax_rate. rs_result-tax_amount = rs_result-net_amount * lv_tax_rate. rs_result-gross_amount = rs_result-net_amount + rs_result-tax_amount. ENDMETHOD.
METHOD get_tax_rate. " Pure business logic - no external dependencies rv_rate = SWITCH #( iv_country WHEN 'DE' THEN COND #( WHEN iv_product_type = 'FOOD' THEN '0.07' ELSE '0.19' ) WHEN 'AT' THEN '0.20' WHEN 'CH' THEN '0.081' ELSE '0.19' ). ENDMETHOD.
ENDCLASS.Application Layer: Use Cases
Der Application Layer orchestriert die Domain-Logik und koordiniert den Datenfluss.
Inbound Port (Interface)
" Inbound Port: Order Service InterfaceINTERFACE zif_order_service PUBLIC.
TYPES: BEGIN OF ty_create_order_request, customer_id TYPE sysuuid_x16, currency TYPE waers, END OF ty_create_order_request,
BEGIN OF ty_add_line_request, order_id TYPE sysuuid_x16, product_id TYPE sysuuid_x16, quantity TYPE i, END OF ty_add_line_request,
BEGIN OF ty_order_response, order_id TYPE sysuuid_x16, status TYPE string, total_amount TYPE decfloat34, currency TYPE waers, END OF ty_order_response.
METHODS create_order IMPORTING is_request TYPE ty_create_order_request RETURNING VALUE(rs_response) TYPE ty_order_response RAISING zcx_order_error.
METHODS add_order_line IMPORTING is_request TYPE ty_add_line_request RAISING zcx_order_error.
METHODS submit_order IMPORTING iv_order_id TYPE sysuuid_x16 RAISING zcx_order_error.
METHODS cancel_order IMPORTING iv_order_id TYPE sysuuid_x16 iv_reason TYPE string RAISING zcx_order_error.
ENDINTERFACE.Use Case Implementation
" Application Service implementing the Use CasesCLASS zcl_order_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES zif_order_service.
METHODS constructor IMPORTING io_order_repository TYPE REF TO zif_order_repository io_product_repository TYPE REF TO zif_product_repository io_price_calculator TYPE REF TO zcl_price_calculator.
PRIVATE SECTION. DATA mo_order_repository TYPE REF TO zif_order_repository. DATA mo_product_repository TYPE REF TO zif_product_repository. DATA mo_price_calculator TYPE REF TO zcl_price_calculator.ENDCLASS.
CLASS zcl_order_service IMPLEMENTATION.
METHOD constructor. mo_order_repository = io_order_repository. mo_product_repository = io_product_repository. mo_price_calculator = io_price_calculator. ENDMETHOD.
METHOD zif_order_service~create_order. " Create new order using domain logic DATA(lo_order) = zcl_order=>create( iv_customer_id = is_request-customer_id iv_currency = is_request-currency ).
" Persist via repository mo_order_repository->save( lo_order ).
" Return response DATA(ls_data) = lo_order->get_data( ). rs_response = VALUE #( order_id = ls_data-order_id status = ls_data-status total_amount = ls_data-total_amount currency = ls_data-currency ). ENDMETHOD.
METHOD zif_order_service~add_order_line. " Load order from repository DATA(lo_order) = mo_order_repository->find_by_id( is_request-order_id ). IF lo_order IS NOT BOUND. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e010(zorder) WITH 'Order not found'. ENDIF.
" Get product price from repository DATA(lo_product) = mo_product_repository->find_by_id( is_request-product_id ). IF lo_product IS NOT BOUND. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e011(zorder) WITH 'Product not found'. ENDIF.
" Add line using domain logic lo_order->add_line( iv_product_id = is_request-product_id iv_quantity = is_request-quantity iv_unit_price = lo_product->get_price( ) ).
" Persist changes mo_order_repository->save( lo_order ). ENDMETHOD.
METHOD zif_order_service~submit_order. DATA(lo_order) = mo_order_repository->find_by_id( iv_order_id ). IF lo_order IS NOT BOUND. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e010(zorder) WITH 'Order not found'. ENDIF.
lo_order->submit( ). mo_order_repository->save( lo_order ). ENDMETHOD.
METHOD zif_order_service~cancel_order. DATA(lo_order) = mo_order_repository->find_by_id( iv_order_id ). IF lo_order IS NOT BOUND. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e010(zorder) WITH 'Order not found'. ENDIF.
lo_order->cancel( iv_reason = iv_reason ). mo_order_repository->save( lo_order ). ENDMETHOD.
ENDCLASS.Adapter fuer RAP (Inbound Port)
Der RAP Behavior Handler fungiert als Driving Adapter, der Anfragen entgegennimmt und an den Application Layer weiterleitet.
RAP als Inbound Adapter
" RAP Behavior Handler as Inbound AdapterCLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION. DATA mo_order_service TYPE REF TO zif_order_service.
METHODS get_order_service RETURNING VALUE(ro_service) TYPE REF TO zif_order_service.
METHODS create FOR MODIFY IMPORTING entities FOR CREATE order.
METHODS submit FOR MODIFY IMPORTING keys FOR ACTION order~submit.
METHODS cancel FOR MODIFY IMPORTING keys FOR ACTION order~cancel.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD get_order_service. IF mo_order_service IS NOT BOUND. " Dependency Injection via Factory mo_order_service = zcl_service_factory=>get_order_service( ). ENDIF. ro_service = mo_order_service. ENDMETHOD.
METHOD create. DATA(lo_service) = get_order_service( ).
LOOP AT entities ASSIGNING FIELD-SYMBOL(<entity>). TRY. DATA(ls_response) = lo_service->create_order( VALUE #( customer_id = <entity>-CustomerID currency = <entity>-Currency ) ).
" Map response to RAP result APPEND VALUE #( %cid = <entity>-%cid %key = VALUE #( OrderID = ls_response-order_id ) OrderID = ls_response-order_id ) TO mapped-order.
CATCH zcx_order_error INTO DATA(lx_error). APPEND VALUE #( %cid = <entity>-%cid %msg = lx_error %element-CustomerID = if_abap_behv=>mk-on ) TO reported-order.
APPEND VALUE #( %cid = <entity>-%cid ) TO failed-order. ENDTRY. ENDLOOP. ENDMETHOD.
METHOD submit. DATA(lo_service) = get_order_service( ).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>). TRY. lo_service->submit_order( iv_order_id = <key>-OrderID ).
APPEND VALUE #( %tky = <key>-%tky ) TO result.
CATCH zcx_order_error INTO DATA(lx_error). APPEND VALUE #( %tky = <key>-%tky %msg = lx_error ) TO reported-order.
APPEND VALUE #( %tky = <key>-%tky ) TO failed-order. ENDTRY. ENDLOOP. ENDMETHOD.
METHOD cancel. DATA(lo_service) = get_order_service( ).
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>). TRY. lo_service->cancel_order( iv_order_id = <key>-OrderID iv_reason = <key>-%param-Reason ).
APPEND VALUE #( %tky = <key>-%tky ) TO result.
CATCH zcx_order_error INTO DATA(lx_error). APPEND VALUE #( %tky = <key>-%tky %msg = lx_error ) TO reported-order.
APPEND VALUE #( %tky = <key>-%tky ) TO failed-order. ENDTRY. ENDLOOP. ENDMETHOD.
ENDCLASS.Service Factory fuer Dependency Injection
" Factory for Service Creation with Dependency InjectionCLASS zcl_service_factory DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION. CLASS-METHODS get_order_service RETURNING VALUE(ro_service) TYPE REF TO zif_order_service.
" For testing: inject mock implementations CLASS-METHODS set_order_service IMPORTING io_service TYPE REF TO zif_order_service.
CLASS-METHODS reset.
PRIVATE SECTION. CLASS-DATA go_order_service TYPE REF TO zif_order_service.ENDCLASS.
CLASS zcl_service_factory IMPLEMENTATION.
METHOD get_order_service. IF go_order_service IS BOUND. ro_service = go_order_service. RETURN. ENDIF.
" Create real implementations DATA(lo_order_repo) = NEW zcl_order_repository_db( ). DATA(lo_product_repo) = NEW zcl_product_repository_db( ). DATA(lo_price_calc) = NEW zcl_price_calculator( ).
ro_service = NEW zcl_order_service( io_order_repository = lo_order_repo io_product_repository = lo_product_repo io_price_calculator = lo_price_calc ). ENDMETHOD.
METHOD set_order_service. go_order_service = io_service. ENDMETHOD.
METHOD reset. CLEAR go_order_service. ENDMETHOD.
ENDCLASS.Adapter fuer externe Services (Outbound Port)
Outbound Port Definition
" Outbound Port: Order Repository InterfaceINTERFACE zif_order_repository PUBLIC.
METHODS find_by_id IMPORTING iv_order_id TYPE sysuuid_x16 RETURNING VALUE(ro_order) TYPE REF TO zcl_order.
METHODS find_by_customer IMPORTING iv_customer_id TYPE sysuuid_x16 RETURNING VALUE(rt_orders) TYPE zcl_order=>ty_order_list.
METHODS save IMPORTING io_order TYPE REF TO zcl_order RAISING zcx_persistence_error.
METHODS delete IMPORTING iv_order_id TYPE sysuuid_x16 RAISING zcx_persistence_error.
ENDINTERFACE.Database Adapter Implementation
" Driven Adapter: Database RepositoryCLASS zcl_order_repository_db DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES zif_order_repository.ENDCLASS.
CLASS zcl_order_repository_db IMPLEMENTATION.
METHOD zif_order_repository~find_by_id. SELECT SINGLE * FROM zorder WHERE order_id = @iv_order_id INTO @DATA(ls_order).
IF sy-subrc <> 0. RETURN. ENDIF.
SELECT * FROM zorder_item WHERE order_id = @iv_order_id INTO TABLE @DATA(lt_lines).
" Map DB records to Domain Entity ro_order = zcl_order=>reconstitute( is_data = CORRESPONDING #( ls_order ) it_lines = CORRESPONDING #( lt_lines ) ). ENDMETHOD.
METHOD zif_order_repository~save. DATA(ls_data) = io_order->get_data( ). DATA(lt_lines) = io_order->get_lines( ).
" Map Domain Entity to DB records and persist MODIFY zorder FROM @( CORRESPONDING zorder( ls_data ) ). DELETE FROM zorder_item WHERE order_id = @ls_data-order_id. INSERT zorder_item FROM TABLE @( CORRESPONDING #( lt_lines ) ). ENDMETHOD.
ENDCLASS.HTTP Adapter fuer externe APIs
" Driven Adapter: External Customer Service via HTTPCLASS zcl_customer_service_http DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES zif_customer_service.ENDCLASS.
CLASS zcl_customer_service_http IMPLEMENTATION.
METHOD zif_customer_service~get_customer. " HTTP Client via Communication Arrangement DATA(lo_dest) = cl_http_destination_provider=>create_by_comm_arrangement( comm_scenario = 'ZCUSTOMER_API' service_id = 'ZCUSTOMER_REST' ). DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( lo_dest ).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
/ui2/cl_json=>deserialize( EXPORTING json = lo_response->get_text( ) CHANGING data = rs_customer ). ENDMETHOD.
ENDCLASS.Vergleich mit traditioneller ABAP-Architektur
| Aspekt | Traditionelles ABAP | Hexagonale Architektur |
|---|---|---|
| Datenbankzugriff | SELECT direkt in Geschaeftslogik | Ueber Repository Interface |
| Externe Services | HTTP-Aufrufe in Geschaeftslogik | Ueber Service Interface |
| Testbarkeit | Schwierig (DB-Abhaengigkeit) | Einfach (Mock-Repositories) |
| Framework-Kopplung | Stark (RAP direkt verwendet) | Lose (RAP nur als Adapter) |
| Aenderbarkeit | Aenderungen betreffen viele Stellen | Aenderungen lokal begrenzt |
| Wiederverwendbarkeit | Gering | Hoch (Domain kann wiederverwendet werden) |
Traditionelles ABAP (Anti-Pattern)
" Alles vermischt - schwer testbarMETHOD create_order. " Direkter DB-Zugriff in Geschaeftslogik SELECT SINGLE * FROM zcustomer WHERE customer_id = @iv_customer_id INTO @DATA(ls_customer).
IF ls_customer-status <> 'ACTIVE'. " Direkte Message-Behandlung MESSAGE e001(zorder). RETURN. ENDIF.
" HTTP-Aufruf direkt in Methode DATA(lo_client) = cl_http_client=>create_by_destination( 'PRICING_API' ). " ... HTTP Logik ...
" DB Insert direkt INSERT INTO zorder VALUES @ls_order.ENDMETHOD.Hexagonale Architektur (Clean)
" Sauber getrennt - leicht testbarMETHOD create_order. " Validierung ueber Service Interface IF NOT mo_customer_service->validate_customer( iv_customer_id ). RAISE EXCEPTION TYPE zcx_order_error. ENDIF.
" Domain-Logik DATA(lo_order) = zcl_order=>create( iv_customer_id = iv_customer_id iv_currency = iv_currency ).
" Persistenz ueber Repository Interface mo_order_repository->save( lo_order ).ENDMETHOD.Testbarkeit durch Mock-Repositories
" Mock Repository fuer Unit TestsCLASS lcl_mock_order_repository DEFINITION FOR TESTING. PUBLIC SECTION. INTERFACES zif_order_repository. DATA mt_saved_orders TYPE TABLE OF REF TO zcl_order. DATA mo_return_order TYPE REF TO zcl_order.ENDCLASS.
CLASS lcl_mock_order_repository IMPLEMENTATION. METHOD zif_order_repository~find_by_id. ro_order = mo_return_order. ENDMETHOD. METHOD zif_order_repository~save. APPEND io_order TO mt_saved_orders. ENDMETHOD.ENDCLASS.
" Unit Test - Domain-Logik ohne DB testbarCLASS ltcl_order_service DEFINITION FOR TESTING DURATION SHORT. PRIVATE SECTION. METHODS test_create_order FOR TESTING.ENDCLASS.
CLASS ltcl_order_service IMPLEMENTATION. METHOD test_create_order. DATA(lo_mock_repo) = NEW lcl_mock_order_repository( ).
DATA(lo_service) = NEW zcl_order_service( io_order_repository = lo_mock_repo io_price_calculator = NEW zcl_price_calculator( ) ).
DATA(ls_response) = lo_service->zif_order_service~create_order( VALUE #( customer_id = cl_system_uuid=>create_uuid_x16_static( ) currency = 'EUR' ) ).
" Assertions cl_abap_unit_assert=>assert_not_initial( ls_response-order_id ). cl_abap_unit_assert=>assert_equals( act = lines( lo_mock_repo->mt_saved_orders ) exp = 1 ). ENDMETHOD.ENDCLASS.Zusammenfassung
| Layer | Verantwortlichkeit | ABAP Cloud Artefakte |
|---|---|---|
| Domain | Reine Geschaeftslogik | Klassen ohne Abhaengigkeiten |
| Application | Use Case Orchestrierung | Service-Klassen mit Interfaces |
| Inbound Adapter | Eingehende Anfragen | RAP Handler, HTTP Handler |
| Outbound Adapter | Externe Systeme | Repository, HTTP Client |
| Ports | Abstraktion | Interfaces |
Die Hexagonale Architektur erfordert mehr initialen Aufwand, zahlt sich aber bei komplexen, langlebigen Anwendungen durch bessere Testbarkeit, Wartbarkeit und Flexibilitaet aus.
Verwandte Themen
- Domain-Driven Design mit RAP - DDD-Konzepte in ABAP Cloud
- Design Patterns fuer RAP - Bewaehrte Architekturmuster
- Clean ABAP - Sauberer, wartbarer ABAP-Code
- Test Doubles und Mocking - Testbare Anwendungen entwickeln
- RAP Basics - Grundlagen des RESTful ABAP Programming