Je vais vous présenter dans cet article le principe d’architecture hexagonale et comment le mettre en place concrètement en utilisant le TDD et les principes SOLID. On verra également comment l’utilisation de l’architecture hexagonale permet de mettre en place facilement des TNR (Tests de Non Régression) automatiques.
Lorsqu’on a un projet (par exemple un mirco-service) avec des règles métier (Business Rules), l’architecture Hexagonale permet de séparer la partie métier (Domain) de la partie technique. L’objectif c’est de faire en sorte qu’il n’y ait aucune dépendance entre le Domain et le monde extérieur (services externes, type de base de données, librairies externes…).
Comment structurer le projet ?
Voici un exemple de comment vous pouvez découper votre projet lorsque vous utilisez une architecture hexagonale (je fais mon exemple en utilisant C# mais le principe est le même pour les autres langages de POO tel que le Java)

- Entities
Les entités sont des objets internes à notre projet. Elles sont des objets sans méthode (autres que les méthodes héritées de la classe Object). Les entités sont composées uniquement de propriétés (attributs dans le monde Java) de types simples : les primitifs, les primitifs nullables, d’autres entités plus petites, les dates… Il ne doit y avoir aucun objet externe (input/output service externe, Framework, librairie externe…) en dehors des objets standard dans les entités.
- Ports
Les ports sont des interfaces avec le monde externe à notre Domain. Il est primordial de n’avoir aucune dépendance (référence) externe. Dans les signatures des méthodes des interfaces des ports (arguments et objets retournés), il ne doit y avoir que des entités définies dans notre projet ou des types simples, et surtout pas un objet d’un service externe.
Supposons que dans notre projet pour faire certains traitements métier nous avons besoin de convertir des prix en EUR (convertir tous les prix en EUR avant de procéder aux traitements métiers). Supposons qu’il existe un service externe (service REST Web Api, service WCF, librairie externe,..) qui propose ce service. Lorsqu’on va créer notre port (interface) on ne mettra que nos entités et les types standard. Par exemple on pourrait créer notre entité PriceEntity et le port IPriceConverterPort suivants :
namespace Entities
{
public class PriceEntity
{
public double? Value { get; set; }
public string Currency { get; set; }
}
}
using Entities;
namespace Ports
{
public interface IPriceConverterPort
{
double? ConvertToEur(PriceEntity priceEntity);
}
}
En fonctionnant ainsi on fait vraiment abstraction du service qui va nous faire la conversion et on va éviter toute dépendance avec notre Domain (cf. rubrique Domain plus loin). L’avantage principal c’est que si le service externe évolue (changement de version, choix d’un nouveau service, changement de protocole WCF en HTPP, changement des inputs/outputs,…) il n y aura aucun impact sur notre Domain. En effet notre Domain ne connait pas les Adapters (en terme de dépendance). Ce qui fait que si une méthode externe est mal nommée ou retourne un gros objet cela n’aura pas d’impact sur la lisibilité (qualité) du code de notre Domain.
- Adapters
Les adapters sont les implémentations des Ports. Il ne faut absolument jamais créer de dépendance entre le Domain et les Adapters (le Domain ne doit jamais référencer les Adapters). Les Adapters référencient les Ports. Et toute dépendance externe (objet externe, type de protocole de communication, type de base de données…) doit rester dans les Adpaters.
Reprenons notre cas d’exemple ci-dessous. Supposons qu’il y a un service externe WCF (ou HTTP ou même une DLL externe) qui permet de faire la conversion en EUR. Supposons que le service externe dispose d’une méthode Convert qu’on veut utiliser.
public ResultInfo Convert(InputInfo input);
//Avec InputInfo
public class InputInfo
{
public double? Value { get; set; }
public string FromCurrency { get; set; }
public string TargetCurrency { get; set; }
public DateTime? Date { get; set; }
}
Cette méthode est un peu pourrie (méthode mal nomée, objets mal nommés). Pour notre besoin, on a besoin de convertir en EUR (c’est pourquoi dans notre port on a créé une méthode ConvertToEur, une entité qui modélise le prix avec sa monnaie et la valeur décimale retournée). Voici comment on pourrait implémenter notre Adapter (en omettant les try/catch)
using System;
using Entities;
using Ports;
namespace Adapters
{
public class PriceConverterAdapter : IPriceConverterPort
{
public const string EurCurrency = "EUR";
public double? ConvertToEur(PriceEntity priceEntity)
{
if (priceEntity == null)
{
throw new ArgumentNullException(nameof(priceEntity), "Unable to call PriceConverter service");
}
var externalInputInfo = TransformPriceEntityToExternalInput(priceEntity);
var resultInfo = Convert(externalInputInfo); //Call external method
var convertedPrice = TransformResultInfoToDoube(resultInfo);
return convertedPrice;
}
private static InputInfo TransformPriceEntityToExternalInput(PriceEntity priceEntity)
{
return new InputInfo
{
Value = priceEntity.Value,
FromCurrency = priceEntity.Currency,
TargetCurrency = EurCurrency,
Date = DateTime.Now
};
}
//Other methods
}
}
Dans cet adapter on a globalement 3 parties : la transformation de notre entité en input de la méthode externe, l’appel de la méthode externe et la transformation de l’output externe en entité interne. En procédant ainsi on va regrouper toutes les dépendances avec le monde extérieur dans le sous-projet Adapters. C’est la partie du projet qui doit changer si le service externe change.
Il est également impératif de faire des tests d’intégration sur nos adapters.
- Domain
Le Domain c’est la partie la plus importante de notre projet. Le Domain doit contenir toutes les règles métier. Lorsqu’on est en DDD (Domain Driven Design), le simple fait de lire le code devrait permettre (par exemple à un nouveau développeur) de comprendre le Business. C’est dans le Domain (certains l’appellent aussi le Core) qu’on doit écrire ce que fait notre projet (l’objectif de notre projet).
Le Domain ne doit jamais référencer les Adpaters, et du coup, ne doit « connaitre » que des objets internes (entités ou types simples). Il doit référencer les ports par injection de dépendances.
Supposons par exemple que l’un des buts de notre projet c’est de faire le calcul de la somme de plusieurs prix avec différentes monnaies qu’il faudra convertir en EUR avant (en utilisant notre service externe). Le calcul se fera du coup dans notre Domain et nous allons utiliser le Port IPriceConverterPort par injection (par exemple par Constructeur)
using System.Collections.Generic;
using Entities;
namespace Domain
{
public interface IPriceComputingService
{
double? ComputePriceSum(IList<PriceEntity> priceEntities);
}
}
using System.Collections.Generic;
using System.Linq;
using Entities;
using Ports;
namespace Domain
{
public class PriceComputingService : IPriceComputingService
{
private readonly IPriceConverterPort _priceConverterPort;
public PriceComputingService(IPriceConverterPort priceConverterPort)
{
_priceConverterPort = priceConverterPort;
}
public double? ComputePriceSum(IList<PriceEntity> priceEntities)
{
if (priceEntities == null || priceEntities.Any() == false)
{
return null;
}
return priceEntities.Sum(priceEntity => _priceConverterPort.ConvertToEur(priceEntity));
}
}
}
La classe (très simplifiée) PriceComputingService permet ainsi de traiter un requirement sans aucune dépendance externe. Pour tester la classe on peut très facilement mocker le service externe grâce à l’injection de dépendances. Idem on peut très facilement mettre en place des tests de non régression (qui vont s’exécuter très rapidement car on va mocker les dépendances externes) de notre Domain.
- IoC (Inversion Of Control)
L’IoC c’est le projet où on instancie les classes à utiliser au Runtime pour les interfaces injectées. Par exemple lorsqu’on utilise Unity voici à quoi pourrait ressembler l’instanciation des interfaces.
using Adapters;
using Domain;
using Microsoft.Practices.Unity;
using Ports;
namespace IoC
{
public class UnityInitializer
{
protected IUnityContainer UnityContainer;
public UnityInitializer()
{
UnityContainer = new UnityContainer();
RegisterAll();
}
private void RegisterAll()
{
RegisterAdapters();
RegisterDomain();
}
private void RegisterAdapters()
{
UnityContainer.RegisterType<IPriceConverterPort, PriceConverterAdapter>();
}
private void RegisterDomain()
{
UnityContainer.RegisterType<IPriceComputingService, PriceComputingService>();
}
public IUnityContainer GetContainer()
{
return UnityContainer;
}
}
}
Lorsqu’on voudra faire des TNR (ou faire des BDD) on pourra injecter des FakeAdapters pour tester le Domain de notre service.
Comment le mettre en place en TDD?
Maintenant qu’on a vu en quoi consiste l’architecture héxagonale et ses avantages on va voir comment le mettre en place facilement en TDD.
Supposons qu’on a un nouveau requirement qui consiste à récupérer des données de taux à partir d’un service externe (par exemple service REST) et qu’on doit intégrer ces données dans notre workflow (Domain). En tant que développeur on doit implémenter ce requirement (idéalement en Pair Programming ou en Mob Programming), voici comment on pourrait procéder step by step.
- Commencer par étudier (sans écrire de ligne de code) le service externe : si c’est un service WebApi REST, voir sur quelles routes il faut l’appeler, si c’est du WCF voir si on a les infos pour l’appeler (EndPoints),…
- Ecrire un test d’intégration dans une nouvelle classe de tests (dont le nom fait référence au service externe). Pour notre exemple on pourra créer un test
[Fact]
public void Should_Get_Rates()
{
//Code here
}
- Ensuite à l’intérieur de la méthode de test implémenter le requirement (sans faire de refacto ni créer d’entités pour le moment).
- S’assurer que le test est vert (on arrive bien à appeler le service externe et qu’il renvoie bien les données attendues)
- A partir de notre longue méthode de test, extraire les entités (créer des objets internes à notre projet qui seront utilisés par nos ports et Domain). Pour le moment les entités (inputs et outputs) peuvent rester dans le fichier de test (classes à part). Il est primordial que dans nos entités (inputs et outputs) de ne mettre aucun objet/type créé par le service externe. Les entités permettent dans ce cas de Wrapper les objets externes.
- Extraire le Port et l’Adapter associé. S’assurer que dans la signature de méthode extraite qu’il n’y ait pas d’objets externes.
- Relancer le test pour voir si c’est toujours vert
- Refactorer proprement le code : par exemple si on fait un appel HTTP, s’assurer qu’on utilise un client générique (à mettre en place s’il n’existe pas) et l’injecter à notre Adapter. S’assurer que les principes de base du clean code sont respectés.
- A ce stade notre test doit être très simplifié vu qu’on utilise maintenant les Port/Adapter pour faire notre appel.
- Maintenant on peut déplacer les entités, Port, Adapter, ClientHttp,.. dans leurs bons repos/projets (projets Entities, Ports, Adapters, Common,…)
- Mettre un test unitaire pour l’apdater : tester les cas d’erreurs (input NULL, exception catchée), tester le cas nominal… Faire les implémentations associées. Relancer le test d’intégration pour voir si on n’a rien cassé.
- S’assurer que tous les tests sont verts avant de commiter (au moins tous les TU et le TI qu’on a ajouté).
- Enfin demander la revue de code (si on codé seul) ou déployer notre DEV (si on a travaillé en pairing/Mob) et aller prendre un café bien mérité 🙂
Conclusion
Voilà, j’espère que vous avez compris un mieux comment mettre en place une architecture Hexagonale et surtout ses nombreux avantages et que vous allez vraiment le tester. N’hésitez pas à me faire un feedback sur cet article 🙂
Enfin la raison pour laquelle ça s’appelle architecture « Hexagonale » (j’aurai dû commencer par ça je sais 🙂 ) est due au fait qu’on dessine souvent un hexagone pour modéliser le Domain (au centre), les Ports (extrémités intérieures de l’hexagone) et les Adapters (extrémités extérieures de l’hexagone)
