Le blog de Quadza Software

Architecture, expertise Progress, tutorat IT
renforcement d'équipes IT

hero-jobbies-7

Architecture Hexagonale : structurez votre code OpenEdge ABL

L'architecture hexagonale (Ports and Adapters) est un modèle de conception qui vise à créer des applications extensibles et maintenables en séparant clairement la logique métier des détails techniques et des dépendances externes.
Ce modèle de conception est tout à fait transposable dans le langage Progress OpenEdge ABL afin d’en tirer les mêmes bénéfices (maintenabilité et capacité d’évolution), démontrant une fois de plus la capacité de cette plateforme à s’adapter aux besoins du moment.

Principe Fondamental

L'idée centrale de l'architecture hexagonale est de placer le domaine métier au centre de l'application et d'entourer cette logique métier avec des interfaces et des adaptateurs qui connectent l'application à des technologies externes (comme les bases de données, les systèmes de fichiers, les API externes, etc.).

Composants de l'Architecture Hexagonale

Domaine (Core Domain)

  • Entités : Représentent les objets métier avec leurs propriétés et méthodes.
  • Valeurs Objet : Représentent des objets immuables, souvent utilisés pour les types de données complexes.
  • Services Métier : Contiennent la logique métier qui ne peut pas être facilement placée dans les entités.

Application

  • Cas d'Utilisation : Définissent les actions spécifiques que l'application peut effectuer. Ils orchestrent les appels vers le domaine et gèrent les transactions.
  • Ports (Interfaces) : Définitions des interfaces que les adaptateurs doivent implémenter. Ils permettent de connecter le domaine aux systèmes externes.

Ports

  • Ports Primaires (Driving Ports) : Interfaces définissant les services que l'application expose à ses utilisateurs (par exemple, des API REST, des interfaces utilisateur).
  • Ports Secondaires (Driven Ports) : Interfaces définissant les services externes que l'application utilise (par exemple, les bases de données, les services externes).

Adaptateurs

  • Adaptateurs Primaires (Driving Adapters) : Implémentations concrètes des ports primaires. Ils gèrent les interactions avec les utilisateurs (comme les contrôleurs d'API REST, les interfaces utilisateur).
  • Adaptateurs Secondaires (Driven Adapters) : Implémentations concrètes des ports secondaires. Ils gèrent les interactions avec les systèmes externes (comme les repositories de bases de données, les services API externes).

Flux de Travail

  1. Interaction Utilisateur : Les utilisateurs interagissent avec l'application via des adaptateurs primaires (par exemple, en envoyant une requête HTTP à une API REST).
  2. Appel aux Cas d'Utilisation : L'adaptateur primaire invoque un cas d'utilisation spécifique dans la couche application.
  3. Logique Métier : Le cas d'utilisation appelle les services métier et les entités dans le domaine pour exécuter la logique métier requise.
  4. Interaction avec les Systèmes Externes : Si nécessaire, le cas d'utilisation invoque des ports secondaires pour interagir avec des systèmes externes via des adaptateurs secondaires.
  5. Résultat : Le cas d'utilisation retourne le résultat à l'adaptateur primaire, qui le formate et le renvoie à l'utilisateur.

Avantages

  • Indépendance Technologique : Les dépendances techniques et la logique métier sont clairement séparées, permettant de changer facilement de technologie sans affecter le domaine.
  • Testabilité : La logique métier peut être testée de manière isolée en utilisant des doubles de test (mocks ou stubs) pour les ports.
  • Extensibilité : De nouveaux adaptateurs peuvent être ajoutés sans modifier la logique métier.
  • Maintenabilité : La séparation des préoccupations rend le code plus propre et plus facile à maintenir.

Schéma de l'Architecture Hexagonale

schema hexa

Exemple de Code en ABL (OpenEdge)

Domain (Logique métier)

Fichier: Customer.cls

CLASS Customer:

DEFINE PRIVATE VARIABLE customerId AS CHARACTER NO-UNDO.
DEFINE PRIVATE VARIABLE name AS CHARACTER NO-UNDO.
DEFINE PRIVATE VARIABLE email AS CHARACTER NO-UNDO.

CONSTRUCTOR PUBLIC Customer
(cid AS CHARACTER, cname AS CHARACTER, cemail AS CHARACTER):
customerId = cid.
Name =name.
email = cemail.
END CONSTRUCTOR.

METHOD PUBLIC CHARACTER getCustomerId():
RETURN customerId.
END METHOD.

METHOD PUBLIC CHARACTER getName():
RETURN name.
END METHOD.

METHOD PUBLIC CHARACTER getEmail():
RETURN email.
END METHOD.

END CLASS.

 

 

Application (Cas d'utilisation) et Ports (Interfaces)

Fichier: CustomerService.cls

USING Progress.Lang.*.

INTERFACE CustomerRepository:
METHOD PUBLIC VOID addCustomer(INPUT customer AS Customer).
METHOD PUBLIC Customer getCustomerById(INPUT customerId AS CHARACTER).
END INTERFACE.

CLASS CustomerService:

DEFINE PRIVATE VARIABLE repository AS CustomerRepository NO-UNDO.

CONSTRUCTOR PUBLIC CustomerService(INPUT repo AS CustomerRepository):
repository = repo.
END CONSTRUCTOR.

METHOD PUBLIC VOID registerCustomer
(customerId AS CHARACTER, name AS CHARACTER, email AS CHARACTER):

DEFINE VARIABLE customer AS Customer NO-UNDO.
customer = NEW Customer(customerId, name, email).
repository:addCustomer(customer).
END METHOD.

METHOD PUBLIC Customer findCustomerById(INPUT customerId AS CHARACTER):
RETURN repository:getCustomerById(customerId).
END METHOD.

END CLASS.

 

Adapters (Implémentations concrètes)

Fichier: CustomerRepositoryImpl.cls

USING Progress.Lang.*.

CLASS CustomerRepositoryImpl IMPLEMENTS CustomerRepository:

DEFINE PRIVATE TEMP-TABLE ttCustomer NO-UNDO:
FIELD customerId AS CHARACTER. FIELD name AS CHARACTER.
FIELD email AS CHARACTER.
END.

METHOD PUBLIC VOID addCustomer(INPUT customer AS Customer):
CREATE ttCustomer.
ASSIGN
ttCustomer.customerId = customer:getCustomerId()
ttCustomer.name = customer:getName()
ttCustomer.email = customer:getEmail().
END METHOD.

METHOD PUBLIC Customer getCustomerById(INPUT customerId AS CHARACTER):
FIND FIRST ttCustomer WHERE ttCustomer.customerId = customerId NO-LOCK
NO-ERROR.
IF AVAILABLE ttCustomer THEN
RETURN NEW Customer(ttCustomer.customerId, ttCustomer.name, ttCustomer.email).
ELSE
RETURN ?.
END METHOD.

END CLASS.

 

Utilisation

Fichier: Main.p

USING CustomerService.
USING CustomerRepositoryImpl.

DEFINE VARIABLE repository AS CustomerRepositoryImpl NO-UNDO. DEFINE VARIABLE service AS CustomerService NO-UNDO.

repository = NEW CustomerRepositoryImpl().
service = NEW CustomerService(repository).

service:registerCustomer("1", "John Doe", "john.doe@example.com").
MESSAGE service:findCustomerById("1"):getName().

 

Cette structure permet de maintenir une séparation claire entre la logique métier et les détails de l'implémentation, rendant ainsi le code plus modulaire et plus facile à tester.

 

Représentation en UML

Diagramme de Classes

Ce diagramme montre les relations entre les différentes classes de notre architecture.

Diagramme de classes

 

Diagramme de Cas d'Utilisation

Ce diagramme montre les principales interactions entre l'utilisateur et le système.

Diagramme de cas dutilisation

 

Diagramme de Séquence

Ce diagramme montre l'ordre des appels entre les objets pour un scénario donné, comme l'enregistrement d'un client.

Diagramme de séquence

 

Conclusion

Que l’on soit dans le monde Progress OpenEdge ou non, l'architecture hexagonale permet de construire des applications robustes et évolutives en séparant clairement la logique métier des détails techniques. Cela rend le code plus facile à maintenir et à tester, tout en permettant une flexibilité accrue pour les changements technologiques.

Ceci est particulièrement utile pour les éditeurs de logiciels métier dont les règles métiers sont maintenues durant plusieurs dizaines d’années.

Quadza Software peut accompagner vos équipes dans la durée pour leur permettre de mettre en place une telle approche.

 

Patrick