Du DDD (Domain Driven Design) dans mon legacy code

Actualité ekino -

Article paru le :
Du DDD (Domain Driven Design) dans mon legacy code
Article paru le :

Le vendredi 10 novembre 2017 s’est tenue une nouvelle édition du BDX.io où j’ai pu assister à une session de live coding ayant pour intitulé “Du DDD dans mon legacy”. Cet article est l’occasion de revenir sur les pratiques mises en avant lors de cette conférence et un peu plus.

La conférence / Live coding était tenue par Thomas Pierrain et Bruno Boucard.

C’est quoi du Legacy code ? C’est quoi le Domain Driven Design (DDD) ?

Le terme “legacy code” désigne un code qui n’a généralement pas été écrit par soi, qui fait partie du code de production et répondant à des critères de qualité ou de méthodologie qui ne sont plus en vigueur ou en deçà du niveau attendu. Le “DDD” ou “Domain Driven Design” est une manière de penser le développement qui consiste à faire émerger les concepts métiers dans le code de l’application. Une forte collaboration est requise entre les experts métier et les développeurs. Ce terme a été premièrement énoncé par Eric Evans dans son livre nommé ainsi. Nous allons donc découvrir comment nous pouvons retravailler du code “legacy” en utilisant le mouvement de pensée induit par “DDD”.

Pour quel objectif ?

L’un des principes fondamentales que prône DDD est le “ubiquitous language” (ou langage universel). Ce principe consiste à utiliser clairement les termes métiers dans la phase de description mais aussi dans la phase de développement et le code source. Cela permet d’avoir un langage unique entre tous les intervenants d’un projet qu’ils soient techniques et fonctionnels. Par ailleurs, il n’est pas toujours agréable de travailler sur une application legacy : vieilles technologies, pratiques obsolètes, méconnaissance du code dans son ensemble, etc. Dans ces conditions, nous aurions tout intérêt à mettre en oeuvre un ensemble d’actions pour nous faciliter la maintenance de ce projet : mise en place de tests, nettoyage du code, remaniement du code pour en faire émerger les concepts métier ? Outre la dimension “amusante” ; cela va nous permettre de réduire le risque de bugs et de garder sous contrôle la dette technique.

Passons à la pratique !

Lors de la conférence, le live coding portait sur une base de code .NET via l’IDE Visual Studio. Bien qu’applicable quelque soit le langage, j’ai décidé de mettre en oeuvre le Domain Driven Design sur une base de code Java se trouvant ici : TriviaGame. L’IDE utilisé est IntelliJ IDEA Ultimate 2017.3.

Par où commence-t-on ?

La première chose à faire est bien évidemment de prendre connaissance du code et du domaine fonctionnel de notre application. Cette première lecture va permettre de comprendre le code et d’identifier d’éventuels défauts.

Tout d’abord, que représente notre application ? Il s’agit d’un jeu, le Trivia, où plusieurs joueurs s’affrontent dans un jeu de question / réponse. Les joueurs se déplacent sur un plateau de 12 cases à l’aide d’un jet de dés ; le numéro de la case identifiant le type de question posée. Ils cumulent un point par bonne réponse et le gagnant est le premier à atteindre 6 points. 
Les questions sont catégorisées par un thème : rock, pop, science et sports. Un joueur répondant mal à une question ne peut plus se déplacer (et donc répondre) tant qu’il n’a pas tiré un nombre impair.

Du côté du code, nous observons que la logique de l’application est concentrée sur la classe “Game”. On notera particulièrement une indentation hasardeuse, une méthode inutilisée, un peu de duplication, les méthodes de visibilité différentes sont mélangées, … 
Nous ne voyons également pas d’objets pouvant appartenir au domaine métier, les joueurs et leurs attributs sont gérés directement dans cette classe via l’index attribué à chaque joueur.

Qu’en est-il du côté des tests ? Eh bien, il n’y en a pas ! Et la première chose à faire avant toute modification d’un code (à fortiori lorsqu’il nous est inconnu) est de s’appuyer sur un harnais de tests. Si des tests sont déjà présents il faut alors en vérifier la couverture, sinon nous allons devoir en créer.

Préparation d’IntelliJ IDEA

Nous l’avons vu, nous allons devoir ajouter une couche de tests ; connaître l’avancement de cette tâche par la couverture de code nous sera donc très utile. Il est possible d’exécuter nos tests avec calcul de la couverture.

L’autre intérêt est de pouvoir faire en sorte que les tests s’exécutent automatiquement à mesure que l’on modifie le code (production et tests).

Premièrement, il faut que l’IDE puisse compiler notre projet automatiquement :

Activation de la compilation automatique du projet

Activation de la compilation automatique du projet

L’idée est alors de tirer partie d’un plugin tiers “Infinitest” qui permet de lancer les tests à chaque modification du code source (code de production) de notre application. Pour cela, il faut parcourir la liste des plugins disponibles et faire une recherche avec le mot clé « infinitest ».

Une fois installé et Intellij IDEA redémarré, il est nécessaire d’activer le plugin dans les options des modules du projet :

Activation du plugin InfiniTest

Activation du plugin InfiniTest

Maintenant, toute modification entraîne le lancement des tests permettant de savoir immédiatement si nos actions ont modifié le comportement initial.

Exemple de l'intérêt du plugin "InfiniTest"

Exemple de l’intérêt du plugin « InfiniTest »

C’est parti !

Maintenant que notre outil de travail est fin prêt, nous pouvons démarrer. Il faut garder à l’esprit que nous n’abordons pas le code dans l’optique de corriger / changer son comportement actuel. Nous allons simplement le nettoyer et le “refactorer” de manière à le rapprocher de nos standards et faire apparaître les notions métiers dans le code.

Un peu de nettoyage…

Première étape, on nettoie ! Outre le reformatage, nous pouvons parcourir le code à la recherche de variables à portée trop grande ou de méthodes n’étant plus utilisées. On peut lancer une inspection générale sur la totalité du projet (warnings, typo, unused, …) ou bien cibler directement l’inspection souhaitée via la commande “Run inspection by name”.

Inspection du projet à la recherche de code non utilisé

Inspection du projet à la recherche de code non utilisé

Si des méthodes sont listées, il convient de les supprimer si celles-ci ne sont jamais utilisées dans le code de production.

Nous pouvons ensuite nous pencher sur le code dupliqué en extrayant une méthode :

Nous pouvons réaliser cette opération pour le changement et le déplacement de joueur, l’obtention de pièce, etc.

Enfin, un point important de DDD consiste à toujours favoriser la simplicité de lecture pour faciliter la compréhension. Il est ainsi plus simple de lire une condition dans un if sans négation.

Inversion d'un "if" / "else" pour éviter la négation

Inversion d’un « if » / « else » pour éviter la négation

Mise en évidence du modèle métier

En jetant un oeil au code, on s’aperçoit que les termes métiers semblent respectés en partie (on retrouve le terme “player” et “question”) mais mal structurés. Le premier objectif serait de différencier clairement la notion de joueur avec une classe dédiée en y rattachant les notions qui lui sont liées : “place” et “purse”.

Faire émerger les notions métiers au sein du code ne signifie pas simplement nommer ses variables avec des termes métiers mais bien utiliser au mieux les outils du langage dans ce but. Ainsi, dans un langage objet comme Java, il est important de matérialiser notre “Player” en tant qu’objet avec l’ensemble des attributs qui lui sont propres.

La classe "Player"

La classe « Player »

Une fois cela fait, nous pouvons adapter les tests qui ne passent plus avant de passer à la prochaine étape. Celle-ci consiste à agrémenter notre nouvelle classe de méthodes insufflant une intelligence directement tirée du métier. Ainsi, notre analyse du code a révélé que le joueur « se déplace » durant la partie :

Nous déplaçons donc la méthode « moveCurrentPlayer » vers la classe « Player » qui permet d’assigner une nouvelle position au joueur.

L’introduction de cette logique dans notre nouvelle classe implique de déplacer les tests vers une classe dédiée permettant de tester unitairement la classe “Player”.

Nous allons par la suite pouvoir remplacer les deux tableaux “places” et “purses” par des attributs de la classe “Player”. Cette étape est à réaliser progressivement puisqu’elle va demander de gros changements que ce soit du code de production ou de test.

Nous pouvons maintenant représenter plus explicitement la notion de joueur courant en remplaçant l’usage de l’index du joueur dans la liste par l’objet “Player” correspondant.

La liste des noms de joueurs est maintenant une liste de "Player"

La liste des noms de joueurs est maintenant une liste de « Player »

Enfin, nous pouvons directement déplacer des méthodes dans notre classe “Player” pour savoir si notre joueur a gagné (dépend de son nombre de pièces).

Déplacement d'une méthode dans la classe "Player"

Déplacement d’une méthode dans la classe « Player »

Par la suite, nous pouvons également introduire la notion de deck qui détiendrait l’ensemble des questions et on s’appuierait sur celui-ci pour tirer une nouvelle question. L’ajout de cette notion permet de découper un peu plus notre code et d’accentuer l’interaction entre nos objets du domaine. Ainsi, nous allons pouvoir matérialiser l’image d’un player tirant une question dans le deck par une méthode impliquant l’objet “Player” et l’objet “Deck”.

La classe "QuestionDeck"

La classe « QuestionDeck »

On voit que nous avons une méthode nous permettant de piocher une question à partir de la catégorie que l’on souhaite. On voit également que cette catégorie est décidée à partir de la position d’un joueur. Nous pouvons alors directement représenter cette action de piocher une carte à partir de sa position.

Ce remaniement se fait en trois étapes :

Tout d’abord, nous déplaçons la méthode « currentCategory » vers la classe « QuestionDeck » que nous adaptons pour prendre en paramètre un objet « Player » et non « Game ». Nous modifions ensuite la méthode « pickAQuestion » pour faire usage de cette nouvelle méthode. Nous l’adaptons également pour prendre en paramètre un objet de type « Player ». Enfin, nous nous servons du retour de cette méthode « currentCategory » pour filter le type de question à sélectionner.

Nous pouvons ensuite terminer l’opération en renommant nos méthodes. Ainsi, dans la classe “Player” nous avons la méthode “pickAQuestionFromDeck” et dans la classe “QuestionDeck” nous avons la méthode “selectQuestionFromPlayerLocation”.

Pour aller plus loin

Ces premiers usages nous permettent de mettre un pied dans la mise en œuvre de DDD mais nous pourrions aller encore plus loin ; par exemple, en suivant les 10 règles de Benoît Prioux.

Abandon des « getter » et « setter« 

Parmi elles, il nous est suggéré de ne plus préfixer nos “getter” afin qu’ils reflètent directement le nom de la propriété à laquelle chacun donne accès et de substituer les “setter” par des méthodes dénommée par un verbe et réalisant une action précise.

Ainsi, notre classe “Player” peut être écrite de la sorte :

La classe "Player" avec accesseurs nommés sans préfixe

La classe « Player » avec accesseurs nommés sans préfixe

L’utilisation des traditionnels “getter” et “setter” nous est probablement hérité des Java Bean pour lesquels il était nécessaire d’accéder aux attributs de classe (privés) par ces accesseurs.

Rien n’implique que nos objets du domaine soient des Java Bean alors autant s’affranchir de cette notation afin d’en adopter une plus proche de nos termes du métier.

Création de cas d’utilisation

Un autre point très intéressant nous invite à créer des classes simples (une seule méthode) dans laquelle nous implémentons une règle de notre domaine métier. Cette approche nous permet de focaliser nos tests avec précision sur chacune de ces règles métiers. Nous devons à tout prix éviter les appels entre différentes règles.

Voici par exemple l’implémentation du cas d’un joueur tirant une carte :

Cas d'usage : le joueur pioche une question

Cas d’usage : le joueur pioche une question

On remarque que le “QuestionDeck” est un attribut de notre cas d’utilisation. Nous avons créé une unique méthode “execute” ayant pour objectif de retranscrire l’action du “Player” piochant une carte.

On fait référence à ces cas d’utilisations par la notion de “service d’application”.

« Living documentation« 

Troisième point très important : maintenir une documentation efficace de l’ensemble du projet. Il faut par exemple garder un schéma à jour du domaine en le générant à partir du code source.

Il faut également documenter le code mais pas nécessairement qu’au travers de la Javadoc. En s’appuyant sur les principes de BDD (Behavior Driven Development), on écrit un cheminement pour chaque comportement attendu. Ainsi, en reprenant l’exemple de notre cas d’utilisation du joueur piochant une carte :

Test unitaire du cas d'utilisation

Test unitaire du cas d’utilisation

Notre test se découpe donc en trois parties distinctes séparées visuellement par un commentaire “Given”, “When” ou “Then”. La lecture de notre test donnerait en français :

  • Given : Sachant que nous avons un joueur, qu’on s’attend à obtenir cette question
    et que la méthode de sélection va nous retourner cette même question
  • When : Quand j’applique le cas d’utilisation “le joueur sélectionne une carte” sur le joueur
  • Then : Alors la question obtenue doit être celle attendue.

Il est important de faire attention à l’utilisation du “when” qui représente ici l’évènement qui va entraîner les résultats que l’on souhaite obtenir : c’est pratiquement tout le temps la méthode que l’on teste. Il ne faut donc pas inclure la gestion des “mock” malgré l’utilisation du mot clé “when” comme dans la librairie Mockito (celle utilisée ici).

Et donc ? Qu’a-t-on gagné dans l’histoire ?

Suite à notre “refacto”, grâce à l’utilisation du Domain Driven Design, nous pouvons constater qu’il est plus agréable de naviguer dans le code source de l’application. Le code est maintenant plus concis et mieux organisé ; avec des concepts regroupés dans des classes distinctes (“Player”, “Question”, etc.). L’appropriation et la compréhension des sources en est par la même occasion grandement facilité pour nous, mais aussi pour les personnes futures.

De plus, ce travail peut avoir une dimension amusante et plaisante à remettre aux goûts du jour un code source daté ou bien ne répondant pas au standards actuels.

À priori, le travail que nous venons de faire pourrait sembler inutile à bien des personnes et pour des raisons différentes mais ce qu’il est important de souligner est qu’il peut être difficile de s’approprier le code d’autres développeurs de tout niveau et ayant produit du code dans des contextes que nous ignorons.

L’important ici est de se familiariser avec cette pratique et ses objectifs ; la bonne mise en oeuvre reposera alors surtout sur du bon sens et la bonne utilisation de son IDE.

Une valeur sûre pour pouvoir comprendre et répondre au besoin exprimé par le client est donc de faire ressortir le plus possible les notions métier comme nous venons de le voir afin que les travaux futur partent d’une base assainie où il sera aisé d’ajouter ou modifier des comportements.

Il n’y a pas de doute que nous pourrions aller encore plus loin dans le refacto et l’application des pratiques DDD et de code propre mais nous n’interviendrons rarement pour le plaisir et ces travaux vont se greffer en amont d’une phase de corrections et d’évolutions inscrite dans un contexte projet où le temps ne nous est pas illimité.

Par ailleurs, bien que ce travail profite à tous, il est souvent plus gratifiant de contribuer à l’ajout de nouvelles fonctionnalités à la fois sur les aspects fonctionnels et techniques. Tout est question d’équilibre et celui-ci est à évaluer via les contraintes inhérentes au projet : délais, budget, apport réel, … Il ne faut pas tomber dans l’excès afin que le fruit de ce travail reste constructif : l’effort ne doit pas être démesurément grand par rapport au gain.

Si vous souhaitez en savoir plus ou tout simplement regarder la présentation originelle, vous pourrez la visionner ici : live coding au BDX.io.