L’Architecture Hexagonale (également appelée Ports & Adapters) est un pattern architectural qui isole la logique métier principale des dépendances externes. Dans ABAP Cloud, ce pattern permet de créer des applications testables, maintenables et flexibles.
Qu’est-ce que l’Architecture Hexagonale ?
L’Architecture Hexagonale a été développée par Alistair Cockburn et résout un problème fondamental : Comment séparer la logique métier des détails techniques ?
┌─────────────────────────────────────────────┐ │ ADAPTATEURS PILOTANTS │ │ (UI, REST API, Tests, Jobs Batch) │ └──────────────────┬──────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────┐ │ PORTS ENTRANTS │ │ (Interfaces / Use Cases) │ └──────────────────┬───────────────────────────┘ │ ┌──────────────────▼───────────────────────────┐ │ │ │ NOYAU DOMAINE │ │ (Logique Métier) │ │ │ └──────────────────┬───────────────────────────┘ │ ┌──────────────────▼───────────────────────────┐ │ PORTS SORTANTS │ │ (Repository, Interfaces Service) │ └──────────────────┬───────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────┐ │ ADAPTATEURS PILOTÉS │ │ (Base de données, APIs Externes, Système de fichiers) │ └──────────────────────────────────────────────┘Concepts Clés
| Concept | Description | Mapping ABAP Cloud |
|---|---|---|
| Domain Core | Logique métier pure sans dépendances | Classes ABAP avec Interfaces |
| Inbound Port | Interface pour les requêtes entrantes | Interface pour Use Cases |
| Outbound Port | Interface pour les appels sortants | Interface pour Repositories/Services |
| Driving Adapter | Appelle l’application | RAP Behavior Handlers, HTTP Handler |
| Driven Adapter | Appelé par l’application | Implémentation Repository, HTTP Client |
Pourquoi l’Architecture Hexagonale dans ABAP Cloud ?
Avantages
- Testabilité: La logique du domaine peut être testée sans base de données ni services externes
- Interchangeabilité: Les adaptateurs peuvent être remplacés sans modifier la logique métier
- Indépendance: Le domaine ne connaît pas les détails techniques (RAP, HTTP, DB)
- Maintenabilité: Des frontières claires facilitent les changements
- Indépendance du Framework: La migration vers de nouvelles technologies devient plus facile
Quand l’effort en vaut-il la peine ?
| Scénario | Recommandation |
|---|---|
| Logique métier complexe avec de nombreuses règles | Architecture Hexagonale |
| Opérations CRUD simples | RAP Standard (Managed) |
| Intégration de plusieurs services externes | Architecture Hexagonale |
| Prototype ou MVP | RAP Standard |
| Application d’entreprise à long terme | Architecture Hexagonale |
| Haute couverture de tests requise | Architecture Hexagonale |
Mapping sur les Concepts ABAP Cloud
┌─────────────────────────────────────────────────────────────────┐│ ABAP CLOUD HEXAGONAL ││ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ ADAPTATEURS PILOTANTS │ ││ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ ││ │ │ RAP │ │ HTTP │ │ Background Job │ │ ││ │ │ Behavior │ │ Handler │ │ (Application Job) │ │ ││ │ └─────┬──────┘ └─────┬──────┘ └──────────┬─────────────┘ │ ││ └───────┼──────────────┼───────────────────┼───────────────┘ ││ │ │ │ ││ ▼ ▼ ▼ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ PORTS ENTRANTS (Interfaces) │ ││ │ zif_order_service, zif_invoice_service │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ COUCHE APPLICATION (Use Cases) │ ││ │ zcl_create_order_use_case, zcl_cancel_order_use_case │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ COUCHE DOMAINE │ ││ │ zcl_order (Entity), zcl_order_line (Entity) │ ││ │ zcl_price_calculator (Domain Service) │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ PORTS SORTANTS (Interfaces) │ ││ │ zif_order_repository, zif_customer_service │ ││ └────────────────────────┬─────────────────────────────────┘ ││ │ ││ ┌────────────────────────▼─────────────────────────────────┐ ││ │ ADAPTATEURS PILOTÉS │ ││ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ ││ │ │ DB │ │ HTTP │ │ SAP ERP │ │ ││ │ │ Repository │ │ Client │ │ Integration │ │ ││ │ └────────────┘ └────────────┘ └────────────────────────┘ │ ││ └──────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘Couche Domaine : La Logique Métier
La Couche Domaine contient la logique métier pure sans aucune dépendance aux frameworks techniques.
Entité de Domaine
" Entité de Domaine : Order" Aucune dépendance à RAP, Base de données ou services externesCLASS 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.
" Méthode Factory 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.
" Reconstruire depuis la persistance 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.
" Méthodes Métier 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. " Règle Métier : L'ID client est obligatoire 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. " Règle Métier : On ne peut ajouter des lignes qu'aux commandes en brouillon 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.
" Règle Métier : La quantité doit être 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. " Règle Métier : Impossible de soumettre une commande vide IF mt_lines IS INITIAL. RAISE EXCEPTION TYPE zcx_order_error MESSAGE e004(zorder) WITH 'Cannot submit empty order'. ENDIF.
" Règle Métier : Seules les commandes en brouillon peuvent être soumises 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. " Règle Métier : Impossible d'annuler une commande déjà annulée 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.Service Domaine
Pour la logique qui n’appartient pas à une seule entité :
" Service Domaine : Calculateur de PrixCLASS 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. " Logique métier pure - pas de dépendances externes 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.Couche Application : Use Cases
La Couche Application orchestre la logique du domaine et coordonne le flux de données.
Port Entrant (Interface)
" Port Entrant : Interface Service CommandeINTERFACE 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.Implémentation Use Case
" Service Application implémentant les 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. " Créer une nouvelle commande en utilisant la logique du domaine DATA(lo_order) = zcl_order=>create( iv_customer_id = is_request-customer_id iv_currency = is_request-currency ).
" Persister via repository mo_order_repository->save( lo_order ).
" Retourner la réponse 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. " Charger la commande depuis le 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.
" Obtenir le prix du produit depuis le 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.
" Ajouter la ligne en utilisant la logique du domaine lo_order->add_line( iv_product_id = is_request-product_id iv_quantity = is_request-quantity iv_unit_price = lo_product->get_price( ) ).
" Persister les changements 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.Adaptateur pour RAP (Port Entrant)
Le RAP Behavior Handler fonctionne comme un Driving Adapter, qui reçoit les requêtes et les transmet à la Couche Application.
RAP comme Adaptateur Entrant
" RAP Behavior Handler comme Adaptateur EntrantCLASS 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. " Injection de Dépendances 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 ) ).
" Mapper la réponse au résultat RAP 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 pour l’Injection de Dépendances
" Factory pour la Création de Services avec Injection de DépendancesCLASS 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.
" Pour les tests : injecter des implémentations mock 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.
" Créer les implémentations réelles 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.Adaptateur pour Services Externes (Port Sortant)
Définition du Port Sortant
" Port Sortant : Interface Repository CommandeINTERFACE 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.Implémentation Adaptateur Base de Données
" Adaptateur Piloté : Repository Base de DonnéesCLASS 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).
" Mapper les enregistrements DB à l'Entité Domaine 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( ).
" Mapper l'Entité Domaine aux enregistrements DB et persister 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.Adaptateur HTTP pour APIs Externes
" Adaptateur Piloté : Service Client Externe 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. " Client HTTP 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.Comparaison avec l’Architecture ABAP Traditionnelle
| Aspect | ABAP Traditionnel | Architecture Hexagonale |
|---|---|---|
| Accès Base de Données | SELECT directement dans la logique métier | Via Interface Repository |
| Services Externes | Appels HTTP dans la logique métier | Via Interface Service |
| Testabilité | Difficile (dépendance DB) | Facile (Mock-Repositories) |
| Couplage Framework | Fort (RAP utilisé directement) | Faible (RAP uniquement comme adaptateur) |
| Changeabilité | Les changements affectent de nombreux endroits | Changements localement limités |
| Réutilisabilité | Faible | Élevée (Le domaine peut être réutilisé) |
ABAP Traditionnel (Anti-Pattern)
" Tout mélangé - difficile à testerMETHOD create_order. " Accès DB direct dans la logique métier SELECT SINGLE * FROM zcustomer WHERE customer_id = @iv_customer_id INTO @DATA(ls_customer).
IF ls_customer-status <> 'ACTIVE'. " Gestion directe des messages MESSAGE e001(zorder). RETURN. ENDIF.
" Appel HTTP directement dans la méthode DATA(lo_client) = cl_http_client=>create_by_destination( 'PRICING_API' ). " ... Logique HTTP ...
" Insert DB direct INSERT INTO zorder VALUES @ls_order.ENDMETHOD.Architecture Hexagonale (Propre)
" Proprement séparé - facilement testableMETHOD create_order. " Validation via Interface Service IF NOT mo_customer_service->validate_customer( iv_customer_id ). RAISE EXCEPTION TYPE zcx_order_error. ENDIF.
" Logique Domaine DATA(lo_order) = zcl_order=>create( iv_customer_id = iv_customer_id iv_currency = iv_currency ).
" Persistance via Interface Repository mo_order_repository->save( lo_order ).ENDMETHOD.Testabilité avec Mock-Repositories
" Mock Repository pour Tests UnitairesCLASS 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.
" Test Unitaire - Logique Domaine testable sans DBCLASS 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.Résumé
| Couche | Responsabilité | Artefacts ABAP Cloud |
|---|---|---|
| Domaine | Logique métier pure | Classes sans dépendances |
| Application | Orchestration Use Case | Classes Service avec Interfaces |
| Adaptateur Entrant | Requêtes entrantes | RAP Handler, HTTP Handler |
| Adaptateur Sortant | Systèmes externes | Repository, HTTP Client |
| Ports | Abstraction | Interfaces |
L’Architecture Hexagonale nécessite plus d’efforts initiaux, mais se révèle payante pour les applications complexes et à long terme grâce à une meilleure testabilité, maintenabilité et flexibilité.
Sujets Associés
- Domain-Driven Design avec RAP - Concepts DDD dans ABAP Cloud
- Design Patterns pour RAP - Patterns d’architecture éprouvés
- Clean ABAP - Code ABAP propre et maintenable
- Test Doubles et Mocking - Développer des applications testables
- RAP Basics - Fondamentaux du RESTful ABAP Programming