Hexagonale Architektur in ABAP Cloud: Ports & Adapters Pattern

kategorie
Best Practices
Veröffentlicht
autor
Johannes

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

KonzeptBeschreibungABAP Cloud Mapping
Domain CoreReine Geschaeftslogik ohne AbhaengigkeitenABAP Klassen mit Interfaces
Inbound PortSchnittstelle fuer eingehende AnfragenInterface fuer Use Cases
Outbound PortSchnittstelle fuer ausgehende AufrufeInterface fuer Repositories/Services
Driving AdapterRuft die Anwendung aufRAP Behavior Handler, HTTP Handler
Driven AdapterWird von der Anwendung aufgerufenRepository Implementation, HTTP Client

Warum Hexagonale Architektur in ABAP Cloud?

Vorteile

  1. Testbarkeit: Domain-Logik kann ohne Datenbank oder externe Services getestet werden
  2. Austauschbarkeit: Adapter koennen ohne Aenderung der Geschaeftslogik ersetzt werden
  3. Unabhaengigkeit: Die Domain kennt keine technischen Details (RAP, HTTP, DB)
  4. Wartbarkeit: Klare Grenzen erleichtern Aenderungen
  5. Framework-Unabhaengigkeit: Migration auf neue Technologien wird einfacher

Wann lohnt sich der Aufwand?

SzenarioEmpfehlung
Komplexe Geschaeftslogik mit vielen RegelnHexagonale Architektur
Einfache CRUD-OperationenStandard RAP (Managed)
Integration mehrerer externer ServicesHexagonale Architektur
Prototyp oder MVPStandard RAP
Langlebige Enterprise-AnwendungHexagonale Architektur
Hohe Testabdeckung erforderlichHexagonale 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 Services
CLASS 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 Calculator
CLASS 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 Interface
INTERFACE 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 Cases
CLASS 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 Adapter
CLASS 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 Injection
CLASS 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 Interface
INTERFACE 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 Repository
CLASS 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 HTTP
CLASS 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

AspektTraditionelles ABAPHexagonale Architektur
DatenbankzugriffSELECT direkt in GeschaeftslogikUeber Repository Interface
Externe ServicesHTTP-Aufrufe in GeschaeftslogikUeber Service Interface
TestbarkeitSchwierig (DB-Abhaengigkeit)Einfach (Mock-Repositories)
Framework-KopplungStark (RAP direkt verwendet)Lose (RAP nur als Adapter)
AenderbarkeitAenderungen betreffen viele StellenAenderungen lokal begrenzt
WiederverwendbarkeitGeringHoch (Domain kann wiederverwendet werden)

Traditionelles ABAP (Anti-Pattern)

" Alles vermischt - schwer testbar
METHOD 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 testbar
METHOD 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 Tests
CLASS 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 testbar
CLASS 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

LayerVerantwortlichkeitABAP Cloud Artefakte
DomainReine GeschaeftslogikKlassen ohne Abhaengigkeiten
ApplicationUse Case OrchestrierungService-Klassen mit Interfaces
Inbound AdapterEingehende AnfragenRAP Handler, HTTP Handler
Outbound AdapterExterne SystemeRepository, HTTP Client
PortsAbstraktionInterfaces

Die Hexagonale Architektur erfordert mehr initialen Aufwand, zahlt sich aber bei komplexen, langlebigen Anwendungen durch bessere Testbarkeit, Wartbarkeit und Flexibilitaet aus.

Verwandte Themen