Uniformiser le code, bonne idée ?

Au cours de ma carrière, j’ai été confronté à plusieurs reprises à des clients et/ou des acteurs internes qui souhaitent assurer un haut niveau d’uniformisation sur le code des applications. Cela se traduit par des conventions de style, de nommage, et parfois même des choix plus impactant comme des frameworks ou des architectures spécifiques. Dans les cas extrêmes, les propos de ces personnes laissaient entendre le refus d’un quelconque écart avec ces règles.

De mon expérience, la motivation derrière cette démarche est toujours la même : permettre à un développeur de facilement passer d’une application à une autre. En effet, les développeurs sont encore aujourd’hui des ressources rares et chères au regard du marché, et la plupart des entreprises n’ont pas les moyens d’attirer et d’en employer un grand nombre. Il s’agit donc ici pour l’entreprise de maximiser l’usage des ressources limitées qu’elle a à sa disposition. Cependant, cela implique d’appliquer ces conventions à l’échelle d’un sous-ensemble d’applications, voire même à l’ensemble des applications d’une entreprise.

Si ces motivations me paraissent tout à fait légitimes et raisonnables, je ne suis pas totalement en accord avec la solution envisagée, tout du moins sous ses formes extrêmes. Je vais tenter ici d’apporter ma réflexion sur le sujet. 

Disclaimer

Dans ce billet je vais raisonner à une échelle macro : un ensemble d’applications ou une seule application regroupant différents contextes métiers. Au sein d’un périmètre restreint (un contexte métier), il me semble en effet important de préserver un minimum de cohérence dans le code. À noter également que je ne remets pas en cause ici l’intérêt d’uniformiser la partie infrastructure et configuration de vos systèmes. Si par exemple vous travaillez avec Microsoft Azure ou Amazon AWS, vous voudrez sans doute maintenir une façon cohérente de vous interfacer avec ces plateformes.

Un même SI, mais différents contextes

Comme je l’ai déjà évoqué, le vrai bénéfice d’uniformiser le code est de permettre au développeur de rester en terrain connu. Il peut rapidement trouver ses marques comme la structure technique de l’application est déjà connue. Le plus gros de l’effort se focalise sur la compréhension de l’application, de son métier. 

Hors, si l’on s’intéresse aux différents métiers d’une entreprise / d’une application, on se rend compte qu’ils ne sont pas homogènes en termes de valeur produite, de complexité, d’usage, de volumétrie des données, et même pourquoi pas de charge de travail. Certains métiers vont également être dépendants d’autres métiers pour produire de la valeur. Un outil comme les Wardley Map permet aujourd’hui de bien mettre en lumière ces dépendances, mais aussi les différences de maturité et de spécificité business des composants d’un SI. 

La sphère DDD nous propose une ségrégation par Bounded Context pour structurer une application. En fonction du rôle (Core, Support, Generic) du domain associé, les solutions techniques proposées ne sont pas les mêmes (cf: Domain-Driven Design Distilled, Types of subdomains, p.46) :

  • Core : métier très spécifique à l’entreprise, code avec des efforts de modélisation et développement importants.
  • Support : métier plus simple et supportant le Core, code avec des efforts moindres.
  • Generic : intégration et usage de systèmes tiers ou outsourcés.

De mon expérience, et de façon grossière, une application business s’articule souvent de cette manière : un métier complexe, dépendant de métiers périphériques (avec souvent des métiers de “paramétrage”), et qui vient parfois interagir avec des systèmes tiers, par exemple un CRM. 

L’usage de conventions de code uniformisées peut faire sens dans le cas des supporting domains. Une simple architecture de type CRUD peut par exemple se révéler suffisante pour répondre à ces besoins. Cependant cela me semble inadapté aux core domain (par les contraintes métiers fortes) ainsi qu’aux generic domain (contraint par les systèmes tiers). Porter l’uniformisation du code à ces contextes augmente la complexité et fait tendre vers des solutions sous optimales. 

Nos limites cognitives

Nous sommes tous contraints par les limites de nos capacités cognitives, ce qui correspond à l’ensemble des informations que l’on est capable de traiter à un instant donné. Il faut garder en tête que toutes les applications ne représentent pas la même charge cognitive pour un développeur, et que plus il a d’applications à gérer, plus sa charge cognitive est importante. Uniformiser le code permet dans une certaine mesure de réduire cette charge et d’accélérer la transition vers une autre application. 

Mais même avec cela, un trop grand nombre d’applications reste problématique :

  • Trop d’informations à garder en tête pour être réellement efficace.
  • Un temps et de l’énergie conséquents perdus lors de nombreux “context switching”.

Pour lutter efficacement contre cette limite, on peut envisager d’autres stratégies qui visent à limiter le nombre d’applications dont un développeur peut être responsable. 

Si l’on en revient aux Wardley Maps, on peut déjà simplement se demander : “À quoi sert cette application ? Est-elle vraiment essentielle pour mon business ?” Posez-vous sérieusement ces questions sans les évacuer d’un revers de manche. Simplifiez autant que possible, car nous avons tendance à surcomplexifier les systèmes que nous produisons, et aussi à apporter des solutions à des problèmes qui ne devraient pas exister.

Une autre stratégie (peut-être moins recommandable) est d’accepter que certaines applications ne soient plus maintenues par quiconque pendant un temps. Bien entendu, vous ne pouvez pas faire ça avec toutes vos applications, ici ce sont les contextes generic et supporting qui peuvent être concernés. Il est tout de même important d’identifier la fréquence des changements ainsi que les futures évolutions nécessaires avant de laisser vivre une application.

Parfois, le besoin d’uniformiser le code est exprimé par les développeurs eux-mêmes. De mon expérience, cette demande vient toujours de profils transverses à qui on demande d’intervenir sur plusieurs projets en même temps. Cela peut parfois mettre en lumière un problème d’organisation (pourquoi a-t-il besoin de travailler sur tous ces projets ?). Mais il reste important d’écouter ce type de retour et d’évaluer si une action d’uniformisation est nécessaire.

Innovations et gestion des risques

Lorsque l’on conçoit un système, il est rare de trouver immédiatement la bonne solution, le bon design. C’est particulièrement vrai dans le logiciel où le code change, les fonctionnalités évoluent. Il est donc très difficile d’anticiper les besoins futurs, et tenter d’anticiper n’est pas toujours souhaitable, car celà nous mène régulièrement à des optimisations prématurées et de l’over-engineering. 

Pour cela, il est important de rester ouvert aux changements et aux propositions d’évolution. L’amélioration des pratiques passe alors nécessairement par une phase d’expérimentation et d’exploration.

Quand une nouvelle contrainte apparaît, il faut donc comprendre ses impacts sur la solution actuelle et identifier de potentielles évolutions à apporter pour mieux y répondre. Cependant, si l’on souhaite conserver un code toujours uniformisé, deux scénarios s’offrent à nous :

  • La quantité de code à modifier est trop importante, et le changement n’a pas lieu, on continue donc à travailler sur solution sous-optimale et l’on accumule de la dette.
  • On se lance dans une évolution “big-bang”, sans savoir si la solution envisagée est la bonne, ni même si elle va aboutir. Là aussi, la méthode est sous-optimale puisqu’il faut investir beaucoup de temps et d’énergie avant d’espérer un quelconque résultat. Sans compter l’introduction de potentiels bugs dans des fonctionnalités qui ne nécessitent aucune évolution et qui fonctionnent parfaitement, ce qui est très frustrant pour ses utilisateurs.

Si les standards peuvent évoluer, il est donc plus pertinent de d’abord les tester sur un périmètre limité, et ainsi rapidement obtenir des feedbacks. Si les changements ne sont pas pertinents, alors le temps et l’énergie engagés auront été limités et il est simple de faire machine arrière. Si ceux-ci se montrent pertinents, on peut alors les généraliser de façon opportuniste, progressivement au fils des développements afin de mieux gérer les risques liés à ces évolutions.

Motivation des équipes

Outre le fait de ne pas rester coincé dans une solution inadaptée, les livres Accelerate (Allow teams to choose their own tools, p.66) et Team Topologies (Monolithic thinking (Standardization), p.114) soulignent également l’aspect humain; ces phases d’expérimentation et d’exploration tendent à augmenter l’engagement et la motivation des développeurs. 

J’y suis pour ma part très sensible, et un entretien avec un potentiel client où celui-ci insiste trop sur l’importance de coder selon ses standards est pour moi un mauvais signal. Ceci parce que le message que j’en retiens est : “Tu devras subir des choix passés, que tu n’as pas pris, et ce sans aucune possibilité de t’en extraire, même s’ils s’avèrent mauvais aujourd’hui. » Bref, pas très motivant. J’ai déjà refusé des missions pour cette raison.

Pour conclure

Vous l’aurez compris, je ne suis pas un grand défenseur de l’uniformisation du code à l’échelle d’une entreprise. Parce que les avantages que l’on peut en tirer me semblent finalement assez limités (limites cognitives, innovations, motivations), et aussi parce que cette approche a vocation à soigner les symptômes plus que les causes du problème (organisation, complexité du système). Comme je l’ai déjà évoqué, cette pratique peut faire sens pour des métiers « satellites » à faible complexité et sur lesquels les efforts de développements seront limités. Sur ce sujet (comme pour beaucoup d’autres), tout est question d’équilibre, il faut constamment veiller à ne pas s’enfermer dans un extrême.

Uniformiser le code, bonne idée ?

Do(n’t) repeat yourself

Un biais cognitif et un usage erroné

Les développeurs aiment bien les acronymes pour énoncer des “bonnes pratiques” (KISS, DRY, SOLID, etc…). Souvent, l’idée véhiculée par ceux-ci est très simple à appréhender.
Cependant, nous souffrons d’un biais cognitif énorme: plus une information est simple à intégrer, moins elle est remise en question / challengée. Et celle-ci est encore mieux intégrée si elle ne va pas en contradiction avec vos croyances.

DRY en est l’exemple parfait: 

  • L’idée sous-jacente est simple à comprendre : si on doit appliquer un changement, on veut l’appliquer à un seul endroit.
  • DRY est assez connu et fréquemment énoncé pour être consciemment ou non étiqueté comme étant une bonne pratique (et elle l’est).
  • Sa mise en application est simple et ne nécessite pas, a priori, d’effort intellectuel particulier : mutualiser la moindre ligne de code dupliquée ou les concepts portant le même nom.

Pourtant, il est fréquent d’observer ceci : en travaillant sur une base de code, nous sommes amenés à gérer des cas métiers de plus en plus variés. Pour ce faire, on applique de plus en plus de conditions pour tester différents cas sur une même structure de données. Ceci peut être un signe qu’il existe un problème d’abstraction. 

Le premier enjeu du DRY est une meilleure gestion de la complexité, pourtant en l’appliquant de manière aussi basique/dogmatique, on observe une augmentation de la complexité.

Don’t Repeat Yourself, définition

Si on regarde plus en détail le concept originel : The DRY principle is stated as « Every piece of knowledge must have a single, unambiguous, authoritative representation within a system »  (https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

Ici, c’est la notion de “peace of knowledge” qui est la plus importante. On peut représenter à un instant T des concepts différents qui peuvent évoluer de manière indépendante dans le temps. Utiliser le même bout de code pour les représenter revient d’une part à les coupler : si un concept doit évoluer, alors il va aussi falloir agir sur l’autre qui est lié. Et d’autre part à créer une ambiguïté entre ces concepts qui rend la compréhension et la maintenance du code plus complexes.

Plusieurs niveaux de lecture

Le même code, mais pas le même usage métier

On peut écrire deux bouts de code identiques, mais qui ne représentent pas la même chose conceptuellement. Pour les identifier, on veut surtout chercher les raisons pour lesquelles ces bouts de code sont appelés.

Le même nom, mais pas le même concept métier

On peut avoir plusieurs concepts portant le même nom, mais qui n’appartiennent pas au même contexte: ils ne représentent pas la même chose. Des processus métier différents, des informations différentes sont des bons signaux pour dire que ce sont des concepts distincts. Pour cela, des ateliers comme l’event storming, sont également de bons outils pour les identifier, et les patterns stratégiques du DDD une bonne approche pour les ségréguer.

Le même nom, mais pas le même usage

Dans un même contexte métier, il est encore possible de représenter de différentes manières un même concept. Un exemple typique serait une simple web API qui expose, persiste de l’information, et y applique de la logique métier : on peut avoir un modèle dédié à chacun de ces trois rôles. C’est d’ailleurs la principale motivation derrière des architectures comme l’architecture en couche ou encore une architecture hexagonale : ségréguer par usage technique. Des architectures comme CQRS vont encore plus loin en proposant un modèle d’écriture et des modèles de lecture dédiés. Un même concept peut alors être représenté de plusieurs façons en fonction du cas d’usage.

Mon heuristique

Si dans un bout de code je retrouve les symptômes énoncés précédemment (beaucoup de if sur un état); alors mon heuristique est le suivant : je duplique le code qui pose problème et ensuite je supprime les conditions pour faire émerger deux cas distincts.

Quand j’ajoute un nouveau cas, si je le peux, j’évite de prendre une décision tout de suite, car je manque surement de connaissance et de feedbacks sur mon design. Dans ce cas, je duplique l’existant et je l’adapte. Si je me retrouve plus tard à devoir faire des modifications à deux endroits, alors il y a peut-être une opportunité pour mutualiser du code.

Les bénéfices du do(n’t) repeat yourself

En ne systématisant pas la mutualisation du code, on en augmente sa quantité (plus de classes, de méthodes), mais on réduit sa complexité. Un bout de code est utilisé idéalement dans un seul cas métier. Il est donc beaucoup plus simple à appréhender d’un point de vue cognitif, et donc plus simple à modifier puisqu’il ne faut pas se soucier d’autres cas en même temps. Cela réduit également le nombre d’effets de bords et donc de bugs potentiels.

Do(n’t) repeat yourself

Le piège des tests unitaires

Voilà maintenant plus de 5 ans que j’applique une approche TDD (test-driven development) sur l’ensemble des projets sur lesquels j’interviens. Si j’utilise toujours cette méthode, c’est parce que la présence de tests me donne confiance dans le code que j’écris : 

  • Je m’assure qu’il fait bien ce que je souhaite.
  • J’améliore constamment son design par du refactoring.
  • Les tests mettent en lumière la très grande majorité des régressions que je peux introduire lors d’un refactoring ou d’une évolution.
  • Je réduis ma charge cognitive et me focalise sur le cas métier que je suis entrain de traiter (les tests s’occupent de vérifier les autres cas pour moi).

Modifier mon code est donc une capacité permise grâce aux tests et que je souhaite conserver tout au long d’un projet : je peux améliorer son design pour le garder constamment adapté aux problèmes métier que je veux résoudre.

Avec le temps, je réalise qu’écrire des tests est une discipline difficile et que certaines pratiques peuvent être dommageables. Les tests unitaires “figent” le code et m’empêchent de le modifier facilement, ce qui peut sembler paradoxal puisqu’ils sont censés au contraire me le permettre.

Dans cet article, je vais essayer de mettre en lumière les raisons de cette dérive. Attention, mon propos ne sera pas focalisé sur la pratique du TDD mais sur les tests unitaires (TU) et la manière dont ils influencent notre capacité à modifier du code.

Écris une classe, écris une classe de tests

À chaque classe, chaque méthode son ou ses tests : C’est une définition du TU qui semble très répandue, je la rencontre beaucoup au cours de discussions avec d’autres développeurs, sur twitter, dans des articles de blog, etc. C’est même de cette manière que l’on m’a initié au TDD.

Il est vrai que c’est une façon simple d’écrire un test. Le périmètre que l’on souhaite tester est petit, avec un nombre de dépendances normalement raisonnable. C’est une approche vers laquelle on peut facilement se tourner lorsque l’on n’est pas à l’aise avec la rédaction de TU.

Cette pratique est souvent associée à l’injection de dépendance pour fonctionner. C’est à ce moment-là que l’on commence à introduire des mocks, on peut ainsi spécifier le comportement d’une dépendance sans dépendre de sa véritable implémentation. Notre classe/méthode reste donc bien isolée du reste du système lors du test. 

Mon point doit peut-être vous déranger : pourquoi vouloir absolument tester en isolement une classe qui de toute façon dépend d’autres classes ? Le comportement de la dépendance correspondra-t-il à celui que j’ai spécifié avec mon mock ?

Certains vous répondront qu’il vous faut également des tests d’intégrations. Afin de vérifier que les différents éléments du système interagissent de la bonne manière, et que le logiciel retourne le résultat attendu. 

Vous avez déjà perdu

Si vous adoptez cette stratégie, vous allez surement souffrir, ou souffrez déjà, d’une forte adhérence entre votre code et vos tests. En effet, il existe une contrainte forte pour chaque élément de votre système : des tests.

Cela signifie qu’à chaque signature de méthode que l’on veut changer, en plus du code l’appelant, il va falloir corriger les tests et les mocks qui lui sont associés. Nous avons donc perdu notre capacité à facilement modifier/refactorer notre code. J’ai encore le souvenir (douloureux) de journées complètes à “réparer les tests” suite à des modifications.

Ajoutez à cela un biais dont nous souffrons tous : celui des coûts irrécupérables. Alors qu’un test ne fait peut-être plus sens, nous avons tendance (consciemment ou non) à vouloir le conserver et le modifier, ceci uniquement parce qu’il est déjà écrit.

Vous l’avez compris, aligner de manière aussi systématique ses tests avec son implémentation génère un couplage important contre lequel vous luttez à chaque modification que vous souhaitez apporter.

Du coup, comment fait-on ?

Les TU impliquent obligatoirement un couplage avec le code. Même s’il existe des techniques pour le limiter, la première question qu’il faut se poser est : à quoi veut-on se coupler ?

De manière caricaturale, si vous travaillez sur des logiciels business, ce que l’on attend de vous est de développer des use cases. Ceux-ci sont des comportements que l’on attend de l’application, et il fait sens de vouloir les tester. Ces use cases peuvent être implémentés en une seule classe, ou en plusieurs. Ce sont des choix de design qui vous reviennent, mais aussi des détails d’implémentation que l’on veut pouvoir facilement changer et qu’un observateur externe du système doit ignorer.

Clairement, la stratégie que l’on vient d’explorer s’attache beaucoup à ces détails d’implémentation. Unitaire ne doit donc pas définir la taille de la portion de code que l’on veut tester. 

Je vous propose maintenant la définition que j’ai en tête quand je parle de TU: un test que l’on peut exécuter en isolation et dont le comportement est répétable et constant. 

Son résultat ne dépend donc pas du résultat d’autres tests ni de l’état de dépendances externes au système (appel à une web API, à une base de données). Notez que je ne définis pas la taille du périmètre testé.

Des tests de comportement

Aujourd’hui, j’adopte autant que possible une stratégie qui me permet de conserver ma capacité à modifier le code.

J’écris mes tests de sorte qu’ils dépendent uniquement des contrats entre mon système et le monde extérieur (endpoint REST, base de données, bus de données, etc.). Tout le reste est une boîte noire inaccessible. 

Ainsi, un test se présente typiquement de la façon suivante : 

  1. Je définis l’état du système (ex: données en BDD)
  2. Je lance une action métier via une API publique (ex: endpoint REST)
  3. Je vérifie le nouvel état de mon système (ex: données en BDD) et les éventuelles interactions avec le monde extérieur (ex: publication d’un message sur un bus)

test-strat1

Pour écrire ces tests et les garder indépendants, j’utilise des implémentations “in memory” de mes dépendances externes (typiquement la base de données). Je m’assure également que ces implémentations ont des comportements identiques à celles que j’utilise réellement en production. Pour cela, j’écris quelques tests d’intégrations paramétrés qui testent uniquement les accès au monde extérieur.

test-strat2

Cette approche peut paraître extrême, mais elle répond au problème que je souhaite adresser dans cet article.

Je dois tout de même lui reconnaître certaines faiblesses : 

  • Des boucles de feedbacks plus lentes.
  • Des erreurs parfois plus dures à analyser.
  • Il reste un couplage à certaines couches applicatives.

Créer des abstractions

Il est tout de même nécessaire d’aller un peu plus loin pour réduire l’adhérence avec les tests. En effet, si nous avons réduit aux interfaces publiques la surface à laquelle nous sommes couplés, celles-ci deviennent encore plus critiques en cas de modification. 

Par expérience, un bon moyen de mesurer le couplage à un contrat est de compter le nombre d’endroits où celui-ci est instancié.

builders

Builders, générateurs ou encore données statiques, quel que soit la ou les technique(s) utilisée(s), l’objectif reste toujours le même : isoler la création d’une donnée, d’un service, de l’application, de sorte que si sa structure change, il ne faille appliquer ce changement qu’à un seul endroit dans les tests. 

Un autre bénéfice de ces méthodes est qu’elles rendent les tests plus clairs, puisque vous n’avez à spécifier que les données qui font sens pour le scénario. Avec le temps, vous arriverez sans doute à faire émerger un DSL pour vos tests, l’ajout de nouveaux use cases en sera facilité voir presque trivial.

test-strat3

Pour conclure

Il m’est arrivé plusieurs fois de rencontrer des gens qui ont tenté de mettre en place des tests unitaires ou du TDD sur leur projet, et qui y ont finalement renoncé après quelque temps parce que “ça ne marche pas”. Je pense que le problème que j’ai évoqué au cours de cet article est la principale raison de ces abandons.

Attention également aux dogmes ! Si je me montre critique envers les tests unitaires “petite maille”, ceux-ci peuvent se révéler utiles et parfois plus simples qu’un test “boîte noire”. De la même manière, il est parfois plus simple d’utiliser un mock plutôt que de définir l’état du système dans sa globalité. Il est ici question de compromis, de choix qui doivent être faits en connaissance de cause.

Si cet article vous a plu, je vous recommande de regarder la conférence DevTernity 2017: Ian Cooper – TDD, Where Did It All Go Wrong.

 

Edit 1 : Je vous partage également cet article que l’on m’a montré en réaction à ce post et qui explique sans doute mieux que moi le point que je souhaitais traiter ici.

Edit 2 : Deux autres remarques m’ont été faites :

  • Certains tests sont déjà naturellement isolés (domaine, hexagone) et ne dépendent pas de détails d’implémentations. Ceux-ci sont aussi viables que la stratégie que je propose dans cet article.
  • Si les tests « petite maille » ne sont pas idéals sur le long terme, ils peuvent être très utiles comme « échaffaudage » pour implémenter progressivement un scénario plus vaste. À condition de les supprimer une fois les conditions d’un test « grosse maille » remplies.
Le piège des tests unitaires

Un code métier pur

Il y a quelques jours, au cours d’une discussion, on m’a demandé quelles sont les pratiques que je pousse dans une équipe dans le but d’améliorer la qualité de code. Bon nombre de pratiques comme TDD, clean code ou encore DDD et ses bounded-contexts ayant déjà été cités, j’ai donc répondu : un code métier pur, parfois appelé functional core.

Dans cet article, je pars du principe que vous faite une distinction et séparation forte entre le code métier qui répond à une logique business, et le code infra qui répond aux problématiques techniques.

Quels intérêts ?

Un code que l’on peut qualifier de pur a deux caractéristiques : 

  • Celui-ci retourne toujours le même résultat pour les mêmes entrées. Il ne dépend donc d’aucun état interne ni d’appels à des dépendances (base de données, heure système, etc.)
  • Il ne modifie aucun état visible du système.

Les raisons pour lesquelles je pousse ce genre de pratiques sont extrêmement simples. Il m’est très facile de raisonner sur ce code puisque son comportement est à la fois prédictible et répétable. 

Il est également très simple de rédiger des tests pour ce genre de code. Vous pouvez donc décrire tous vos cas métiers sous cette forme : “mon système est dans cet état, je lance cette action, alors j’obtiens ce résultat”.

Par exemple, un scénario pour la réservation d’un parking : 

  • J’ai renseigné mes dates et heures d’arrivée et de départ .
  • Je valide ma réservation.
  • Ma réservation est acceptée pour les dates .

Si l’on peut parfois considérer les problèmes de charge comme inhérents au métier, on a tout de même envie de les traiter comme des problématiques techniques. Gérer l’accès à un état partagé sur lequel on souhaite écrire se révèle vite complexe (usage de lock, de transactions par exemple) et empêche un code scaler. Nous ne voulons donc pas polluer de la logique métier avec ce genre de problématiques : garder le code pur est une façon simple de s’en assurer.

La raison d’être d’un logiciel 

Cependant, si nous écrivons des logiciels, c’est souvent pour produire ce que nous qualifions jusqu’à maintenant d’effet de bords : écrire en base de données, envoyer un mail, une notification, etc. Nous devons donc être capable de passer d’un code pur à impure et inversement. 

Une façon (peut-être simpliste) de voir un logiciel est une succession de transformations de données. Je veux lire une donnée sur mon disque dur (imprédictible), puis la transformer (prédictible) et enfin écrire le résultat sur mon disque (imprédictible). 

Comment faire vivre les deux ?

Nous avons vu jusqu’ici qu’il doit y avoir une distinction claire entre, le code métier que l’on veut pure, et le code infra qui lui est nécessairement impure puisque sa responsabilité est de traiter avec des appels réseaux et système. 

Pour faire cohabiter ces deux mondes, il nous faut donc un bout de code dont la seule responsabilité est : 

  1. De récupérer les données nécessaires à une opération métier.
  2. Appeler le code métier.
  3. Envoyer le résultat à la couche d’infrastructure.

Répondre à cette problématique de séparation métier/infra est la principale motivation derrière l’architecture hexagonale. Dans cette architecture, nos services portent cette responsabilité :

pure1

Avec une architecture CQRS ou CQRS/ES, ce rôle est porté par le commandHandler.

pure2

Notez que la structure du code reste inchangée, seuls les types changent.

Ce pattern demande une certaine rigueur de la part des développeurs, il est en effet facile d’introduire des effets de bords dans le code métier. Pour cette raison la stratégie adoptée par Haskell consiste à encapsuler les effets de bord dans des IO monade. 

Je ne vais pas m’aventurer ici à définir ce qu’est une monade, mais si vous n’êtes pas familier avec ce concept, voici une image très grossière : Une monade est comme une boîte contenant de la donnée, pour manipuler cette donnée, vous devez fournir à la monade la fonction à appliquer. Une liste est par exemple une monade, l’IO monade en Haskell représente un effet de bord.

Dans cet exemple, j’ouvre le fichier input.txt, j’applique la fonction toUpperString puis j’écris le résultat dans le fichier output.txt. J’ai fait l’effort ici de décomposer les fonctions afin de voir les signatures. 

pure3

La transition du monde de l’IO vers du code pur se fait grâce à une fonction appelée fmap, ici appelée via l’opérateur <&>. fmap prend une fonction pure et l’applique un contenu d’une IO pour produire une nouvelle IO. On obtient ici un IO Uppercase.

Enfin, pour écrire le résultat, on applique la fonction writeOutput via la méthode bind (opérateur >>=). bind nous permet d’appliquer une fonction retournant une IO au contenu d’une IO.

Out of the Tar Pit

Si cet article vu a plu et que vous souhaitez approfondir le sujet, je vous encourage à lire le papier Out of the Tar Pit qui traite de la complexité logiciel, et qui propose un découpage similaire du code. J’ai découvert ce papier grâce à un talk explicatif de Romeu Moura.

Un code métier pur

À la découverte du property based testing

Je suis un développeur convaincu par les bénéfices du TDD, je l’applique au quotidien sur les projets que me confient mes clients. Cela me permet de rapidement valider que mon code a bien le comportement attendu, de le “documenter” et décrivant un cas d’usage et de m’assurer par la suite que je n’introduis aucune régression si je modifie le code testé.

Je fais tout ceci en sachant que je choisis des cas de test qui me semblent représentatifs de l’usage de la fonction, on parle parfois d’Example Based Tests. Si cette méthode est souvent suffisante, il m’arrive parfois de me poser les questions suivantes : Puis-je être sûr que ma fonction est correctement développée si je ne suis pas capable d’identifier un cas qui est représentatif ? Ai-je bien identifié tous les cas limites ?

C’est là qu’il devient intéressant de se pencher sur le property based testing.

C’est quoi le Property Based Testing (PBT) ?

L’idée est simple : identifier et tester des invariants. Comprenez quelque chose qui sera toujours vrai, quelles que soient les données que vous fournissez à votre algorithme.

Pour cela, il faut utiliser un framework qui va générer des données aléatoires et vérifier si l’invariant reste vrai. À chaque exécution de votre suite de tests, celui-ci va tester différentes combinaisons (généralement une centaine). Il est important de noter qu’un test de PBT en succès ne signifie pas que l’implémentation est correcte, il veut juste dire que le framework n’a pas su mettre en défaut l’implémentation. Il est tout à fait possible que celui-ci trouve un cas limite après plusieurs heures, jours, semaines, mois…

Ok, et si un test échoue ?

Si le framework arrive finalement à trouver un cas limite, il existe trois possibilités :

  • le code de production n’est pas correct
  • la façon dont l’invariant est testé n’est pas correcte
  • la compréhension et définition de l’invariant ne sont pas correctes

Il est important d’avoir cette réflexion dès qu’un cas est identifié. Quoi qu’il en soit, le framework est capable de vous donner les données utilisées pour mettre à mal votre code, vous pouvez donc facilement écrire un TU classique pour reproduire le cas.

Un bon framework de PBT est capable de faire du shrinking. Une fois le cas limite identifié, celui-ci va travailler sur les données utilisées pour essayer de les simplifier au maximum tout en reproduisant l’erreur. Ceci nous facilite l’effort d’analyse : imaginez une fonction qui prend une liste en argument, est-ce ma liste de 250 éléments ou juste un élément qui plante mon code ? S’il s’agit d’un élément, le shrinking peut l’isoler.

C’est quoi un invariant ? Un exemple ?

C’est à la fois tout l’intérêt de cette méthode de test, mais aussi toute sa difficulté. Il faut être capable de raisonner sur le métier de son application pour pouvoir en faire émerger des règles.

Un des premiers exemples que l’on peut rencontrer est celui de l’addition. Celle-ci a trois propriétés :

  • L’identité : x + 0 = x avec 0 comme élément neutre
  • L’associativité : (a + b) + c = a + (b + c)
  • La commutativité : a + b = b + a

Vous retrouvez également ces propriétés avec la multiplication, seul l’élément neutre change. Ces exemples sont très mathématiques (et peu intéressants), mais ce n’est pas le cas de toutes les propriétés, celles-ci peuvent prendre diverses formes.

Petite appartée, les exemples qui suivent sont écrits en F# avec FsCheck. J’ai volontairement  choisi un “mauvais” exemple métier puisqu’il est envisageable de tester tous les cas de manière unitaires, mais il me paraît très adapté pour illustrer ce qu’est un invariant.

Pour cet exemple, j’ai décidé d’écrire un petit programme qui doit me dire quelle est la main gagnante entre deux mains de deux cartes. Il s’agit d’une version simplifiée du Poker, si vous ne les connaissez pas, voici les règles :

  • si les deux mains sont équivalentes, alors il y a égalité
  • une paire gagne sur une main mixte (main avec deux cartes différentes)
  • s’il y a deux paires, la paire avec la meilleure carte gagne
  • s’il y a deux mains mixtes :
    • on compare la carte la plus forte de chaque main
    • si les cartes les plus fortes sont identiques, on compare les cartes les moins fortes
  • l’As est la meilleure carte et le deux la moins bonne

Je vous laisse quelques secondes pour trouver des invariants…

Vous avez trouvé ? Il s’agit tout simplement de la liste de règles que je viens d’énoncer : celles-ci sont toujours vraies.

Ok, prenons la première règle : “si deux mains sont équivalentes, alors il y a égalité”. Pour cela, je laisse le framework me générer deux cartes aléatoires (seule la valeur de la carte importe ici) qui constituent les deux mains :

draw

On peut aussi tester qu’une paire est toujours meilleure qu’une main mixte. Dans ce cas, on doit s’assurer que les cartes générées par le framework sont différentes. Pour cela, il est possible de poser des conditions qui, si elles sont respectées, permettent l’exécution du cas de test :

pair

Je ne vais pas détailler l’ensemble des cas de test sur cet article, vous pouvez tenter de le refaire de votre côté. Vous pourrez trouver une solution possible sur mon github.

Certaines imprécisions

Contrairement à un TU, il n’est pas toujours possible de spécifier le résultat exact que l’on attend à l’issue d’un test de PBT. Pour répondre à ceci, j’aime beaucoup la définition proposée par Jessica Kerr.

Un test de PBT défini un cadre métier dont on ne doit pas sortir :

L’idée est d’être moins spécifique sur le résultat, l’important est de s’assurer que les impératifs métiers sont validés. Cela présente l’avantage de laisser une plus grande liberté dans l’implémentation puisque son couplage avec le test est moins important.

Si l’on souhaite tout de même tester un résultat de manière exacte, alors il faudra revenir à un test unitaire avec un résultat hard-codé.

Pour conclure

On peut trouver certains inconvénients au PBT, comme des temps d’exécution un peu plus longs que des tests unitaires classiques, ou encore une plus grande difficulté à écrire ces tests.

Mais vous l’aurez compris, le PBT améliore notre compréhension de l’application puisqu’il pousse à raisonner à des niveaux d’abstraction plus élevés que ce que nous incite à faire des tests unitaires classiques : “une paire est meilleure qu’une main mixte” est à un niveau d’abstraction supérieur à “une paire de 5 est meilleure que la main avec le 8 et le roi”.

Enfin, parce que le code est validé par un grand nombre de cas différents, le PBT améliore également la qualité de notre code ainsi que la confiance que nous avons dans celui-ci.


Si après la lecture de cet article le PBT vous intéresse, vous pouvez également regarder ce talk de Jessica Kerr ou encore celui de Romeu Mourra qui sont pour moi des références sur ce sujet.

À la découverte du property based testing

Vous n’êtes pas maître de votre code

J’ai récemment pu participer à un atelier animé par Romeu Mourra lors des NCrafts. Pas de technique ici, le but était de mettre en lumière des problèmes d’ordres systémiques. Pour cela, nous avons fait un Kebab Kata sous forme d’itérations aux-cours desquelles Romeu jouait le rôle du client, puis également de l’architecte. Son but était de nous faire échouer en usant de différents comportements toxiques que l’on retrouve fréquemment dans de vraies missions.

Objectif rempli

L’atelier s’est déroulé de la façon suivante :

  • à chaque itération le client donne un périmètre et un budget (du temps) pour le réaliser.
  • pendant les itérations, le client répond aux sollicitations des équipes et va voir spontanément en tentant de les influencer.
  • à la fin de chaque itération, le client attend une démonstration.
  • pendant qu’une équipe réalise sa démonstration, les autres équipes ont le droit de “tricher” en continuant à coder.
  • une courte revue de code auto-organisée avec les autres équipes est mise en place après les démonstrations, il est interdit de coder durant cette période.
  • au bout de trois sprints, nous sommes “virés” puis recrutés en tant que nouvelle équipe. Il nous faut alors réaliser un audit et énoncer des actions à prendre sur le code.
  • un architecte, appuyé par le client, apporte alors des directives de conception en parallèle de notre audit.

Comme prévu, nous avons tous échoué : à l’issue de l’atelier, toutes les équipes ont considéré leur code comme étant du legacy.

Des responsabilités partagées

Il en ressort clairement que les développeurs ne sont pas les seuls responsables de la qualité finale du code. De façon synthétique et non exhaustive :

La contrainte la plus évidente est le temps sur un périmètre donné : les délais sont très courts et incitent à prendre des “raccourcis” comme ne pas utiliser de tests unitaires. Le client n’hésite pas à demander s’ils sont nécessaires. Il écoute les rares équipes qui tentent de négocier les délais mais cela n’aboutit à rien d’autre qu’à un moyen pour les développeurs d’exprimer leur frustration.

Les demandes du client ne sont pas claires ni priorisées : “si vous avez le temps, j’aimerais aussi cette feature”. Ce comportement ne fournit aucune visibilité à l’équipe, elle ne connaît pas la finalité du logiciel, ni le véritable besoin.

Tout comme le droit de “tricher” pendant les démonstrations, ce manque de visibilité incite les développeurs à constamment coder pour rattraper leur retard, ce qui a plusieurs effets pervers :

  • Aucun temps n’est alors accordé à la prise de recul, à la remise en question du code et de sa conception : l’équipe est constamment maintenue occupée au détriment de la qualité.
  • Le client n’a pas besoin d’exercer la moindre forme de management, les développeurs sont livrés à eux mêmes et subissent la situation.
  • L’équipe ne communique pas avec les autres pour échanger sur les solutions possibles. De plus, les temps accordés aux revues de codes sont inutiles car bien trop courts (et désorganisés) pour être constructifs : il n’est pas possible de faire émerger de réels axes d’amélioration

Enfin, les équipes subissent des pressions sur leurs choix techniques. Le client fait part des retours fait par l’équipe front end (dont l’existence n’avait d’ailleurs jamais été évoquée avant !) et des difficultés qu’elle rencontre lors de l’intégration. L’architecte impose, appuyé par le client qui “le paie très chère”, une architecture basée sur le design pattern composit. Il s’avère que cette solution répond bien au problème de conception de ce kata, mais ne reflète pas du tout la façon dont le métier du client peut évoluer, ce qui rend toute évolution encore plus coûteuse.

Prisonniers et gardiens d’un système

Avec tous ces éléments, les développeurs se sentent isolés puisque considérés comme de simples exécutants de décisions qu’ils ne comprennent pas et pour lesquelles ils n’ont pas été consultés. Il n’existe aucune confiance entre l’équipe de développement et ses interlocuteurs.

Romeu décrit le système dans lequel sont pris les développeurs comme étant un panoptique. La majeure partie des comportements observés lors de l’exercice peuvent être associés à trois piliers qu’il a identifiés :

  • le manque / l’absence de communication entre les équipes
  • la bonne visibilité du management sur les équipes
  • l’opacité du management pour les équipes

Une fois pris dans un tel système, les développeurs ont l’impression d’être constamment surveillés et ne se sentent plus libres de leurs manières de travailler. Ils s’imposent alors un mode de fonctionnement qu’ils finissent, à terme, par trouver normal. Malgré des lacunes plus ou moins évidentes de sa part, le système n’est alors plus remis en question.

C’est ainsi que ces mêmes développeurs peuvent se montrer hostiles à l’introduction de nouvelles pratiques comme le TDD ou le pair programming. Parce que cela ne leurs semble pas concevable et qu’ils craignent que le système rejette cela.

Tenter et innover

Bien que fréquents, les comportements évoqués plus haut ne sont pas adoptés pour sciemment nuire au projet. Il est tout de même important de savoir les identifier, les remettre en cause et initier des changements de méthode, de comportement.

Parmi les pratiques à mettre en place, Romeu proposait les suivantes :

  • Le mob programming pour rassembler les gens, les pousser à communiquer, comprendre ce qu’ils développent et pourquoi cela est nécessaire.
  • Supprimer la double contrainte temps / périmètre en appliquant notamment le no estimate. Un comportement qui peut être adopté serait de dire “Ok, je te livrerai uniquement ce qui sera prêt à cette date là” tout en ayant une vision claire des prioritées métier. Ce discours est parfaitement entendable contrairement à ce que l’on a tendance à penser.
  • Ne plus travailler à flux tendu : une équipe de développement est souvent perçue comme une source de coût, encore plus si elle n’est pas occupée. Les managers et clients cherchent donc à constamment les alimenter en tâches. Il est important de dégager du temps pour des activités annexes : refactoring, automatisation, veille technique, etc… Aujourd’hui, de plus en plus d’entreprises ont un jour par semaine dédié à ces activités.

Essayer de convaincre les gens avant de tenter quoi que ce soit est généralement un effort vain. Il ne faut donc pas avoir peur de prendre des initiatives, les résultats sont souvent plus parlant que les débats.

Merci à Ouarzy et Léna pour leurs retours, merci à Romeu pour cet atelier très instructif.

Vous n’êtes pas maître de votre code

La complexité métier

Dans mon précédent article, j’ai évoqué les raisons pour lesquelles il faut s’orienter ou non vers une architecture de type CQRS. Parmi ces raisons, la première que j’ai évoqué était le niveau de complexité du métier : plus le métier est complexe, plus CQRS devient pertinent.

Seulement, comment définir et évaluer la complexité métier de son application ?

La complexité, c’est quoi ?

“Complexité, n.f. : Caractère de ce qui est complexe, qui comporte des éléments divers qu’il est difficile de démêler” : définition proposée par le Larousse.

Cette définition met clairement en évidence une première notion, elle implique de fortes dépendances entre plusieurs éléments.

Chain texture

J’ai récemment pu assister au talk “Out The Tar Pit Vulgarized” de Romeu Moura où il est justement question de complexité logiciel. Il commence par y définir les termes simple et complexe :

  • Simple : Qui n’est pas composé, c’est à dire, qui ne fait l’objet d’aucune dépendance et d’aucune récursivité.
  • Complexe : Qui est composé, c’est à dire, qui fait l’objet de dépendances et / ou de récursivités.

Dans le monde de la finance, les intérêts simples et composés retranscrivent bien ces notions.

Le métier et sa complexité

On peut définir le métier d’une application par l’ensemble des règles fonctionnelles qu’elle doit savoir gérer. C’est la partie essentielle d’un logiciel, la raison pour laquelle il est développé. C’est également ces règles qui permettent d’évaluer la complexité métier d’une application : sont-elles composées ?

Know The Rules-stamp

Cependant il est important de ne pas confondre le métier tel qu’il existe dans la vraie vie avec le métier tel qu’il doit être traité dans l’application. Si vous appliquez le Domain Driven Design, vous allez vouloir expliciter dans votre code le métier et ses règles, notamment au travers de bounded contexts et leurs ubiquitous language respectifs. Cette démarche n’a pas pour but de refléter avec exactitude la réalité, au contraire, elle encourage à utiliser une abstraction adaptée au problème que l’on souhaite résoudre.

J’aime beaucoup cette courte vidéo de Scott Millett qui explique très simplement ce qu’est l’abstraction d’un domain. Dans cet exemple, il montre qu’un plan de métro est une abstraction de la réalité (le réseau) adaptée pour un problème donné : savoir comment se déplacer d’un point A vers un point B.

Une autre forme de complexité

La complexité métier ne reflète pas toujours la complexité du code source : l’usage de langages de programmation, de frameworks ainsi qu’un mauvais design ajoutent un niveau de complexité supplémentaire, la complexité accidentelle.

Pour refaire le lien avec CQRS, l’intérêt est d’éliminer une trop forte complexité dans le code en utilisant des modèles de lectures et d’écritures adaptés aux besoins. Ces modèles sont des abstractions qui ne comportent que les éléments nécessaires à l’exécution d’une fonction, d’une règle métier. Leurs niveaux de compositions sont donc réduits à leurs minimums.

Une autre solution pour se protéger contre cette complexité accidentelle est l’architecture hexagonale.

Conclusion

La complexité métier est donc l’ensemble des règles métier et leurs dépendances. Plus il existe de dépendances entre ces règles, plus le métier peut être considéré comme étant complexe.

Merci à Ouarzy pour ses retours.

La complexité métier

Pourquoi utiliser CQRS et ES ?

Actuellement, j’entend de plus en plus parler de CQRS et CQRS/ES : par mes collègues autour de la machine à café, lors d’entretiens techniques, sur Twitter, les blogs, etc.

musthave

Le principe du Command and Query Responsability Segregation (CQRS) est de séparer modèles d’écriture et modèles de lecture. L’Event Sourcing (ES) quant à lui consiste à sauvegarder des événements au lieu d’entités, pour reconstruire une entité il faut agréger des événements. Exprimés de cette façon, ces concepts semblent plutôt simples à comprendre, mais les aspects techniques peuvent vite les rendre complexes à appréhender et implémenter.

Alors pourquoi choisir de modifier la façon dont nous représentons nos modèles de données ?

Si vous lisez attentivement un livre sur le Domain Driven Design (dont découle CQRS et ES), la réponse que vous obtiendrez sera : tout dépend de votre métier et de vos besoins.

Prendre une décision et décrire un changement

Pour comprendre un des avantages de CQRS, il faut se focaliser sur notre mode d’expression oral.

Imaginons qu’à un instant T je réside à l’adresse A, puis je déménage à l’adresse B. On peut dire qu’à T+1 je réside à l’adresse B.

Ici je représente une entité et mon adresse est une de mes propriétés. Un système de type CRUD (Create Request Update Delete) remplace mon adresse A par une adresse B, ce qui est fondamentalement vrai et simple à comprendre dans cet exemple. Cependant, le CRUD impose de me connaître en tant qu’entité : mon adresse n’est sans doute pas la seule chose qui me caractérise, on peut penser à mon nom, prénom, âge, sexe, taille, poids, etc. On observe une forte complexité accidentelle pour un changement d’état qui est pourtant simple. Mon poids n’a pas d’influence sur le choix de ma nouvelle adresse mais il est connu, et il doit être fourni lors de mon changement d’état.

De plus, avec le CRUD je ne mets pas réellement en avant l’action qui me fait changer d’adresse : mon déménagement. Les deux informations nécessaires pour me faire déménager sont mon identité et ma nouvelle adresse : ceci est ma commande “déménage” dans une architecture CQRS. Ensuite, mon identité et mon adresse actuelle sont sans doute les seuls éléments nécessaires pour prendre la décision de déménager. C’est une description partielle de mon état, mais adaptée à ma prise de décision, ceci est ma query dans une architecture CQRS.

Vu de cette façon, le CQRS semble donc plus proche de la façon dont nous raisonnons naturellement. On peut donc facilement exprimer les gestes métiers issus de l’Ubiquitous Language dans le code. Cette approche s’adapte bien avec une pratique comme le Behavior Driven Development (BDD), une action décrite dans un scénario de test se traduit naturellement par un commande envoyée au système.

Alors CRUD ou CQRS ? Quel est le niveau de complexité de votre métier ? Voici une réponse possible :

Complexité métier / Architecture

Simple Complexe

CRUD

Adapté

Complexité accidentelle

CQRS Sur-qualité

Adapté

La mémoire des actions

Est-il important de savoir quelles actions ont été menées sur votre système ? Cela peut être le cas dans certains métiers comme le e-commerce : ceci permet par exemple de savoir quels articles ont pu être ajoutés au panier puis retirés, et ainsi cibler les suggestions pour un client donné.

Le problème d’un système de persistance par état (utilisation d’entités) est qu’il n’y a pas d’historique des états précédents. En suivant cette logique pour mon déménagement, je sais à l’instant T je réside à l’adresse A. À l’instant T+1 je réside à l’adresse B mais je n’ai aucune trace d’un changement d’adresse.

Pourtant si vous me posez la question lors d’une conversation, je vais être capable de vous dire que je résidais à l’adresse A à l’instant T, et que maintenant en T+1 je réside à l’adresse B parce que j’ai déménagé entre temps. Notre mémoire fonctionne à la façon de l’event sourcing. Je retiens les événements qui me sont arrivés et grâce à eux je peux restituer mes états aux instants T et T+1.

events

Là encore, cette pratique s’adapte bien au BDD. Quand vous définissez l’état de votre système, vous décrivez les événements qui se sont produits.

Pour tester une architecture CQRS/ES avec le BDD, vous ajoutez donc un ensemble d’événements dans votre event store. Puis vous lancez une commande et vous vérifiez ensuite le comportement attendu (levé d’une exception, mise à jour des projections, etc.). Tester ce type d’architecture avec une approche métier est par conséquent très simple avec le langage naturel.

bdd
Un autre avantage de l’event sourcing est qu’il facilite la communication entre plusieurs contextes. Un bounded context peut émettre un événement dans un event bus, tous les bounded contexts qui attendent ce type d’événement le récupéreront et l’appliqueront à leurs propres modèles. Pour autant, il n’est pas nécessaire que ces deux contextes utilisent l’ES, une simple couche d’anti-corruption peut permettre d’interfacer un système de type CRUD.

Gagner en performance et en robustesse

Votre système a-t-il des attentes élevées en terme de performance ? On peut par exemple imaginer un site de billetterie en ligne, à l’annonce d’une date importante, celui-ci risque d’être pris d’assaut par les utilisateurs et nécessitent donc d’être robustes et rapides.

La majorité des systèmes ont un ratio lecture/écriture très déséquilibré, avec un nombre de lectures bien supérieur au nombre d’écritures. Gérer les relations entre plusieurs entités, notamment à l’aide de jointures, peut nécessiter d’importantes ressources et provoquer des latences.

C’est là l’un des autres avantages de CQRS, produire des modèles de lectures dédiées permet des requêtes rapides sans jointure. Chaque vue de votre application ne doit dépendre que d’un seul modèle de lecture, et ainsi effectuer une requête sur une seule table pour obtenir l’ensemble des informations qui lui sont nécessaires.

Pour rendre plus rapidement la main à l’utilisateur suite à l’exécution d’une commande, la mise à jour des modèles de lecture peut se faire de manière asynchrone. Il peut alors être nécessaire de mettre à jour les informations affichées pour assurer la cohérence des données avant que la commande ne soit réellement appliquée au modèles de lectures.

L’event sourcing permet également des gains de performance et de robustesse. Je parle ici du nombre d’opérations menées sur la base d’écriture. Un événement est un fait, il s’est produit et est irrévocable. Chaque événement est indépendant des autres, il n’existe donc aucune forme de relation entre les événements dans la base de données. On ne peut donc qu’écrire des nouveaux événements ou faire des lectures pour générer des agrégats.

Les événements suppriment également un problème inhérent aux modèles de données relationnels : vous allez devoir insérer ou mettre à jour plusieurs objets dans des repositories différents. Une écriture / mise à jour des données peut échouer en cours d’exécution, pour éviter une donnée partiellement enregistré, il faut alors mettre en place des systèmes de contextes. Ces mécanismes sont lourds à mettre en place et à gérer, ils ajoutent également une forte complexité accidentelle. L’ES vous affranchit des problèmes de cohérence des données en cas d’erreur lors de la persistance : l’écriture de votre événement fonctionne ou non.

Pour conclure

Bien que CQRS et CQRS/ES soient les nouvelles architectures “à la mode”, on constate qu’il ne s’agit pas de silver bullets : elles répondent à des problématiques précises. Il est donc important de clairement identifier ses besoins avant de se tourner vers ces architectures. Si vous choisissez de les utiliser, il ne faut pas les craindre : si celles-ci sont plus complexes à appréhender qu’une architecture en couche de type CRUD, les bénéfices compensent le coût initial de mise en place.

Merci à Nadège pour ses retours.

Pourquoi utiliser CQRS et ES ?

Les développeurs et le besoin métier

Développer est une tâche complexe, maintenir et faire évoluer un projet existant l’est aussi.

Une mauvaise qualité de code a de nombreux impacts négatifs : un nombre d’anomalies et de régressions affolantes, des coûts et délais exponentiels à chaque évolution, un manque de performances, voir une solution qui ne répond pas aux besoins. Le tout en sapant progressivement le moral des développeurs qui ont le malheur de travailler dans ces conditions.

Un problème de code

Le mauvais code peut prendre de très nombreuses formes, mais on retrouve souvent certaines caractéristiques :

  • Redondance
  • Faible consistance et absence de norme
  • Forte complexité cyclomatique
  • Fortes dépendances
  • Design chaotique
  • Ne révèle pas les intentions métier

Toutes ces caractéristiques rendent le code extrêmement difficile à lire et à comprendre. Comment déterminer ce que fait le programme en lisant le code ? Comment localiser une fonctionnalité ?

Un autre problème majeur est le turnover parmi les développeurs qui peut générer d’importantes pertes de connaissances s’il est mal anticipé : vous êtes parfaitement incapables de faire un lien clair entre un besoin, une fonctionnalité et son implémentation.

Dès lors, la moindre modification se fait à taton avec son lot de souffrance : effets de bords, incompréhension du code, régressions, etc…

Documenter, spécifier, recommencer

Une solution envisagée est de produire d’importantes quantités de documentation et de spécifications. C’est par exemple le parti pris des projets réalisés en cycle en V. L’idée est d’analyser le besoin et conceptualiser la solution à produire avant les développements.

cycle-en-v
Le premier problème est que plus une erreur est introduite tôt dans ce processus de documentation, plus les documents qui en découlent sont erronés.

  • Le périmètre doit donc être figé, sinon :
    • Les documentations sont très coûteuses à maintenir.
    • Les documentations deviennent rapidement obsolètes.
  • Les spécifications doivent êtres :
    • complètes
    • cohérentes
    • correctes
    • sans ambiguïté
    • réalisables

En principe, les développeurs ne réalisent que la conception détaillée, cette solution comporte plusieurs inconvénients majeurs :

  • L’architecture est imposée aux développeurs, et peut ne pas être adaptée.
  • Les développeurs sont focalisés sur les aspects techniques de l’application.

Le développement logiciel est une activité non-déterministe, par conséquent dans la très grande majorité des cas les développeurs rencontreront ces difficultés : aucune spécification ne peut être parfaite, il faut donc savoir improviser. Étant limités à une vision purement technique du projet, ils ne savent y répondre que par des solutions techniques sans aucun sens. Au fil du projet, ceci pollue de plus en plus le code et génère des deltas qui invalident progressivement les documents de référence. Un code illisible, une spécification qui ne correspond pas : vous avez de nouveau perdu les connaissances sur votre projet.

Le code, la seule vérité

S’il existe une vérité, c’est bien celle du code. Peu importe ce qui est écrit dans votre spécification, votre ordinateur appliquera ce que votre code lui dicte : le code fait foi, il est lui même la spécification la plus détaillée et la plus précise de votre programme.

Alors pourquoi ne pas l’utiliser comme tel ? Pourquoi ne pas s’efforcer à produire du code facilement compréhensible, facilement modifiable ? C’est pourtant ceci qui caractérise un code propre. Si celui-ci est expressif, alors n’importe qui (même une personne qui n’est pas développeur) peut le lire et comprendre les actions réalisées. Il est généralement accompagné d’un ensemble de tests unitaires qui expriment chaque cas géré.

strip-les-specs-cest-du-code-650-finalenglish

On peut voir le métier de développeur de beaucoup de manières différentes, il est souvent comparé à celui d’artisan (software craftsman), je le vois également comme un rôle de traducteur. Quand j’écris du code, je traduis dans un langage compréhensible pour ma machine un besoin qui m’a été exprimé dans un langage qu’elle ne comprend pas.

Mais expliquer quelque chose que l’on ne comprend pas soi-même est insensé. Il est donc primordial que les développeurs comprennent ce qu’ils développent, d’un point de vue technique, mais aussi d’un point de vue métier.

Améliorer la qualité

Pour écrire du code de qualité, il faut faire attention aux comportements, ne pas se contenter de quelque chose qui marche :

“How it is done is as important as getting it done.” Sandro Mancuso

Il est donc nécessaires de maîtriser et appliquer avec rigueur certaines pratiques et principes : TDD, SOLID, SRP, KISS, etc. Lire des livres tels que Clean Code de Robert C. Martin sont une bonne façon de les aborder.

Il faut ensuite travailler sur l’expressivité du code, du design. Est-ce que ma classe représente une notion métier ? Est-ce que ma méthode exprime une action métier ou technique ? Bien entendu, certaines problématiques restent purement techniques, mais elles doivent être les plus discrètes possibles dans le code en étant masquées derrière des interfaces dédiées.

Pour être expressif, encore faut-il savoir quoi exprimer. La meilleure façon est de s’intéresser au métier du logiciel. Discutez, même de façon informelle, avec l’utilisateur final, avec le product owner, avec l’expert métier : N’importe quelle personne pouvant vous aider à comprendre le problème auquel vous apportez une solution.

cameleon

Une fois que vous aurez compris le métier de vos interlocuteurs, vous serez capables d’échanger facilement avec eux, de challenger leurs besoins. Vous pourrez retranscrire les connaissances acquises dans votre code, celui-ci deviendra alors plus compréhensible, ses intentions seront beaucoup plus claires. En cas de doute, vous saurez également vers qui vous tourner pour répondre à vos questions.

Quelques méthodes

Il existe divers pratiques pour améliorer la compréhension métier des développeurs, et ainsi la qualité du code produit.

Adopter un fonctionnement agile est le premier pas. Ces méthodologies permettent de rapprocher développeurs et clients dans le but de faciliter dialogues et feedbacks. Mettre en place ce fonctionnement est un pré-requis à un certain nombre de méthodes de conception et de développement.

Le BDD (Behavior Driven Development) est une pratique intéressante à mettre en place. Il s’agit d’une variante du TDD qui met en avant le langage naturel et les interactions métier au travers de features découpées en scénarios d’utilisation. Idéalement, la rédaction de ces scénarios doit se faire avec un expert métier ou un product owner. Le développeur comprend alors clairement ce qu’il développe, et peut s’appuyer sur les notions, le vocabulaire employé dans ces features pour désigner son code. Cette pratique permet également l’émergence de l’Ubiquitous Language.

Enfin, le Domain-Driven Design. Il a été formalisé pour la première fois par Eric Evans dans son blue book qui présente un ensemble de patterns tactiques et techniques. Ces patterns couvrent l’ensemble du cycle de vie d’un projet : des méthodologies pour comprendre et représenter un métier, des choix d’architecture, de design, etc. L’idée est de produire une architecture qui présente de manière pratique plus que purement exhaustive les différents composants et interactions d’un domaine. Les points de complexité d’un logiciel doivent alors êtres des points de complexité métier et non techniques. L’arrivée de nouvelles pratiques comme l’event storming, ou d’architectures logiciel comme CQRS/ES découlent directement du DDD.

Pour quels résultats

Dans mon équipe actuelle, nous nous efforçons chaque jour d’appliquer ces principes et ces méthodes avec rigueur. Les bénéfices de ce travail se ressentent petit à petit.

La qualité de notre code augmente, le nombre d’anomalies est quasiment nulle. Étant bien découplé, et ainsi ne souffrant pas d’une forte complexité, notre code est également évolutif et peut subir rapidement des modifications qui peuvent être majeures.

Notre code fait foi : en cas d’un doute sur une question métier, le réflexe de tous (même celui du product owner) est de regarder le code. Nos documentations ne servent qu’à formaliser les futurs développements, et dans de rares cas à s’assurer qu’un morceau de code est bien conforme.

Merci à Ouarzy et Nadège pour leurs retours.

Les développeurs et le besoin métier

Le mutation testing

J’ai récemment lu un article de l’oncle Bob Martin, il y expose sa découverte du mutation testing et semble très enthousiaste à ce sujet. J’ai donc décidé d’essayer un outil pour mieux comprendre cette démarche.

Le principe

Aujourd’hui, beaucoup de projets sont réalisés en appliquant le TDD. Développer en appliquant le test first permet d’être sûr que l’on écrit uniquement le code nécessaire pour rendre un test valide.

Cependant, certains reprochent à cette méthode de mettre en évidence la présence de bugs, et non de démontrer l’absence de bug : un test qui échoue montre qu’il y a une anomalie, mais une anomalie peut exister sans qu’il n’y ait de test pour le montrer.

L’idée du mutation testing est de créer des mutations sur le code testé. Un outil analyse le code couvert par les tests puis génère des mutants : Mes tests sont-ils toujours vrais si je modifie cette condition ? Et si je ne fais pas d’appel à cette fonction ? Un mutant peut avoir deux états : mort ou vivant .

Les mutations peuvent prendre diverses formes : la modification d’une limite conditionnelle (< devient <=), l’inversion d’une condition (== devient !=), la suppression d’un appel à une méthode, etc.

Un mutant mort montre qu’au moins un test échoue si l’on modifie le code, on peut donc en déduire que les tests protègent bien le code contre les régressions. Un mutant vivant montre que tous les tests passent malgré une modification du code. Le mutation testing peut ainsi révéler que le code est mal protégé contre les régressions, il peut s’agir d’un problème de design ou alors c’est la qualité des tests qui peut être remise en cause.

Exemple

Pour mon exemple, j’utilise VisualMutator qui s’intègre directement dans visual studio.

Cas initial

Ici, je teste de manière laxiste une simple méthode qui me dit si mon objet Sequence contient un seul élément. Voici une première solution :

mutationtesting1

mutationtesting2Mutations

Après une première session de mutation sur mon code on constate des faiblesses dans mes tests :

mutationtesting3

Le mutant LessThanOrEqual me montre que je peux modifier ma condition tout en gardant mes tests valides. Je le constate bien si j’applique cette modification (< 2 devient <= 2).

mutationtesting4

Je peux ici rejeter la faute à mon dernier test qui fournit une liste de trois objets. Une fois corrigé je peux relancer un test par mutation :

mutationtesting5

mutationtesting6

On constate bien cette fois que la mutation LessThanOrEqual n’est plus vivante. Mais cette fois ci le mutant NotEquality reste vivant, il me manque donc clairement un test.

mutationtesting7

Cette fois ci je constate que mes mutants LessThanOrEqual et NotEquality sont tous les deux tués par mes tests.

mutationtesting8

L’utilité

Cette approche est clairement faite pour tester la robustesse des tests plus que le code en lui même. Elle permet de mettre en évidence les limites de notre jeu de tests, et ainsi la présence de potentielles anomalies non identifiées. En d’autres termes : Est-ce que je peux faire confiance à mes tests ?

Je ne suis donc pas convaincu que le mutation testing apporte une grande plus-value si le TDD est appliqué avec rigueur. J’avoue ne pas avoir su produire de mutant vivant sur un premier exemple écrit de cette manière.

Cette approche est donc beaucoup plus intéressante pour la gestion de legacy. Avant d’y apporter des modifications, mieux vaut écrire des tests pour se protéger contre les régressions. N’importe quel développeur ayant réalisé cet exercice sait qu’il s’agit d’une tâche complexe et qu’il est parfois difficile d’identifier tous les cas gérés. Utiliser le mutation testing peut facilement mettre en évidence ces cas non identifiés.

L’inconvénient

Il faut tout de même avoir conscience que cette méthode se révèle extrêmement coûteuse comparée à de simples tests unitaires. Il faut considérer le temps passé à l’analyse du code, à la génération des mutants, ainsi qu’à l’exécution des tests pour chaque mutant, ce qui peut prendre plusieurs heures sur un projet conséquent.

De manière grossière, imaginons un projet de 200 classes avec en moyenne 5 mutants par classe et un jeu de tests complet qui est exécuté en 30 secondes. On obtient :

200 * 5 * 0.5 = 8h20 (500 minutes)

Les tests utilisant la mutation ne peuvent donc pas être joués de manière systématique comme le sont les TUs. Il est selon moi beaucoup plus intéressant de l’appliquer de manière ponctuelle sur des régions ciblées du code.

Merci à Nadège pour sa relecture.

Le mutation testing