Onion Architecture
L'Onion Architecture, aussi nommé Architecture en Oignon, est un patron d'architecture conçu pour gérer la complexité des applications en créant des couches bien définies et en s'appuyant sur des principes de séparation des préoccupations et de dépendances inversées. Développé par Jeffrey Palermo, ce modèle vise à rendre le code plus facilement testable, maintenable et évolutif.
Principes de l'Onion Architecture
- Séparation des couches : Les couches sont organisées en cercles concentriques qui permettent de séparer la logique métier des dépendances externes. Les couches internes ne doivent pas dépendre des couches externes, mais l'inverse est permis.
- Dépendance Inversée : L'architecture repose sur le principe de l'inversion de dépendance, c'est-à-dire que les couches externes (infrastructure, interfaces utilisateur) dépendent des couches internes (logique métier).
- Organisation en couches :
- Coeur du Domaine (Domain Core) : Situé au centre, ce noyau contient les entités de domaine (modèles métier) et les interfaces (par exemple, les interfaces de service) définissant les comportements métier. Cette couche ne dépend d'aucune autre composante.
- Logique d'Application (Application Services) : Cette couche contient la logique métier spécifique à l'application et dépend uniquement du domaine. Elle orchestre les opérations métiers, valide les règles et gère les cas d'utilisation sans se préoccuper de la base de données ou de l'interface utilisateur.
- Infrastructure : Cette couche gère les dépendances externes, comme la persistance des données, les API externes et les services d'authentification. Elle implémente les interfaces définies dans les couches internes et contient des adaptateurs pour interagir avec les dépendances.
- Interface Utilisateur : La couche externe, contenant les éléments de l'interface utilisateur (par exemple, les contrôleurs, les vues). Elle dépend des services d'application pour afficher ou manipuler les données.
Avantages de l'Onion Architecture
- Indépendance des dépendances : En utilisant des interfaces pour interagir entre les couches, l'Onion Architecture permet une indépendance par rapport aux technologies ou cadres d'applications utilisés. La logique métier peut être facilement testée sans dépendances externes.
- Flexibilité et maintenabilité : En séparant les responsabilités et en isolant la logique métier, l'architecture rend le code plus facile à adapter aux changements de besoins ou de technologie.
- Testabilité : La logique métier est isolée des dépendances, ce qui facilite les tests unitaires des couches internes sans nécessiter les composantes d'infrastructure.
- Contrôle de la complexité : En organisant l'application en couches, l'Onion Architecture aide à contrôler la complexité des applications en limitant les interactions entre les composantes.
Limites de l'Onion Architecture
- Courbe d'Apprentissage : Pour les développeurs peu familiers avec les principes de séparation des préoccupations et d'inversion de dépendance, cette architecture peut avoir une courbe d'apprentissage importante.
- Surcharge initiale de conception : Mettre en place toutes les couches nécessaires au début d'un projet peut paraître excessif pour des applications simples ou de petite taille.
- Complexité des dépendances : Le respect strict des couches nécessite de créer des interfaces et des adaptateurs supplémentaires pour les interactions, ce qui peut alourdir le code.
Cas d'utilisation de l'Onion Architecture
L'Onion Architecture est particulièrement adaptée aux applications d'entreprise avec une logique métier complexe, où la séparation des responsabilités et l'indépendance des dépendances sont primordiales, comme :
- Les systèmes de gestion des stocks et des commandes.
- Les systèmes bancaires ou financiers où des règles métier strictes sont appliquées.
- Les applications nécessitant une extensibilité à long terme et une maintenance rigoureuse.
Exemple
Voici un exemple en langage de programmation Free Pascal de l'architecture en Oignon (Onion Architecture). Considérons un exemple simplifié d'application pour gérer un inventaire de produits. L'Onion Architecture est basée sur le principe de couches concentriques protégeant le noyau de la logique métier de la dépendance vis-à-vis des couches extérieures, telles que les interfaces utilisateur, les bases de données, et autres infrastructures.
Créer les entités et les interfaces dans le coeur
Voici le fichier «Core/ProductEntity.pas» :
- Unit ProductEntity;
-
- {$MODE OBJFPC}
-
- INTERFACE
-
- Type
- TProduct=Class
- Private
- FID:Integer;
- FName:String;
- FPrice:Double;
- Public
- Property ID:Integer read FID write FID;
- Property Name:String read FName write FName;
- Property Price:Double read FPrice write FPrice;
- Constructor Create(AID:Integer;AName:String;APrice:Double);
- End;
-
- IMPLEMENTATION
-
- Constructor TProduct.Create(AID:Integer;AName:String;APrice:Double);Begin
- FID:=AID;
- FName:=AName;
- FPrice:=APrice;
- End;
-
- END.
Voici le fichier «Core/IProductRepository.pas» :
Service d'application
Le service d'application gère la logique métier et utilise l'interface du dépôt pour l'accès aux données.
Voici le fichier «Application/ProductService.pas» :
- Unit ProductService;
-
- {$MODE DELPHI}
-
- INTERFACE
-
- Uses
- ProductEntity, IProductRepository;
-
- Type
- TProductService=Class
- Private
- FRepository:IProductRepository.IProductRepository;
- Public
- Constructor Create(ARepository: IProductRepository.IProductRepository);
- Procedure AddProduct(AProduct: TProduct);
- Function FindProductByID(ID: Integer): TProduct;
- End;
-
- IMPLEMENTATION
-
- Constructor TProductService.Create(ARepository:IProductRepository.IProductRepository);Begin
- FRepository:=ARepository;
- End;
-
- Procedure TProductService.AddProduct(AProduct:TProduct);Begin
- FRepository.AddProduct(AProduct);
- End;
-
- Function TProductService.FindProductByID(ID:Integer):TProduct;Begin
- FindProductByID:=FRepository.GetProductByID(ID);
- End;
-
- END.
Infrastructure pour les données
La couche infrastructure implémente l'interface de dépôt.
Voici le fichier «Infrastructure/ProductRepository.pas» :
- Unit ProductService;
-
- {$MODE DELPHI}
-
- INTERFACE
-
- Uses
- ProductEntity, IProductRepository;
-
- Type
- TProductService=Class
- Private
- FRepository:IProductRepository.IProductRepository;
- Public
- Constructor Create(ARepository: IProductRepository.IProductRepository);
- Procedure AddProduct(AProduct: TProduct);
- Function FindProductByID(ID: Integer): TProduct;
- End;
-
- IMPLEMENTATION
-
- Constructor TProductService.Create(ARepository:IProductRepository.IProductRepository);Begin
- FRepository:=ARepository;
- End;
-
- Procedure TProductService.AddProduct(AProduct:TProduct);Begin
- FRepository.AddProduct(AProduct);
- End;
-
- Function TProductService.FindProductByID(ID:Integer):TProduct;Begin
- FindProductByID:=FRepository.GetProductByID(ID);
- End;
-
- END.
Voici le fichier «Infrastructure/ProductRepository.pas» :
- Unit ProductRepository;
-
- {$MODE DELPHI}
-
- INTERFACE
-
- Type
- { Déclaration de l'interface IProductRepository }
- IProductRepository=Interface
- ['{E6C8B2C6-A8C6-4B3F-8BA8-ADF3B2F58D9A}'] // GUID pour l'identification unique
- Procedure AddProduct(Const AProduct:String);
- Function GetProduct(Const AID:Integer):String;
- End;
-
- { Déclaration de la classe TProductRepository implémentant l'interface IProductRepository }
- TProductRepository=Class(TInterfacedObject,IProductRepository)
- Private
- FProducts: array of string;
- Public
- Procedure AddProduct(const AProduct: string);
- Function GetProduct(const AID: Integer): string;
- End;
-
- IMPLEMENTATION
-
- Procedure TProductRepository.AddProduct(Const AProduct:String);Begin
- { Ajouter le produit dans le tableau (à titre d'exemple) }
- SetLength(FProducts,Length(FProducts)+1);
- FProducts[High(FProducts)]:=AProduct;
- End;
-
- Function TProductRepository.GetProduct(Const AID:Integer):String;Begin
- If(AID >= 0)and(AID < Length(FProducts))Then Result:=FProducts[AID]
- Else Result:='Produit non trouvé';
- End;
-
- END.
Interface Utilisateur pour l'interaction
Un programme console utilise le service pour ajouter et rechercher des produits.
Voici le fichier «UI/Main.pas» :
- Program Main;
-
- {$MODE DELPHI}
-
- Uses
- IProductRepository,ProductEntity, ProductService,
- ProductRepository, SysUtils;
-
- Var
- Repository:TProductRepository;
- Service:TProductService;
- NewProduct,FoundProduct:TProduct;
- BEGIN
- Repository:=TProductRepository.Create;
- { Service:=TProductService.Create(Repository);}
-
- { Ajouter un produit }
- NewProduct:=TProduct.Create(1,'Ordinateur portable', 1299.99);
- Service.AddProduct(NewProduct);
- WriteLn('Produit ajouté: ',NewProduct.Name);
-
- { Rechercher le produit par ID }
- FoundProduct:=Service.FindProductByID(1);
- If Assigned(FoundProduct)Then WriteLn('Produit trouvé: ',FoundProduct.Name,' - Prix: $',FoundProduct.Price:0:2)
- Else WriteLn('Produit non trouvé.');
- END.