Domain-Driven Design avec RAP : Modeliser les domaines metier

Catégorie
RAP
Publié
Auteur
Johannes

Le Domain-Driven Design (DDD) est une approche de developpement logiciel qui place le domaine metier au centre. RAP (RESTful ABAP Programming) offre des conditions ideales pour mettre en oeuvre les principes DDD : les Business Objects, Entities et Compositions refletent directement les concepts DDD.

Pourquoi DDD avec RAP ?

Concept DDDImplementation RAPAvantage
Bounded ContextSoftware Component / PackageDelimitation claire des domaines metier
AggregateBusiness Object (Root Entity)Limites de coherence definies
EntityCDS View EntityIdentite et cycle de vie
Value ObjectStructure CDS / Embedded ViewObjets valeur immuables
Domain EventRAP Business EventCouplage lache entre contextes
RepositoryRAP Runtime (EML)Abstraction de la persistance

Concepts de base DDD

Bounded Context

Un Bounded Context est une frontiere logique a l’interieur de laquelle un modele de domaine specifique s’applique. Dans ABAP Cloud, cela correspond generalement a une Software Component ou une arborescence de packages.

┌─────────────────────────────────────────────────────────────┐
│ ENTREPRISE │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ CONTEXTE VENTES │ │ CONTEXTE LOGISTIQUE │ │
│ │ ┌───────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ Order (BO) │ │ │ │ Shipment (BO) │ │ │
│ │ │ Customer │──┼────┼──│ DeliveryAddress │ │ │
│ │ │ OrderItem │ │ │ │ ShipmentItem │ │ │
│ │ └───────────────┘ │ │ └───────────────────────┘ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ Product (ref) │◄─┼────┼──│ Inventory (BO) │ │ │
│ │ └───────────────┘ │ │ │ StockLevel │ │ │
│ └─────────────────────┘ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Aggregate et Root Entity

Un Aggregate est un groupe d’objets lies avec une Root Entity. Dans RAP, la Root Entity correspond au Business Object avec 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,
" Champs administratifs
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
" Associations
_Items,
_Customer
}

Child Entity (partie de l’Aggregate)

Les Entities a l’interieur d’un Aggregate ne sont accessibles que via la Root Entity :

" CHILD ENTITY - Partie de l'Aggregate Order
@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,
" Champ calcule
quantity * unit_price as LineTotal,
_Order,
_Product
}

Value Object

Les Value Objects n’ont pas d’identite propre et sont definis par leurs attributs. Dans RAP, ils sont modelises comme des structures imbriquees ou des champs calcules :

" Value Object comme Structure CDS
@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;
}
" Utilisation dans une 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
}

Implementer les regles d’Aggregate dans RAP

Regle 1 : Acces uniquement via Aggregate Root

La Behavior Definition garantit que les Child Entities ne sont manipulees que via la Root Entity :

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;
" Les Items sont crees/modifies via Order
association _Items { create; }
" Assurer les invariants de l'Aggregate
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;
" Pas de creation directe - uniquement via Order
field ( readonly ) OrderId;
association _Order;
}

Regle 2 : Proteger les invariants

Les invariants de l’Aggregate sont verifies dans les Validations :

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.
" Verifier les invariants de l'Aggregate
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Charger les Items
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).
" Invariant 1 : Au moins un Item requis
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 = 'La commande doit contenir au moins une position"
)
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
CONTINUE.
ENDIF.
" Invariant 2 : Le montant total doit correspondre aux Items
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 = 'Le montant total ne correspond pas aux positions"
)
) TO reported-order.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-order.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Regle 3 : Coherence transactionnelle

Un Aggregate est toujours sauvegarde comme un tout :

METHOD create_order_with_items.
" Deep Insert - Creer l'Aggregate en entier
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).
" Tout ou rien - transactionnel
IF lt_failed IS NOT INITIAL.
" Rollback automatique par RAP Runtime
RAISE EXCEPTION TYPE cx_order_creation_failed.
ENDIF.
COMMIT ENTITIES.
ENDMETHOD.

Implementer des Domain Services

Les Domain Services encapsulent la logique metier qui n’appartient pas a une seule Entity :

" Interface pour 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.
" Implementation
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.
" Calculer le sous-total
DATA(lv_subtotal) = REDUCE decfloat34(
INIT sum = CONV decfloat34( 0 )
FOR item IN it_items
NEXT sum = sum + ( item-quantity * item-unit_price )
).
" Appliquer la remise client
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.
" Valider et appliquer le code de remise
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.

Utiliser un Domain Service dans une RAP Action

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.
" Charger Order et Items
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).
" Instancier le Domain Service
DATA(lo_pricing) = NEW zcl_pricing_service( ).
LOOP AT lt_orders INTO DATA(ls_order).
" Recuperer les Items et les donnees client
DATA(lt_order_items) = FILTER #( lt_items
WHERE OrderId = ls_order-OrderId ).
DATA(ls_customer) = VALUE #( lt_customers[
CustomerId = ls_order-CustomerId ] OPTIONAL ).
" Appeler le Domain Service
DATA(ls_pricing) = lo_pricing->zif_pricing_service~calculate_order_total(
it_items = CORRESPONDING #( lt_order_items )
iv_customer_tier = ls_customer-CustomerTier ).
" Mettre a jour l'Aggregate
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.
" Retourner le resultat
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT result.
ENDMETHOD.
ENDCLASS.

Domain Events dans RAP

Les Domain Events signalent des evenements metier importants et permettent un couplage lache entre les Bounded Contexts :

Definir des Events

" Behavior Definition avec Events
managed implementation in class zbp_i_order unique;
define behavior for ZI_Order alias Order
{
" ... Operations standard ...
" Declarer les Domain Events
event OrderCreated parameter zs_order_created_event;
event OrderConfirmed parameter zs_order_confirmed_event;
event OrderShipped parameter zs_order_shipped_event;
event OrderCancelled;
}

Parametre d’Event comme structure

" Definition du payload de l'Event
@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;
}

Declencher des Events

CLASS lhc_order IMPLEMENTATION.
METHOD confirmOrder.
" Changer le statut
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.
" Lire les donnees actuelles
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Declencher le Domain Event
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.
" Resultat
result = CORRESPONDING #( lt_orders ).
ENDMETHOD.
ENDCLASS.

Event Handler dans un autre 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'.
" Lire les donnees de l'Event
DATA ls_event TYPE zs_order_confirmed_event.
ls_event = CORRESPONDING #( is_event_data ).
" Creer un Shipment dans le contexte Logistics
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

Un concept central du DDD est le Ubiquitous Language - un vocabulaire commun pour le domaine metier et le developpement :

Conventions de nommage CDS

" Les noms d'Entity correspondent aux termes metier
define root view entity ZI_SalesOrder " Commande client
define view entity ZI_SalesOrderItem " Position de commande
define view entity ZI_DeliveryNote " Bon de livraison
define view entity ZI_Invoice " Facture
define root view entity ZI_Customer " Client
" Les noms de champs sont explicites
@EndUserText.label: 'Commande client"
define root view entity ZI_SalesOrder
{
key order_number as OrderNumber, " Numero de commande
customer_number as CustomerNumber, " Numero client
order_date as OrderDate, " Date de commande
requested_delivery as RequestedDelivery, " Date de livraison souhaitee
net_value as NetValue, " Valeur nette
tax_amount as TaxAmount, " Montant TVA
gross_value as GrossValue, " Valeur brute
payment_terms as PaymentTerms, " Conditions de paiement
shipping_method as ShippingMethod, " Mode d'expedition
order_status as OrderStatus " Statut de commande
}

Types de donnees specifiques au domaine

" Elements de donnees propres pour les termes metier
@EndUserText.label: 'Numero de commande"
@AbapCatalog.dataMaintenance: #ALLOWED
define type zdd_order_number : abap.numc(10);
@EndUserText.label: 'Numero client"
define type zdd_customer_number : abap.numc(10);
@EndUserText.label: 'Statut de commande"
define type zdd_order_status : abap.char(2);
" Utilisation dans une table et 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;
}

Integration des Bounded Contexts

Anti-Corruption Layer

Un Anti-Corruption Layer protege son propre Bounded Context des modeles de donnees etrangers :

" Interface vers le systeme externe
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.
" Recuperer le produit externe
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.
" Traduire vers notre propre modele de domaine
rs_product = translate_product( ls_external ).
ENDMETHOD.
METHOD translate_product.
" Mapping vers notre propre structure
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.
" Mapper les categories externes vers les internes
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

Une Context Map documente les relations entre les Bounded Contexts :

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

Bonnes pratiques pour DDD avec RAP

1. Definir correctement les limites d’Aggregate

" BON : Order est Aggregate Root avec Items comme Children
define root view entity ZI_Order
composition [0..*] of ZI_OrderItem as _Items " Partie de l'Aggregate
" MAUVAIS : Aggregate trop grand
define root view entity ZI_Order
composition [0..*] of ZI_OrderItem as _Items
composition [0..*] of ZI_Invoice as _Invoices " Aggregate propre !
composition [0..*] of ZI_Shipment as _Shipments " Aggregate propre !

2. Coherence au sein de l’Aggregate

" La Determination met a jour les valeurs derivees dans l'Aggregate
determination calculateTotals on modify {
field Quantity, UnitPrice; " Lors de la modification des Items
}
METHOD calculateTotals.
" Sommer tous les Items
DATA(lv_total) = REDUCE decfloat34(
INIT sum = CONV decfloat34( 0 )
FOR item IN lt_items
NEXT sum = sum + item-LineTotal
).
" Mettre a jour la Root Entity
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( TotalAmount )
WITH VALUE #( ( %tky = ls_order-%tky
TotalAmount = lv_total ) ).
ENDMETHOD.

3. Abstraire les Repositories

" Interface Repository pour 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.
" Implementation basee sur RAP
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.
" Charger les Items
READ ENTITIES OF zi_order
ENTITY Order BY \_Items
ALL FIELDS WITH VALUE #( ( OrderId = iv_order_id ) )
RESULT DATA(lt_items).
" Assembler l'Aggregate
rs_order-header = CORRESPONDING #( lt_orders[ 1 ] ).
rs_order-items = CORRESPONDING #( lt_items ).
ENDMETHOD.
ENDCLASS.

4. Methodes metier au lieu de techniques

" BON : Action metier
action confirmOrder result [1] $self;
action shipOrder parameter ZA_ShipmentDetails result [1] $self;
action cancelOrder parameter ZA_CancellationReason result [1] $self;
" MAUVAIS : Action technique
action setStatusConfirmed result [1] $self;
action updateShippingData parameter ZA_ShippingData result [1] $self;

Sujets connexes

Conclusion

Domain-Driven Design et RAP se completent parfaitement :

  • Aggregate = Business Object : l’architecture RAP avec Root et Child Entities correspond directement au concept d’Aggregate
  • Bounded Contexts = Software Components : les structures de packages definissent des limites claires
  • Domain Events = RAP Business Events : couplage lache entre les contextes
  • Repository = EML/RAP Runtime : la persistance est abstraite par le framework

Le DDD necessite plus d’analyse au depart, mais cela est payant par une meilleure maintenabilite, une communication plus claire entre le domaine metier et le developpement, ainsi qu’une architecture plus flexible. RAP fournit la base technique pour mettre en oeuvre ces concepts avec elegance.