Vincent Beauvivre
le 

Petite vulgarisation de code moderne pour tech et non-tech

13 minutes de lecture

TDD, BDD, DDD, CQRS, Event Sourcing, Hexagonal … autant de termes barbares qu’on entend de temps à autre dans la bouche des techs. Dans cet article, je vous propose de présenter certains d’entre eux, et de vous donner des aperçus pour décider s’il s’agit de bonnes orientations pour votre projet.

La version pour non-tech

Ces techniques sont relativement indépendantes. Elles permettent de piloter le découpage et l’organisation technique du projet. En voici un aperçu :

ApprochePrincipeIntérêt
BDDCommencer par décrire le comportement métier par du codeSe concentrer sur le besoin métier
TDDCommencer par écrire les tests techniquesAssurer la testabilité et le découpage
CQRSSéparer les flux de lecture et d’écritureÉviter les effets indésirables
Ubiquitous languageLe vocabulaire est clair et identique pour tous les acteurs du projet, y compris dans le codeÉviter les malentendus
Couche DomaineMettre à part le code du métier, sans dépendance de librairiesMaintenance simplifiée
Value objectManipuler les valeurs du métier sous la forme d’objets métier, au lieu de chaînes de caractères, d’entiers, etc.Porter le sens et la validation des données
HexagonalIdentifier et exposer les interactions du métier avec les utilisateurs d’une part (cas d’usage), et l’infrastructure d’autre part (interfaces)Réutilisation de code et maintenabilité
Event SourcingConsidérer l’état d’un objet comme la succession de ses changementsCohérence des données

Les points essentiels :

  • Utiliser ces techniques prend plus de temps et requiert plus d’expertise que des méthodes plus traditionnelles.
  • Leur pertinence est à mettre en balance avec des critères comme la criticité du besoin, la maintenabilité requise, l’équipe projet, l’implication du demandeur

La version pour tech

Avant toute chose, sachez que, de manière générale, ces techniques n’ont pas pour objectif d’améliorer la rapidité de développement. Les techniques de développement rapide existent depuis longtemps. Le coût d’une application en est ainsi réduit efficacement, mais cette tendance apporte son lot d’inconvénients. Pour prendre une analogie, c’est comme si on inventait des méthodes pour bâtir un édifice très rapidement. Mais en contrepartie, le bâtiment serait plus compliqué à entretenir, il aurait moins de possibilité d’évolutions, etc.

C’est donc en pensant au long-terme que ces outils et techniques viennent apporter leurs contributions.

Du besoin au code

Créer des applications, c’est comprendre des besoins et proposer des solutions. Nous appelons cela «le métier», même si cela ne se rapporte pas nécessairement à un domaine professionnel.

Pour décrire ce métier, nous avons l’habitude de l’écrire sous la forme d’un document rédigé. Ce sont les spécifications. En voici un exemple :

1Règle 149 : Lorsque le client ajoute un produit au panier, le montant du panier est augmenté de la valeur du produit.

Juste avec cette petite phrase, on voit émerger les concepts du métier :

  • Les objets métier : le client, le produit, le panier
  • Une action métier : ajouter un produit au panier
  • Un retour d’information : connaître le montant total du panier

    Imaginez que nous transcrivions ces spécifications directement en définitions programmatiques. Cela pourrait donner :

    1  Définir un objet métier : le Panier !
    2
    3      Définir une propriété du panier : la liste des produits qu’il contient
    4
    5      Définir une action : ajouter un produit :
    6
    7          Ajouter ce produit à la liste des produits du panier
    8
    9      Définir une action : connaître le montant total des produits dans le panier :
    10
    11          Renvoyer la somme des prix des produits dans le panier
    

Pour PHP, cet objet pourrait être comme ceci :

1class Panier
2{
3    private ListeDeProduits $listeDesProduits;
4
5    public function ajouterProduit(Produit $produit): void
6    {
7        $this->listeDesProduits->ajouter($produit);
8    }
9    public function calculerMontant(Produit $produit): int
10    {
11        return $this->listeDesProduits->calculerLeMontantTotal();
12    }
13}

Le principal avantage ici, c’est que l’on garde le lien direct entre la spécification et le code. On sait à quelle règle il correspond, et pourquoi on l’a fait. Ça n’a l’air de rien, mais parlez-en avec un développeur qui a dû maintenir un projet, on se demande régulièrement : mais pourquoi on fait ça ? On peut mettre en place tout le comportement de l’application juste avec ces règles. Au passage, écrire le code oblige un tel formalisme qu’il permet aussi de mettre en lumière les incohérences, les manquements … Autre grand intérêt : avec cette façon de procéder, on divise la complexité du métier en sous-ensembles relativement indépendants.

Spécifications comme du code

Allons un peu plus loin : si on écrivait ces specs avec du code ? Il existe même un outil en php pour nous aider dans cette démarche : phpSpec http://phpspec.net

Petit clin d’œil au commit strip qui illustre l’idée parfaitement : https://www.commitstrip.com/fr/2016/08/25/a-very-comprehensive-and-precise-spec/

Pour notre exemple de boutique :

1class PanierSpec extends PhpSpec\ObjectBehavior
2{
3    function it_is_initializable()
4    {
5
6        $this->shouldHaveType(Panier::class);
7    }
8
9    function it_changes_the_amount_when_a_product_is_added()
10    {
11
12        $this->ajouterProduit(new Produit('T-shirt', 3000));
13
14        $this->calculerMontant()->shouldReturn(3000);
15
16    }
17}

On peut lire et comprendre cette règle sans avoir besoin d’être développeur. Et on peut voir spontanément les spécifications qui découlent de cette règle. Par exemple, que le montant devrait être de zéro par défaut.

1function it_has_an_amount_by_default_at_zero()
2{
3    $this->calculerMontant()->shouldReturn(0);
4}

De règle en règle, on peut décrire toute l’application ici, sans la créer. C’est le rôle des spécifications.

Avantage énorme pour écrire les spécifications dans ce format : on peut vérifier que notre application les respecte bien ! Nous venons en fait d’écrire les tests métiers, et ce sont les plus importants !

Dans cette partie, vous avez découvert ce qu’est le « design by Specification » − Conception basée sur les spécifications

Et le « métier » du développeur ?

Quand on a spécifié toute l’application, vous pensez que notre travail est finalisé ? Pas du tout !

Pour le moment, les actions et les objets ne peuvent être gérées que par du code. Selon les applications, on a besoin de relier la base de code du métier à une ou plusieurs sources d’interaction. Elle peut être de plusieurs types :

  • une application graphique sur votre OS, dans le système de fenêtres
  • une API
  • un site internet
  • une application sur téléphone
  • voire même un objet connecté

Ça, c’est le « métier » du développeur. Et sa tâche maintenant consiste à faire des ponts entre son métier et celui du client.

Concentrons-nous sur une source d’interaction web. Selon le principe de ne pas réinventer la roue, nous allons utiliser un framework (cadriciel) qui permet déjà de faire les opérations de base telles que comprendre une requête HTTP (par exemple un visiteur qui navigue sur notre site) et y donner une réponse.

Il nous faut donc brancher les actions sur le serveur aux actions métier. Avec un contrôleur Symfony, cela donnerait :

1class PanierController
2{
3    /**
4     * @Route("/panier/ajouter-produit/{idProduit}", name="app_panier_ajouter_produit")
5      */
6     public function ajouterProduit(int $idProduit, Panier $panier): Response
7    {
8            $panier->ajouterProduit($idProduit);
9            return new Response(
10                '<html><body>Le produit a bien été ajouté</body></html>'
11            );
12    }
13}

Comme on peut le voir, la partie « HTTP » de l’infrastructure est capable de gérer les requêtes HTTP et d’y répondre, en faisant appel au métier. Dans notre jargon, on parle de couches (layers). On peut déjà distinguer deux couches :

  • L’infrastructure est chargée des interactions avec le monde extérieur.
  • Le métier, qui est responsable pour faire appliquer les règles métier.

La première couche peut faire appel à la deuxième, mais on s’interdit de faire le chemin inverse. D’abord, parce qu’on ne souhaite pas que le métier soit contraint par l’infrastructure. Ensuite, d’une façon plus pragmatique, on s’assure ainsi d’avoir une base de code qui ne dépend pas d’éléments extérieurs tel qu’un framework, ou une librairie. Très concrètement : si une modification est apportée sur la façon d’utiliser Symfony (et il y en a !), on ne veut pas avoir à modifier la couche métier pour y correspondre.

Cette séparation, c’est le cœur de ce que l’on nomme DDD, Domain Driven Design − Conception pilotée par le métier.

Stocker des données, et autres relations extérieures

À ce point, nous avons donc une base de code métier indépendante au milieu, une infrastructure autour, et des composants externes.

Un composant externe, ça peut être tout type de brique indépendante :

  • une base de données (PostgreSQL, …)
  • un moteur de recherche (elastic search, …)
  • un système de fichiers (s3, …)
  • un système de file d’attente de messages (AMQP, …)
  • un web-service (https://api.insee.fr, …)
  • une API externe (https://ifttt.com/, …)

D’un point de vue métier, on souhaite clairement ignorer ces détails bassement techniques ! Mais on souhaite indiquer des actions demandées par le métier. Ce qui compte, ce n’est pas comment l’action est réalisée (techniquement), mais quel est son intention.

Si par exemple on souhaite sauvegarder le panier, on utiliserait la magie des contrats que sont les Interfaces.

Objet métier :

1class Panier
2{
3    public function sauvegarder()
4    {
5
6        $this->stockeur->sauvegarder($this);
7    }
8}

Interface métier :

1interface Stockeur
2{
3    public function sauvegarder($panier);
4}

Implémentation infrastructure :

1class StockeurBaseDonnee implements Stockeur
2{
3    public function sauvegarder($panier)
4    {
5        $this->baseDonnee->executer('Insert into panier values ($panier)');
6    }
7}

Pour résumer : un cœur de métier au centre, un ensemble de briques autour pour interagir avec son environnement, et une couche d’adaptation entre les deux. Voilà les bases de ce que l’on peut nommer « l’architecture Hexagonale ». Dans un contexte Hexagonal :

  • Les interactions avec l’utilisateur sont représentées comme le côté gauche de l’application.
  • Les interactions avec l’environnement sont représentées comme le côté droit de l’application.

Exposer des points d’entrée

Revenons à la couche métier, au cœur de notre application. Nous allons déclarer explicitement les actions que nous proposons, dans une nouvelle couche entre l’infrastructure et le métier. On appelle généralement cette couche « Application » et ces actions des « cas d’usage ».

Voici par exemple le cas d’usage « ajout au panier ».

1class AjouterUnProduitAuPanier
2{
3    public function __invoke(AjouterUnProduitAuPanierInput $input): void
4    {
5        $produit = trouverProduitDepuisSonIdentifiant($input->identifiantProduit);
6
7        $this->panier->ajouterProduit($produit);
8    }
9}

Définissons également l’objet que l’on attend en entrée

1class AjouterUnProduitAuPanierInput
2{
3    public string $identifiantProduit;
4}

Ainsi, côté infrastructure (Symfony), faisons appel à ce cas d’usage :

1class PanierController
2{
3    /**
4     * @Route("/panier/ajouter-produit/{idProduit}", name="app_panier_ajouter_produit")
5     */
6    public function ajouterProduit(int $idProduit, AjouterUnProduitAuPanier $useCase): Response
7    {
8        $input = new AjouterUnProduitAuPanierInput();
9        $input->identifiantProduit = idProduit;
10        $useCase($input);
11        return new Response(
12            '<html><body>Le produit a bien été ajouté</body></html>'
13        );
14    }
15}

Par cette technique, on montre clairement ce que l’on peut faire dans le métier. Vous pouvez imaginer un travail en deux équipes (ou deux développeurs) :

  • L’une est chargée de mettre en place la couche métier, et les cas d’usage.
  • L’autre est chargée de faire coller l’infrastructure à cette couche métier.

Valider

Dans le milieu informatique, on entend qu’il vaut mieux avoir de bonnes données dans un mauvais programme, qu’un excellent programme avec des données de mauvaise qualité. Profitons donc de cette démarche pour nous assurer de la validité et de la cohérence des données. La validation concerne toutes les couches de notre application :

  • Dans la couche infrastructure, on pourra préciser qu’on ne peut ajouter au panier que des produits qui existent (par une référence en base de données).
  • Dans la couche application, on pourra vérifier que l’identifiant est bien un nombre entier.
  • Dans la couche métier, on pourra vérifier que tous les produits dans le panier sont de la même catégorie (parce que pourquoi pas ?)

Dans tous les cas, on pourra déclencher des erreurs de validation pour en afficher un retour à l’utilisateur. Elles seront interceptées au niveau de l’infrastructure, car leurs mises en forme dépendra du canal d’interaction entre l’utilisateur et le logiciel.

On pourra l’ajouter au contrôleur Symfony

1$input = new AjouterUnProduitAuPanierInput();
2$input->identifiantProduit = idProduit;
3try {
4    $useCase($input);
5} catch (ExceptionMetier $e) {
6    return new Response(
7        '<html><body>Le produit n’a pas été ajouté car '.$e->getMessage().'</body></html>'
8    );        
9}

Lire ou écrire ?

Parmi les actions que l’on peut réaliser dans l’application, on peut identifier deux grandes catégories : Lire ou écrire. C’est un risque qui existe sur une application, obtenir des effets de bord qui provoque des modifications lorsqu’on ne fait que de la lecture.

Par exemple, si un produit n’existe plus quand on demande la liste des produits dans le panier, le système peut retirer le produit du panier. Or, cette requête ne devrait pas modifier l’état des données sur le serveur.

Par ailleurs, la façon d’apporter des modifications aux données peut se faire différemment de la façon de manipuler ces données pour affichage. Par exemple, pour afficher les produits au panier, on peut avoir besoin de leurs montants, leurs catégories, leurs tags … Alors que ces données sont superflues quand on veut juste ajouter le produit au panier. C’est donc tout le code qui sera différent pour les opérations en lecture et en écriture.

Pour clarifier cette séparation, on peut s'appuyer sur le modèle CQRS (Command Query Responsibility Segregation)

Attention cependant à bien faire la balance entre le coût et le bénéfice, qui se situe essentiellement dans l’optimisation des performances.

Se comprendre !

Une autre tendance qui est amenée par le DDD, est la nécessité d’un vocabulaire unique. L’idée, c’est que les concepts métiers soient partagés par tous les acteurs du projet par les mêmes termes. Nous l’avons utilisé dans les exemples plus haut.

Au final, cette pratique dissipe beaucoup de malentendus, et elle rend le code beaucoup plus accessible, y compris à des membres de l’équipe qui ne sont pas des profils techniques.

Cette technique porte le nom so-british de « Ubiquitous Language »

Dans nos contrées, quand la communication avec le métier se fait en français, il faut statuer si on traduit ou non les termes métier. C’est une décision qui doit être prise au niveau du projet, car les deux solutions ont leurs avantages et leurs inconvénients. getPanier ou getCart ? Utiliser les mêmes termes ou garder la même langue dans le code ?

Pour finir

Nous aurions pu compléter ce tour d’horizon pragmatique par le développement piloté par les tests, ou l’intérêt de mettre en place une API ou de la standardisation de la communication backend / frontend … Les champs ouverts par ces approches et leurs confluences ouvrent la voie à du code toujours plus lisible. Attention cependant à ne pas faire le raccourci que ces approches réduisent la complexité. Schématiquement, celle-ci est étalée sur une plus grande surface (nombre de fichiers) plutôt que empilée en tas (taille des fichiers).

Bref, j’espère vous avoir apporté un aperçu de ce que peuvent être ces outils et techniques qui permettent d’améliorer la qualité d’un programme informatique. Comme vous avez pu le découvrir, il s’agit comme toujours de bien faire la part des choses entre le bénéfice et le risque. Avant de prendre une décision d’utiliser telle ou telle technique, identifiez votre besoin, la criticité, la durée de vie de votre projet. Mais aussi d’autres critères comme la variété des développeurs qui devront intervenir sur le projet.

Pour finir, il me semble que le code ainsi produit devient plus compréhensible par des non-développeurs, et permet d’éviter bien des écueils et malentendus ! Partagez nous votre point de vue ou votre expérience sur ce sujet. Ça m’intéresse beaucoup, en particulier si vous n’êtes pas technicien du code et que vous avez réussi à arriver au bout de cet article !

Intéressé·e par le code moderne ?

Découvrez notre expertise DDD !