Le meetup sur l’immutabilité et les bugs qu’il corrige
Ce meetup a été l’occasion de voir ou revoir l’immutabilité : une technique de programmation.
Cette technique permet de réduire les sources possibles de bugs.
Note : on dit plutôt immuabilité en bon français.
Immutability en anglais.
On fera ici du franglais par abus de langage 🙂
Cet article résume une présentation orale lors d’un meetup.
Plus de détails sont disponibles dans les slides en bas de l’article.
Quels bugs résout l’immutabilité ?
La première partie de la présentation a mis en exergue les problèmes que l’on rencontre souvent en programmation quand on manipule beaucoup d’objets muables :
- Initialisation d’un objet en plusieurs étapes complexes :
Un objet en cours d’initiation est passé de méthode en méthode. Chacune va définir certains champs de l’objet. Ou pire, certaines méthodes vont redéfinir les valeurs de champs déjà initialisés. L’analyse du code et son débogage s’en trouvent grandement complexifié ;
- Oubli d’initialisation de champs :
On ne sait jamais si un objet est en cours d’initialisation.
Donc, on ne sait jamais si un objet est dans un état intermédiaire.
Ou si ses champs null sont vraiment null car l’objet a terminé son initialisation.
Ou si les champs à null sont juste des bugs causés par la séquence d’initialisation trop complexe ;
- Dommages collatéraux :
Un objet pouvant être assigné à plusieurs autres, et étant passé à un nombre incalculable de méthodes, on n’est jamais certain que l’une d’elles ne modifie pas par mégarde un objet partagé ;
- Multi-threading :
Un objet modifié par plusieurs threads est toujours un danger potentiel de bug subtil.
L’immutabilité a ce bonus d’être plus sécurisant à cet égard.
La solution de l’immutabilité
Nous avons vu comment l’immutabilité offre une solution (parmi d’autres) à ces problématiques.
La solution s’énonce très simplement : un objet est entièrement initialisé par son constructeur.
Son état n’est plus autorisé à changer après.
Il existe déjà plusieurs classes immuables en Java :
- String :
C’est la plus connue. Contrairement au C ou C++, on ne peut pas modifier les caractères d’une chaîne. Ceci évite bien des bugs de modification d’une chaîne qui était partagée avec plusieurs autres objets qui ne s’attendent pas à ce que leurs versions des chaînes changent ; - BigDecimal / BigInteger :
L’addition de deux de ces nombres produit un troisième objet, pour les mêmes raisons que les String ; - Instant, LocalDate, ZonedDateTime et les autres classes du package java.time :
Offrent des manipulations de dates et heures sans dommages collatéraux ; - Stream.toList() ainsi que les ajouts récents au JDK Java :
Les derniers ajouts tendent de plus en plus à être immuables pour réduire les risques de bugs…
Nous avons également vu quelques exemples de classes métier à créer en version immuables.
Implémentation de l’immutabilité
Ensuite, nous avons décrit comment coder une classe immutable :
- Tous les champs sont finaux ;
- Le constructeur initialise tous les champs ;
- Aucun setter n’est permis ;
- Rendre la classe finale est fortement recommandé pour éviter qu’une classe fille ne soit mutable.
C’est même parfois obligatoire, comme dans le cas des records Java ; - Lombok offre l’annotation @Value pour implémenter tout ça ;
- Java 16 offre le mot clef record pour faire la même chose de manière standard ;
- Si le type d’un champ est mutable, il faut créer des copies lors de la construction et lors du getter s’il y en a ;
- L’objet peut contenir des champs mutables pour optimisation (lazy-loading, par exemple).
Mais ce comportement ne doit pas être visible de l’extérieur par les utilisateurs de la classe.
Démonstration de refactoring
Ensuite, nous avons enchaîné sur un exemple de code buggé car ne mutant pas assez de champs, ou ne mutant pas les bons champs.
Le but de la démo était de refactorer le code pour que la plupart des classes deviennent immutables.
En faisant cela, nous avons vu que les paramètres obligatoires des constructeurs ont forcé le code à être moins buggé. Il est alors devenu :
- plus mutualisé : l’abandon préalable de l’héritage pour préférer la composition a permis d’éliminer du code dupliqué ;
- un peu moins lisible, mais nous avons vu quelques techniques de factorisations qui ont permis d’offrir des solutions élégantes : le résultat final est plus simple à comprendre (initialisations d’objets à un seul endroit) ;
- plus correct : pas d’oubli d’appels de setters ou de méthodes de builders ;
- plus descriptif : les méthodes créées pour remplacer les mutations par setters ont maintenant un nom descriptif de chaque geste métier requérant une mutation. C’est le principes du Tell, Don’t Ask, ainsi que de l’utilisation de static factories ;
- en bonus, maintenant que la création d’un objet passe toujours par le même constructeur, ce constructeur fait office d’endroit unique où ajouter des règles de validations comme l’interdiction de valeurs nulls sur certains champs, par exemple. Aussi, les méthodes with*() offrent maintenant un endroit unique pour ajouter certaines règles métier comme la non-historisation d’un changement de prix qui remettrait le même prix…
Exercices à faire chez soi
L’exemple de la démo est fourni en tant que kata.
C’est un exercice que vous pourrez refaire vous-même pour vous perfectionner dans la transformation de classes en classes immutables.
Il est même recommandé de faire l’exercice avant de voir les slides.
Cela permet de trouver certaines astuces soi-même, et donc de mieux les assimiler, ou au contraire de voir où on bloque pour estimer son niveau et comment on peut améliorer son apprentissage.
Voir l’exercice sur GitHub : utiliser la branche “main” pour commencer, puis il existe une branche de solution et une branche bonus qui va encore plus loin dans la solution.
Un second exercice est aussi disponible suite aux questions de l’auditoire après le meetup. Il s’agit ici de transformer du code qui va initialiser une hiérarchie d’objets. Cela peut donner du fil à retordre la première fois. En effet, il faut d’abord initialiser les objets les plus profonds dans la hiérarchie, pour remonter petit à petit cette hiérarchie. Cela contraste avec la méthode habituelle de créer l’objet racine puis d’appeler des setters (ou des add() sur des listes) pour petit à petit définir les sous-objets. Une branche de solution existe aussi.
Et après ?
La dernière partie du talk offre succinctement certaines pistes de recherche pour ceux qui veulent aller plus loin dans la pratique. Pêle-mêle :
- Comment intégrer des objets immutables aux entities Hibernate, via les annotations @Embeddable, @Embedded et @AttributeOverrides ;
- Utilisation d’autres principes de développement pour créer du code orienté métier et transformer la contrainte de l’immutabilité en force pour obtenir du code plus expressif et maintenable :
- Tell Don’t Ask,
- Feature Envy,
- Loi de Déméter,
- Encapsulation ;
- The Checker Framework pour une gestion des nulls par le compilateur (annotations @NotNull / @Nullable) : le fait de ne plus avoir d’objets à moitié initialisées avec des nulls transitoires est une énorme aide pour pouvoir se débarrasser des NullPointerException en production, et pour offrir au développeur la sérénité de savoir quand et pourquoi un champ peut ou ne peut pas être null ;
- L’architecture hexagonale (et éventuellement le Domain Driven Design) dans laquelle les objets métier ne sont pas des entités Hibernate avec tous leurs champs nullables (car nécessitant un constructeur sans paramètres) ;
- Programmation fonctionnelle : l’immutabilité est au cœur de ce style de programmation ;
- Var : offre des classes Java immutables utilisables avec beaucoup de facilité ;
- Kotlin : ce langage de la JVM encourage encore mieux la création de classes immutables (et offre aussi la garantie de la gestion des nulls, ce qui permet de réduire presque à néant ces deux plus gros problèmes en programmation)…
Vous trouverez également plus d’informations sur la page dédiée du site du speaker.
Plus en détails
Vous trouverez bien sûr de plus amples détails dans les slides de la présentation :
Recent Posts
- UbikLoadPack Video Streaming Plugin 9.1.5 14 June 2024
- What is DASH multi period and when to use it ? 4 December 2023
- UbikLoadPack Java Serialization Plugin 23 November 2023