Architecture Hexagonale dans ABAP Cloud : Le Pattern Ports & Adapters

Catégorie
Best Practices
Publié
Auteur
Johannes

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

ConceptDescriptionMapping ABAP Cloud
Domain CoreLogique métier pure sans dépendancesClasses ABAP avec Interfaces
Inbound PortInterface pour les requêtes entrantesInterface pour Use Cases
Outbound PortInterface pour les appels sortantsInterface pour Repositories/Services
Driving AdapterAppelle l’applicationRAP Behavior Handlers, HTTP Handler
Driven AdapterAppelé par l’applicationImplémentation Repository, HTTP Client

Pourquoi l’Architecture Hexagonale dans ABAP Cloud ?

Avantages

  1. Testabilité: La logique du domaine peut être testée sans base de données ni services externes
  2. Interchangeabilité: Les adaptateurs peuvent être remplacés sans modifier la logique métier
  3. Indépendance: Le domaine ne connaît pas les détails techniques (RAP, HTTP, DB)
  4. Maintenabilité: Des frontières claires facilitent les changements
  5. Indépendance du Framework: La migration vers de nouvelles technologies devient plus facile

Quand l’effort en vaut-il la peine ?

ScénarioRecommandation
Logique métier complexe avec de nombreuses règlesArchitecture Hexagonale
Opérations CRUD simplesRAP Standard (Managed)
Intégration de plusieurs services externesArchitecture Hexagonale
Prototype ou MVPRAP Standard
Application d’entreprise à long termeArchitecture Hexagonale
Haute couverture de tests requiseArchitecture 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 externes
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.
" 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 Prix
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.
" 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 Commande
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.

Implémentation Use Case

" Service Application implémentant les 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.
" 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 Entrant
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.
" 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épendances
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.
" 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 Commande
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.

Implémentation Adaptateur Base de Données

" Adaptateur Piloté : Repository Base de Données
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).
" 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 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.
" 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

AspectABAP TraditionnelArchitecture Hexagonale
Accès Base de DonnéesSELECT directement dans la logique métierVia Interface Repository
Services ExternesAppels HTTP dans la logique métierVia Interface Service
TestabilitéDifficile (dépendance DB)Facile (Mock-Repositories)
Couplage FrameworkFort (RAP utilisé directement)Faible (RAP uniquement comme adaptateur)
ChangeabilitéLes changements affectent de nombreux endroitsChangements localement limités
RéutilisabilitéFaibleÉlevée (Le domaine peut être réutilisé)

ABAP Traditionnel (Anti-Pattern)

" Tout mélangé - difficile à tester
METHOD 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 testable
METHOD 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 Unitaires
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.
" Test Unitaire - Logique Domaine testable sans DB
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.

Résumé

CoucheResponsabilitéArtefacts ABAP Cloud
DomaineLogique métier pureClasses sans dépendances
ApplicationOrchestration Use CaseClasses Service avec Interfaces
Adaptateur EntrantRequêtes entrantesRAP Handler, HTTP Handler
Adaptateur SortantSystèmes externesRepository, HTTP Client
PortsAbstractionInterfaces

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