L'immutabilité : ce super-pouvoir contre les bugs !

Par Sébastien Laoût, Ubik Ingénierie

En développement, l'une des sources de bugs est un schéma d'initialisation complexe pour un objet, ou avec trop de règles implicites (tels ou tels setters doivent ou non être appelés pour tel geste métier).

Nous allons voir comment l'immutabilité permet de transformer ces problèmes en erreurs de compilation.
L'initialisation d'un objet se fera en une seule étape atomique par son constructeur, sans risque d'oublier des appels de setters important pour un geste métier en particulier, et sans risque d'interférer avec un autre objet ou thread.
Nous allons aussi surtout voir comment ce simple mécanisme implique la mise en œuvre de diverses autres techniques pour produire un code clean, expressif, et donc une plus faible surface d'attaque pour les bugs.

La présentation est accompagnée de deux exercices de mise en pratique ("kata", dans la terminologie Software Craftmanship) : un expliqué dans la présentation (mais que vous pouvez faire par vous-même à mi-chemin de la présentation), et un autre pour approfondir un cas particulier qui peut rebuter lorsqu'on commence à transformer son code pour utiliser l'immutabilité.

La présentation

▶ Visionner les diapositives ou regarder la vidéo :

Exercice 1 : la kata servant de démonstration à la présentation

Exercez-vous avec ce premier kata.
Ne vous spoilez pas !
Visualisez uniquement les 28 premiers slides de la présentation pour vous impregnier des avantages de Value Objects immutables.
Une présentation de l'application y est aussi disponible de la slide 26 à la slide 28.

Voici les étapes de la transformation à effectuer :

  1. Préférez la composition à l'héritage
    Moins de champs, moins de setters, moins de lots de mutations (Edit extends User)
  2. Appeler les constructeurs complets
    au lieu des setters et builders (Price, Currency, Edit, User)
  3. Créer des méthodes with*() et / ou des static factories
    avec des noms de workflows métier
  4. Renforcer l’applications des invariants métier dans le constructeur
    Exemple : pas de devises nulles
    + règle mutualisable gratuitement : les modifications inutiles (en masse) ne doivent pas être historisées

Le kata est disponible ici :
Code Java de l'exercie 1 sur GitHub (le repository Git contient aussi le second exercice)
Le kata est directement présent sur la branche "main".

En voici les principales classes :

Solution de l'exercice 1

Spoiler : la solution est présente sur la branche "java/exercise1/solution".
Une solution qui va plus loin est présente sur la branche "java/exercise1/solution-bonus".

Exercice 2 : kata supplémentaire sur l'initialisation d'une hiérarchie d'objets

Remarque sur les objets hiérarchiques

Cet exercice permet de découvrir par soi-même une réponse à une question récurante concernant l'immutabilité suite à la présentation.

Il arrive régulièrement de devoir initialiser une hiérarchie d'objets.
On crée un objet parent, puis petit à petit, on set() ses objets enfants.
Ou alors on ajoute des enfants à une liste, qui eux-mêmes contiennent d'autres objets, etc.

Comment faire dans le cadre de l'immutabilité ?
En effet, on ne peut pas avoir ces états transitoires non-initialisés : nulls ou listes vides.

Il va falloir inverser sa façon de programmer.
On va initialiser en premier les petits objets les plus bas dans la hiérarchie, puis petit à petit les agréger pour initialiser les objets de plus en plus haut dans la hiérarchie.

Exercice sur les objets hiérarchiques

Pour mettre en pratique la remarque ci-dessus, je vous propose l'exercice suivant :
https://github.com/slaout/immutability-super-power-kata/tree/main/exercise2/java/src

On a en entrée les lignes d'un fichier CSV qu'on a parsé dans une liste d'objets à plat.
Les lignes sont "dénormalisées" : les colonnes des objets parent ont des données en doublon : une fois pour chaque objet enfant.
Il s'agira de regrouper plusieurs lignes CSV pour créer les objets hiérarchiques.

Ça sera plus clair avec ce test unitaire qui montre les objets en entrée et en sortie :
https://github.com/slaout/immutability-super-power-kata/blob/main/exercise2/java/src/test/java/com/github/slaout/immutability/exercise2/usecase/FlatOrderImportUseCaseTest.java

Voici la classe des lignes CSV plates en entrée :
https://github.com/slaout/immutability-super-power-kata/blob/main/exercise2/java/src/main/java/com/github/slaout/immutability/exercise2/dto/OrderCsvRow.java

Voici les objets hiérarchiques en sortie, à transformer en @Value-Objects immutables :
La classe Order contient des OrderLine et cette dernière contient des Option :
https://github.com/slaout/immutability-super-power-kata/tree/main/exercise2/java/src/main/java/com/github/slaout/immutability/exercise2/domain

Et voici le code à refactorer :
https://github.com/slaout/immutability-super-power-kata/blob/main/exercise2/java/src/main/java/com/github/slaout/immutability/exercise2/usecase/FlatOrderImportUseCase.java
À vos claviers :-)
N'hésitez pas à me proposer vos solutions et remarques.

TODO MIXER LES DEUX PRÉSENTATIONS


TODO : schéma !
Le but de l'exercice est de transformer les cinq classes du package domain en Value Objects immutables, et de refactorer la classe UseCase afin d'initialiser les objets.

Le kata est disponible ici :
Code Java de l'exercie 2 sur GitHub (même repository Git que le premier exercice)
Le kata est directement présent sur la branche "main".

Voici la classe à refactorer et son test unitaire associé :

Voici la classe de l'objet passé en paramètre de la méthode à refactorer :

Voici les classes de la hiérarchie d'objets retournés par la méthode à refactorer :

Une solution de l'exercice 2

Spoiler : la solution est présente sur la branche "java/exercise2/solution".

Si vous n'aurez pas le temps de le faire, ou si vous voulez juste voir à quoi ça peut ressembler, je vous donne une solution :
ATTENTION : SPOILER :
https://github.com/slaout/immutability-super-power-kata/blob/java/exercise2/solution/exercise2/java/src/main/java/com/github/slaout/immutability/exercise2/usecase/FlatOrderImportUseCase.java

J'ai pris soin d'appliquer les principes du Clean Code pour comprendre facilement la solution :

La solution est ici à base de streams, sous forme de programmation fonctionnelle : aucun objet n'est muté et les fonctions sont "pures".
Mais c'est une programmation fonctionnelle soft / pragmatique : on n'a pas de notions mathématiques absconses telles que des monades, monoids, functors...

On peut bien sûr partir d'une solution à base de boucles for() imbriquées et petit à petit extraire des bouts de code en fonctions à base de streams.
On se rend compte qu'avec un code à base de streams, on démarre la lecture du code de l'objet le plus gros vers ceux les plus profonds, comme le code non-refactoré.
Même si, lors de l'exécution, ce sont bien les petits objets qui sont créés en premier.

SPOILER POC

À propos du speaker

Sébastien est un développeur full-stack avec une spécialisation sur le backend Java, principalement dans l'e-commerce, depuis 14 ans.
Touche à tout, il conçoit et développe des applications Spring, des clients VueJS, et même un jeu Android (et il a fait du PHP, mais mieux vaut ne pas l'ébruiter ;-) ).
Ayant à cœur la qualité du code et de ses livrables (architecture technique, clean code et expérience utilisateur), il a une très bonne expérience dans l'automatisation des tests Selenium, Cucumber et Postman.

À propos de l'événement

Présentation effectuée le 26 avril 2022 chez Ubik Ingénierie.

Crédit des images utilisées dans la présentation

Voir les commentaires présentateur des diapositives concernées.

Licence

La présentation est fournie sous licence Creative Commons - Attribution - Partage dans les Mêmes Conditions 4.0 International (CC BY-SA 4.0)