Domain-Driven Design mit RAP: Fachdomaenen modellieren

kategorie
RAP
Veröffentlicht
autor
Johannes

Domain-Driven Design (DDD) ist ein Ansatz zur Softwareentwicklung, der die Fachdomaene ins Zentrum stellt. RAP (RESTful ABAP Programming) bietet ideale Voraussetzungen, um DDD-Prinzipien umzusetzen: Business Objects, Entities und Compositions spiegeln direkt die DDD-Konzepte wider.

Warum DDD mit RAP?

DDD-KonzeptRAP-UmsetzungVorteil
Bounded ContextSoftware Component / PackageKlare Abgrenzung von Fachbereichen
AggregateBusiness Object (Root Entity)Konsistenzgrenzen definiert
EntityCDS View EntityIdentitaet und Lebenszyklus
Value ObjectCDS Struktur / Embedded ViewUnveraenderliche Wertobjekte
Domain EventRAP Business EventLose Kopplung zwischen Kontexten
RepositoryRAP Runtime (EML)Abstraktion der Persistenz

DDD Grundkonzepte

Bounded Context

Ein Bounded Context ist eine logische Grenze, innerhalb derer ein bestimmtes Domänenmodell gilt. In ABAP Cloud entspricht dies typischerweise einer Software Component oder einem Package-Baum.

┌─────────────────────────────────────────────────────────────┐
│ ENTERPRISE │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ SALES CONTEXT │ │ LOGISTICS CONTEXT │ │
│ │ ┌───────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ Order (BO) │ │ │ │ Shipment (BO) │ │ │
│ │ │ Customer │──┼────┼──│ DeliveryAddress │ │ │
│ │ │ OrderItem │ │ │ │ ShipmentItem │ │ │
│ │ └───────────────┘ │ │ └───────────────────────┘ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ Product (ref) │◄─┼────┼──│ Inventory (BO) │ │ │
│ │ └───────────────┘ │ │ │ StockLevel │ │ │
│ └─────────────────────┘ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Aggregate und Root Entity

Ein Aggregate ist eine Gruppe von zusammengehoerenden Objekten mit einer Root Entity. In RAP entspricht die Root Entity dem Business Object mit define root view entity:

" ROOT ENTITY = Aggregate Root
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Order - Root Entity (Aggregate)'
define root view entity ZI_Order
as select from zorder
composition [0..*] of ZI_OrderItem as _Items
association [0..1] to ZI_Customer as _Customer
on $projection.CustomerId = _Customer.CustomerId
{
key order_id as OrderId,
customer_id as CustomerId,
order_date as OrderDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
total_amount as TotalAmount,
@Semantics.currencyCode: true
currency_code as CurrencyCode,
status as Status,
" Administrative Felder
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
" Assoziationen
_Items,
_Customer
}

Child Entity (Teil des Aggregates)

Entities innerhalb eines Aggregates werden nur ueber die Root Entity erreicht:

" CHILD ENTITY - Teil des Order Aggregates
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Order Item - Child Entity'
define view entity ZI_OrderItem
as select from zorder_item
association to parent ZI_Order as _Order
on $projection.OrderId = _Order.OrderId
association [1..1] to ZI_Product as _Product
on $projection.ProductId = _Product.ProductId
{
key order_id as OrderId,
key item_id as ItemId,
product_id as ProductId,
quantity as Quantity,
@Semantics.amount.currencyCode: 'CurrencyCode'
unit_price as UnitPrice,
@Semantics.currencyCode: true
currency_code as CurrencyCode,
" Berechnetes Feld
quantity * unit_price as LineTotal,
_Order,
_Product
}

Value Object

Value Objects haben keine eigene Identitaet und werden durch ihre Attribute definiert. In RAP werden sie als eingebettete Strukturen oder berechnete Felder modelliert:

" Value Object als CDS Structure
@EndUserText.label: 'Address Value Object'
define structure zs_address {
street : abap.char(60);
house_number: abap.char(10);
postal_code : abap.char(10);
city : abap.char(40);
country : land1;
}
" Verwendung in Entity
define root view entity ZI_Customer
as select from zcustomer
{
key customer_id as CustomerId,
customer_name as CustomerName,
" Value Object: Adresse
street as AddressStreet,
house_number as AddressHouseNumber,
postal_code as AddressPostalCode,
city as AddressCity,
country as AddressCountry
}

Aggregate-Regeln in RAP umsetzen

Regel 1: Zugriff nur ueber Aggregate Root

Die Behavior Definition erzwingt, dass Child Entities nur ueber die Root Entity manipuliert werden:

managed implementation in class zbp_i_order unique;
strict ( 2 );
define behavior for ZI_Order alias Order
persistent table zorder
lock master
authorization master ( instance )
etag master LastChangedAt
{
create;
update;
delete;
" Items werden ueber Order erstellt/geaendert
association _Items { create; }
" Aggregate-Invarianten sicherstellen
validation validateOrderConsistency on save { create; update; }
" Domain Events
event OrderCreated;
event OrderConfirmed;
}
define behavior for ZI_OrderItem alias OrderItem
persistent table zorder_item
lock dependent by _Order
authorization dependent by _Order
{
update;
delete;
" Keine direkte Erstellung - nur ueber Order
field ( readonly ) OrderId;
association _Order;
}

Regel 2: Invarianten schuetzen

Aggregate-Invarianten werden in Validations geprueft:

CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS validateOrderConsistency FOR VALIDATE ON SAVE
IMPORTING keys FOR Order~validateOrderConsistency.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD validateOrderConsistency.
" Aggregate-Invarianten pruefen
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Items laden
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order BY \_Items
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_items).
LOOP AT lt_orders INTO DATA(ls_order).
" Invariante 1: Mindestens ein Item erforderlich
DATA(lt_order_items) = FILTER #( lt_items
WHERE OrderId = ls_order-OrderId ).
IF lines( lt_order_items ) = 0.
APPEND VALUE #(
%tky = ls_order-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Bestellung muss mindestens eine Position enthalten'
)
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
CONTINUE.
ENDIF.
" Invariante 2: Gesamtbetrag muss mit Items uebereinstimmen
DATA(lv_calculated_total) = REDUCE decfloat34(
INIT sum = CONV decfloat34( 0 )
FOR item IN lt_order_items
NEXT sum = sum + ( item-Quantity * item-UnitPrice )
).
IF ls_order-TotalAmount <> lv_calculated_total.
APPEND VALUE #(
%tky = ls_order-%tky
%element-TotalAmount = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Gesamtbetrag stimmt nicht mit Positionen ueberein'
)
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Regel 3: Transaktionale Konsistenz

Ein Aggregate wird immer als Ganzes gespeichert:

METHOD create_order_with_items.
" Deep Insert - Aggregate als Ganzes erstellen
MODIFY ENTITIES OF zi_order
ENTITY Order
CREATE FIELDS ( CustomerId OrderDate TotalAmount CurrencyCode Status )
WITH VALUE #( (
%cid = 'ORDER_1'
CustomerId = iv_customer_id
OrderDate = cl_abap_context_info=>get_system_date( )
TotalAmount = lv_total
CurrencyCode = 'EUR'
Status = 'NEW'
) )
CREATE BY \_Items
FIELDS ( ProductId Quantity UnitPrice CurrencyCode )
WITH VALUE #(
( %cid_ref = 'ORDER_1'
%target = VALUE #(
( %cid = 'ITEM_1'
ProductId = 'PROD001'
Quantity = 2
UnitPrice = '100.00'
CurrencyCode = 'EUR' )
( %cid = 'ITEM_2'
ProductId = 'PROD002'
Quantity = 1
UnitPrice = '250.00'
CurrencyCode = 'EUR' )
)
)
)
MAPPED DATA(lt_mapped)
FAILED DATA(lt_failed)
REPORTED DATA(lt_reported).
" Alles oder nichts - transaktional
IF lt_failed IS NOT INITIAL.
" Rollback automatisch durch RAP Runtime
RAISE EXCEPTION TYPE cx_order_creation_failed.
ENDIF.
COMMIT ENTITIES.
ENDMETHOD.

Domain Services implementieren

Domain Services kapseln Geschaeftslogik, die nicht zu einer einzelnen Entity gehoert:

" Interface fuer Domain Service
INTERFACE zif_pricing_service.
METHODS calculate_order_total
IMPORTING
it_items TYPE ztt_order_items
iv_customer_tier TYPE zdd_customer_tier
RETURNING
VALUE(rs_result) TYPE zs_pricing_result.
METHODS apply_discount
IMPORTING
iv_subtotal TYPE decfloat34
iv_discount_code TYPE zdd_discount_code
RETURNING
VALUE(rv_total) TYPE decfloat34.
ENDINTERFACE.
" Implementierung
CLASS zcl_pricing_service DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_pricing_service.
PRIVATE SECTION.
METHODS get_customer_discount
IMPORTING iv_tier TYPE zdd_customer_tier
RETURNING VALUE(rv_discount) TYPE decfloat34.
ENDCLASS.
CLASS zcl_pricing_service IMPLEMENTATION.
METHOD zif_pricing_service~calculate_order_total.
" Zwischensumme berechnen
DATA(lv_subtotal) = REDUCE decfloat34(
INIT sum = CONV decfloat34( 0 )
FOR item IN it_items
NEXT sum = sum + ( item-quantity * item-unit_price )
).
" Kundenrabatt anwenden
DATA(lv_discount) = get_customer_discount( iv_customer_tier ).
DATA(lv_discount_amount) = lv_subtotal * lv_discount / 100.
rs_result-subtotal = lv_subtotal.
rs_result-discount_percentage = lv_discount.
rs_result-discount_amount = lv_discount_amount.
rs_result-total = lv_subtotal - lv_discount_amount.
ENDMETHOD.
METHOD zif_pricing_service~apply_discount.
" Rabattcode validieren und anwenden
SELECT SINGLE discount_percentage
FROM zdiscount_codes
WHERE code = @iv_discount_code
AND valid_from <= @cl_abap_context_info=>get_system_date( )
AND valid_to >= @cl_abap_context_info=>get_system_date( )
INTO @DATA(lv_percentage).
IF sy-subrc = 0.
rv_total = iv_subtotal * ( 1 - lv_percentage / 100 ).
ELSE.
rv_total = iv_subtotal.
ENDIF.
ENDMETHOD.
METHOD get_customer_discount.
CASE iv_tier.
WHEN 'GOLD'.
rv_discount = 15.
WHEN 'SILVER'.
rv_discount = 10.
WHEN 'BRONZE'.
rv_discount = 5.
WHEN OTHERS.
rv_discount = 0.
ENDCASE.
ENDMETHOD.
ENDCLASS.

Domain Service in RAP Action verwenden

CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS recalculateTotal FOR MODIFY
IMPORTING keys FOR ACTION Order~recalculateTotal
RESULT result.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD recalculateTotal.
" Order und Items laden
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order BY \_Items
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_items).
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order BY \_Customer
FIELDS ( CustomerTier )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_customers).
" Domain Service instanziieren
DATA(lo_pricing) = NEW zcl_pricing_service( ).
LOOP AT lt_orders INTO DATA(ls_order).
" Items und Kundendaten holen
DATA(lt_order_items) = FILTER #( lt_items
WHERE OrderId = ls_order-OrderId ).
DATA(ls_customer) = VALUE #( lt_customers[
CustomerId = ls_order-CustomerId ] OPTIONAL ).
" Domain Service aufrufen
DATA(ls_pricing) = lo_pricing->zif_pricing_service~calculate_order_total(
it_items = CORRESPONDING #( lt_order_items )
iv_customer_tier = ls_customer-CustomerTier ).
" Aggregate aktualisieren
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( TotalAmount DiscountAmount )
WITH VALUE #( (
%tky = ls_order-%tky
TotalAmount = ls_pricing-total
DiscountAmount = ls_pricing-discount_amount
) ).
ENDLOOP.
" Ergebnis zurueckgeben
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT result.
ENDMETHOD.
ENDCLASS.

Domain Events in RAP

Domain Events signalisieren wichtige Geschaeftsereignisse und ermoeglichen lose Kopplung zwischen Bounded Contexts:

Events definieren

" Behavior Definition mit Events
managed implementation in class zbp_i_order unique;
define behavior for ZI_Order alias Order
{
" ... Standard-Operationen ...
" Domain Events deklarieren
event OrderCreated parameter zs_order_created_event;
event OrderConfirmed parameter zs_order_confirmed_event;
event OrderShipped parameter zs_order_shipped_event;
event OrderCancelled;
}

Event-Parameter als Struktur

" Event-Payload Definition
@EndUserText.label: 'Order Created Event'
define abstract entity ZA_OrderCreatedEvent
{
OrderId : abap.numc(10);
CustomerId : abap.numc(10);
OrderDate : abap.dats;
TotalAmount : abap.dec(15,2);
CurrencyCode : abap.cuky;
ItemCount : abap.int4;
}

Events ausloesen

CLASS lhc_order IMPLEMENTATION.
METHOD confirmOrder.
" Status aendern
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( Status ConfirmedAt )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky
Status = 'CONFIRMED'
ConfirmedAt = utclong_current( ) ) )
FAILED failed
REPORTED reported.
" Aktuelle Daten lesen
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Domain Event ausloesen
LOOP AT lt_orders INTO DATA(ls_order).
RAISE ENTITY EVENT zi_order~OrderConfirmed
FROM VALUE #( (
%key = ls_order-%key
%param = VALUE zs_order_confirmed_event(
order_id = ls_order-OrderId
customer_id = ls_order-CustomerId
confirmed_at = ls_order-ConfirmedAt
total_amount = ls_order-TotalAmount
currency_code = ls_order-CurrencyCode
)
) ).
ENDLOOP.
" Ergebnis
result = CORRESPONDING #( lt_orders ).
ENDMETHOD.
ENDCLASS.

Event Handler in anderem Bounded Context

CLASS zcl_logistics_event_handler DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_event_handler.
ENDCLASS.
CLASS zcl_logistics_event_handler IMPLEMENTATION.
METHOD if_rap_event_handler~handle_event.
CASE iv_event_name.
WHEN 'ORDERCONFIRMED'.
" Event-Daten auslesen
DATA ls_event TYPE zs_order_confirmed_event.
ls_event = CORRESPONDING #( is_event_data ).
" Shipment im Logistics Context erstellen
MODIFY ENTITIES OF zi_shipment
ENTITY Shipment
CREATE FIELDS ( OrderId CustomerId Status )
WITH VALUE #( (
%cid = 'SHIP_1'
OrderId = ls_event-order_id
CustomerId = ls_event-customer_id
Status = 'PENDING'
) )
MAPPED DATA(lt_mapped)
FAILED DATA(lt_failed)
REPORTED DATA(lt_reported).
IF lt_failed IS INITIAL.
COMMIT ENTITIES.
ENDIF.
ENDCASE.
ENDMETHOD.
ENDCLASS.

Ubiquitous Language

Ein zentrales DDD-Konzept ist die Ubiquitous Language - ein gemeinsames Vokabular fuer Fachbereich und Entwicklung:

CDS Naming Conventions

" Entity-Namen entsprechen Fachbegriffen
define root view entity ZI_SalesOrder " Kundenauftrag
define view entity ZI_SalesOrderItem " Auftragsposition
define view entity ZI_DeliveryNote " Lieferschein
define view entity ZI_Invoice " Rechnung
define root view entity ZI_Customer " Kunde
" Feldnamen sind selbsterklaerend
@EndUserText.label: 'Kundenauftrag'
define root view entity ZI_SalesOrder
{
key order_number as OrderNumber, " Auftragsnummer
customer_number as CustomerNumber, " Kundennummer
order_date as OrderDate, " Auftragsdatum
requested_delivery as RequestedDelivery, " Wunschliefertermin
net_value as NetValue, " Nettowert
tax_amount as TaxAmount, " Steuerbetrag
gross_value as GrossValue, " Bruttowert
payment_terms as PaymentTerms, " Zahlungsbedingungen
shipping_method as ShippingMethod, " Versandart
order_status as OrderStatus " Auftragsstatus
}

Domain-spezifische Datentypen

" Eigene Datenelemente fuer Fachbegriffe
@EndUserText.label: 'Auftragsnummer'
@AbapCatalog.dataMaintenance: #ALLOWED
define type zdd_order_number : abap.numc(10);
@EndUserText.label: 'Kundennummer'
define type zdd_customer_number : abap.numc(10);
@EndUserText.label: 'Auftragsstatus'
define type zdd_order_status : abap.char(2);
" Verwendung in Tabelle und CDS
define table zsales_order {
key client : abap.clnt not null;
key order_number : zdd_order_number not null;
customer_number : zdd_customer_number;
order_status : zdd_order_status;
}

Bounded Context Integration

Anti-Corruption Layer

Ein Anti-Corruption Layer schuetzt den eigenen Bounded Context vor fremden Datenmodellen:

" Interface zum externen System
INTERFACE zif_external_product_service.
METHODS get_product
IMPORTING iv_external_id TYPE string
RETURNING VALUE(rs_product) TYPE zs_external_product.
ENDINTERFACE.
" Anti-Corruption Layer
CLASS zcl_product_acl DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING io_external_service TYPE REF TO zif_external_product_service.
METHODS get_product_for_order
IMPORTING iv_product_id TYPE zdd_product_id
RETURNING VALUE(rs_product) TYPE zs_order_product
RAISING cx_product_not_found.
PRIVATE SECTION.
DATA mo_external_service TYPE REF TO zif_external_product_service.
METHODS translate_product
IMPORTING is_external TYPE zs_external_product
RETURNING VALUE(rs_internal) TYPE zs_order_product.
ENDCLASS.
CLASS zcl_product_acl IMPLEMENTATION.
METHOD constructor.
mo_external_service = io_external_service.
ENDMETHOD.
METHOD get_product_for_order.
" Externes Produkt abrufen
DATA(ls_external) = mo_external_service->get_product(
CONV #( iv_product_id ) ).
IF ls_external IS INITIAL.
RAISE EXCEPTION TYPE cx_product_not_found.
ENDIF.
" In eigenes Domaenenmodell uebersetzen
rs_product = translate_product( ls_external ).
ENDMETHOD.
METHOD translate_product.
" Mapping auf eigene Struktur
rs_internal-product_id = is_external-sku.
rs_internal-name = is_external-title.
rs_internal-price = is_external-list_price.
rs_internal-currency = is_external-currency_iso.
" Externe Kategorien auf interne mappen
CASE is_external-category_code.
WHEN 'ELEC'.
rs_internal-product_group = 'ELECTRONICS'.
WHEN 'FURN'.
rs_internal-product_group = 'FURNITURE'.
WHEN OTHERS.
rs_internal-product_group = 'OTHER'.
ENDCASE.
ENDMETHOD.
ENDCLASS.

Context Map

Eine Context Map dokumentiert die Beziehungen zwischen Bounded Contexts:

┌─────────────────────────────────────────────────────────────────┐
│ CONTEXT MAP │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ Events ┌──────────────────┐ │
│ │ SALES │ ──────────────────────► │ LOGISTICS │ │
│ │ Context │ OrderConfirmed │ Context │ │
│ │ │ OrderCancelled │ │ │
│ │ [Upstream] │ │ [Downstream] │ │
│ └──────────────┘ └──────────────────┘ │
│ │ │ │
│ │ API Call │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ PRODUCT │ ◄────────────────────────────────┘ │
│ │ Context │ ACL (Read-Only) │
│ │ │ │
│ │ [Upstream] │ │
│ └──────────────┘ │
│ │ │
│ │ Conformist │
│ ▼ │
│ ┌──────────────┐ │
│ │ EXTERNAL │ │
│ │ Catalog │ │
│ │ (SAP S/4) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Best Practices fuer DDD mit RAP

1. Aggregate-Grenzen richtig setzen

" GUT: Order ist Aggregate Root mit Items als Children
define root view entity ZI_Order
composition [0..*] of ZI_OrderItem as _Items " Teil des Aggregates
" SCHLECHT: Zu grosses Aggregate
define root view entity ZI_Order
composition [0..*] of ZI_OrderItem as _Items
composition [0..*] of ZI_Invoice as _Invoices " Eigenes Aggregate!
composition [0..*] of ZI_Shipment as _Shipments " Eigenes Aggregate!

2. Konsistenz innerhalb des Aggregates

" Determination aktualisiert abgeleitete Werte im Aggregate
determination calculateTotals on modify {
field Quantity, UnitPrice; " Bei Aenderung von Items
}
METHOD calculateTotals.
" Alle Items summieren
DATA(lv_total) = REDUCE decfloat34(
INIT sum = CONV decfloat34( 0 )
FOR item IN lt_items
NEXT sum = sum + item-LineTotal
).
" Root Entity aktualisieren
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( TotalAmount )
WITH VALUE #( ( %tky = ls_order-%tky
TotalAmount = lv_total ) ).
ENDMETHOD.

3. Repositories abstrahieren

" Repository Interface fuer Aggregate
INTERFACE zif_order_repository.
METHODS find_by_id
IMPORTING iv_order_id TYPE zdd_order_id
RETURNING VALUE(rs_order) TYPE zs_order_aggregate
RAISING cx_not_found.
METHODS find_by_customer
IMPORTING iv_customer_id TYPE zdd_customer_id
RETURNING VALUE(rt_orders) TYPE ztt_order_aggregates.
METHODS save
IMPORTING is_order TYPE zs_order_aggregate
RAISING cx_save_failed.
ENDINTERFACE.
" RAP-basierte Implementierung
CLASS zcl_order_repository DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_order_repository.
ENDCLASS.
CLASS zcl_order_repository IMPLEMENTATION.
METHOD zif_order_repository~find_by_id.
READ ENTITIES OF zi_order
ENTITY Order
ALL FIELDS WITH VALUE #( ( OrderId = iv_order_id ) )
RESULT DATA(lt_orders)
FAILED DATA(lt_failed).
IF lt_failed IS NOT INITIAL OR lt_orders IS INITIAL.
RAISE EXCEPTION TYPE cx_not_found.
ENDIF.
" Items laden
READ ENTITIES OF zi_order
ENTITY Order BY \_Items
ALL FIELDS WITH VALUE #( ( OrderId = iv_order_id ) )
RESULT DATA(lt_items).
" Aggregate zusammensetzen
rs_order-header = CORRESPONDING #( lt_orders[ 1 ] ).
rs_order-items = CORRESPONDING #( lt_items ).
ENDMETHOD.
ENDCLASS.

4. Fachliche Methoden statt technischer

" GUT: Fachliche Action
action confirmOrder result [1] $self;
action shipOrder parameter ZA_ShipmentDetails result [1] $self;
action cancelOrder parameter ZA_CancellationReason result [1] $self;
" SCHLECHT: Technische Action
action setStatusConfirmed result [1] $self;
action updateShippingData parameter ZA_ShippingData result [1] $self;

Weiterführende Themen

Fazit

Domain-Driven Design und RAP ergaenzen sich hervorragend:

  • Aggregate = Business Object: Die RAP-Architektur mit Root und Child Entities entspricht direkt dem Aggregate-Konzept
  • Bounded Contexts = Software Components: Package-Strukturen definieren klare Grenzen
  • Domain Events = RAP Business Events: Lose Kopplung zwischen Kontexten
  • Repository = EML/RAP Runtime: Persistenz wird vom Framework abstrahiert

DDD erfordert anfangs mehr Analyse, zahlt sich aber durch bessere Wartbarkeit, klarere Kommunikation zwischen Fachbereich und Entwicklung sowie flexiblere Architektur aus. RAP bietet die technische Grundlage, um diese Konzepte elegant umzusetzen.