Comment structurer une application complexe ?

Introduction aux modulithes


Lorsqu’on écrit une nouvelle application, on se pose souvent la question de comment organiser ses fichiers source.

Une pratique courante est de grouper les fichiers par “couches horizontales”, par exemple:

controllers
domain
persistence
services

et dans ces dossiers des classes du genre:

controllers
  CustomerController
  InvoiceController
  ContractController
  ...
domain
  Customer
  Invoice
  Contract
  ...
persistence
  CustomerRepository
  InvoiceRepository
  ContractRepository
  ...
services
  CustomerService
  InvoiceService
  ContractService
  ...

Parfois, il y a des variantes:

controllers
  customers
    CustomerController
  invoice
    InvoiceController
  contracts
    ContractController
  ...
domain
  customers
    Customer
  invoice
    Invoice
  contracts
    Contract
  ...
persistence
  customers
    CustomerRepository
  invoice
    InvoiceRepository
  contracts
    ContractRepository
  ...
services
  customers
    CustomerService
  invoice
    InvoiceService
  contracts
    ContractService
  ...

Ou pour les adeptes d’architectures hexagonales:

ports
  CustomerPort
  InvoicePort
  ContractPort
  ...
adapters
  in
    CustomerController
    InvoiceController
    ContractController
    ...
  out
    CustomerRepository
    InvoiceRepository
    ContractRepository
    ...
domain
  Customer
  Invoice
  Contract
  ...

Il y a bien entendu d’autres variantes mais l’idée est la même: le découpage est réalisé selon des critères techniques.

Le hic c’est que cela peut fonctionner pour des petites applications mais que ça devient vite compliqué à gérer lorsque le domaine se complique.

Si on essaie de représenter comment les objets dans ces packages communiquent entre eux, cela ressemble souvent à:

Architecture fort couplage et faible cohésion

Là, je n’ai représenté que 3 classes par package mais imaginez ces relations dans une application réelle…On dit que ce type d’architecture a une cohésion faible et un couplage fort.

La cohésion est faible car les éléments susceptible de changer ensemble sont éparpillés: si, par exemple, on devait ajouter une fonctionnalité concernant les contrats, il y a de fortes chances que l’on doive modifier des fichiers dans chacun des packages.

Le couplage est fort car il y a beaucoup d’échanges entre des éléments de packages différents.

Notez qu’avec ce type de structure, il est impossible de savoir ce que fait l’application en ne regardant que ces packages et leurs relations.

Une meilleure approche est d’effectuer un découpage “vertical”, c’est-à-dire regrouper les packages par “modules métier”.

Par exemple:

customers
  Customer
  CustomerController
  CustomerRepository
  CustomerService
invoice
  Invoice
  InvoiceController
  InvoiceRepository
  InvoiceService
contracts
  Contracts
  ContractsController
  ContractsRepository
  ContractsService
...

voire éventuellement:

customers
  Customer
  Controller
  Repository
  Service
  ...
invoice
  Invoice
  Controller
  Repository
  Service
  ...
contracts
  Contracts
  Controller
  Repository
  Service
  ...
...

Si on trace le diagramme de relations, on obtient:

Architecture faible couplage et forte cohésion

La cohésion est bien meilleure avec cette structure et le couplage entre composants est réduit.

Ce type d’architecture est souvent la base de ce que l’on appelle désormais des “modulithes”: des applications structurées selon le fonctionnel plutôt que le technique. C’est une approche souvent utilisées par des équipes qui ne souhaitent pas se lancer dans des microservices sans vraiment aucune raison: au fur et à mesure des fonctionnalités, les développeurs apprennent à écrire du code modulaire et si le besoin s’en fait sentir, il est en effet tout à fait possible de transformer certains modules en microservices par la suite.

Avec cette approche, il est “facile” de contrôler la visibilité de chacun des éléments d’un module et éviter les “relations spaghetti” du premier découpage. Une équipe peut ainsi décider que les modules ne peuvent communiquer entre eux que par l’intermédiaire de services et/ou d’évènements.

J’écris “facile” entre guillemets car il n’y a pas encore beaucoup d’outils qui aident à contrôler cela (ArchUnit, Spring Modulith mais c’est pour le monde de la JVM).

Il y a bien entendu des questions que l’on peut se poser avec ce type d’architecture (comment on gère les évènements, comment fait-on pour faire communiquer les objets métiers, etc.) mais cela nécessite plusieurs articles pour en discuter.


© 2023 Du côté de chez Fouad. Tous droits réservés.