Dans ce billet, j’aborde la notion de classe immutable. Beaucoup de développeurs en connaissent la définition, mais ne connaissent pas les implications de ce pattern. Il est pourtant très intéressant, car il permet de rendre plus robuste les développements dans bien des cas.

Vous pouvez également retrouver le premier article clean code.

Immutable/Immuable qu’est que c’est ?

Immutable est le terme anglais, j’utiliserai uniquement ce dernier dans la suite de l’article.

Place à la théorie, ne fuyez pas c’est diablement simple :

Une classe immutable est une classe dont les objets, une fois instanciés, ne peuvent plus changer d’état.

Qu’est-ce que cela implique ?

Rendre une classe immutable à ces avantages et un inconvénient principal. Commençons par ses nombreux avantages :

Les classes immutables sont par nature thread-safe, elles sont conseillées en environnement multi-thread.

Elles peuvent être mises en cache côté client sans risque de désynchronisation.

Elles peuvent être utilisées sans risque comme clef dans des Map ou dans des Sets (voir glossaire en fin d’article), car leur hashcode est constant.

Quand ces objets sont utilisés comme variables d’une classe, leur initialisation n’a pas à être fait à partir d’une copie défensive (voir glossaire).

Il est possible pour le hashcode de ces classes, d’utiliser la technique de la lazy initialisation (voir glossaire) et de le garder en cache.

Une classe immutable, est plus simple et plus lisible. Pas de setter etc..

L’invariant de classe (voir glossaire) n’a besoin d’être validé qu’à la création de l’objet.

Il n’est pas nécessaire de leur créer un constructeur par copie ou d’implémenter l’interface Clonable (spécifique Java)

Le seul réel désavantage est le suivant :

Si l’on souhaite modifier un objet, il faut le recréer ce qui peut être plus couteux qu’une simple modification.

Malgré cette longue liste d’avantages et de qualités, l’utilisation d’objets immutables est trop peu fréquente. Alors qu’ils permettent de résoudre énormément de problèmes classiques de programmation.

Quand utiliser une classe immutable?

De manière générale, le plus souvent possible. Mais si c’était aussi simple, nous en mettrions partout. La question à se poser est : La classe que je manipule est-elle amenée à changer de valeur au cours de son cycle de vie? Si la réponse est non ou peu, alors elle est une bonne candidate.

Le use case parfait est l’utilisation dans des classes représentant des valeurs, car l’objet a rarement de raison d’évoluer et il est peu couteux à créer. Par exemple dans Java, les classes suivantes sont des classes immutables :

Integer

Long

Float

BigInteger

BigDecimal

Un autre use case classique est la représentation de couleur (en RGB par exemple), ainsi on retrouve dans le package java.awt.color un ensemble de classes immutables.

Parmi les autres uses cases classiques d’utilisations:

Les évènements, qui sont amenés à peu évolué et ont une durée de vie faible

Les systèmes de messagerie entre composants, les messages sont très souvent des objets immutables.

Le cas échéant, rendre un objet le plus immutable possible

Dans certains cas, faire une classe 100% immutable peut être contraignant, car un ou deux attributs de la classe sont amenés à évoluer régulièrement au cours du cycle de vie de l’objet.

Dans ce cas, il faut rendre la classe la moins mutable possible en acceptant les modifications uniquement sur le ou les éléments nécessaires. Plus vous fermerez vos objets à la modification, plus vous pourrez bénéficier des effets bénéfiques de l’immutabilité (voir liste d’avantages).

Rendre une classe immutable

Il est assez simple de mettre en place une classe immutable en suivant ces conseils :

Votre classe doit être finale afin de ne pas être étendue

Toutes les variables de votre classe doivent être privées et finales afin d’empêcher les modifications ultérieures à la création.

Ne pas créer de « setter » ou de méthode modifiant vos variables de classe

Si l’objet contient une variable qui est une référence à une classe mutable il ne faut pas : Avoir de méthodes modifiant cet objet partager de références sur cet objet et s’assurer qu’il n’y a pas de références sur la classe courante. Dans le cas contraire, faire une copie défensive de cet objet.



Exemple d’une classe immutable

Après la théorie, place à la pratique. Voici un exemple de classe immutable, qui respecte les règles précédentes.

import java.util.Date; // La classe doit être finale pour ne pas être étendue public final class ImmutableObject { // les variables de la classe doivent être privées et finales private final int red; private final int green; private final int blue; private final Date pastDate; // Ne peux pas être final en cas de lazy initialisation, // car la valeur va être initialisé uniquement lors de l'appel de la méthode hashCode() private int hashcode; public ImmutableObject(int red, int green, int blue, Date date) { // Vérification des invariants de classe if(checkParameters(red, green, blue,date)){ this.red = red; this.green = green; this.blue = blue; // Date est une classe mutable donc il faut créer une nouvelle instance // C'est une copie défensive this.pastDate = new Date(date.getTime()); } else{ throw new IllegalArgumentException("Un des arguments ne respecte pas les restrictions"); } } private boolean checkParameters(int red, int greeen, int blue, Date date){ return checkColors(red, green, blue) && checkDateIsPast(date); } private boolean checkColors(int red, int green, int blue) { return checkAColor(blue) && checkAColor(green) && checkAColor(blue); } private boolean checkAColor(int aColorValue){ return aColorValue >= 0 && aColorValue <= 255; } private boolean checkDateIsPast(Date dateToCheck){ Date now = new Date(); return dateToCheck.before(now); } /* * Uniquement des getters, pas de setter */ public int getRed() { return red; } public int getGreen() { return green; } public int getBlue() { return blue; } /* * Afin de ne pas passer une référence sur notre objet, * on fait un copie de ce dernier */ public Date getPastDate() { return new Date(pastDate.getTime()); } /* * lazy initialisation et mise en cache du hashcode */ @Override public int hashCode(){ if(this.hashcode == 0){ this.hashcode = this.red * 1_000_000 + this.green * 1_000 + this.blue; } return this.hashcode; } }

Enfin, voici une classe main qui illustre les points de fonctionnement importants:

import java.util.Date; public class ImmutableMain { public static void main(String[] args) { // Création d'une date (le -1900 est due au fonctionnement de la classe Date Date firstJanuary2012 = new Date(2012 - 1900, 0, 1); int red = 240; int green = 240; int blue = 150; // initialisation de l'objet immutable, ce dernier ne sera plus modifiable ImmutableObject object1 = new ImmutableObject(red, green, blue, firstJanuary2012); System.out.println("Red = " + object1.getRed() + " Green=" + object1.getGreen() + " Blue=" + object1.getBlue() + " Date=" + object1.getPastDate().toString()+ "

"); //Modification de l'objet date passé en référence firstJanuary2012 = new Date(1999 - 1900, 0, 1); System.out.println("On modifie la référence de la date mais grâce à la copie " + "de l'objet dans le constructeur, il n'y a pas de modification dans l'objet."); System.out.println("Date=" + object1.getPastDate().toString() + "

"); //Modification de l'objet date retourné par la méthode getPastDate de l'objet object1.getPastDate().setMonth(2); System.out.println("On modifie la référence de la date récupérer dfazns l'objet immutable, " + "la date ne change pas car on modifie une copie de l'objet Date."); System.out.println("Date=" + object1.getPastDate().toString() + "

"); // objet.setRed(150) est impossible il faut alors recréer un objet pour obtenir la valeur souhaitée red = 150; firstJanuary2012 = new Date(2012 - 1900, 0, 1); ImmutableObject objectRouge150 = new ImmutableObject(red, green, blue, firstJanuary2012); System.out.println("Red = " + objectRouge150.getRed() + " Green=" + objectRouge150.getGreen() + " Blue=" + objectRouge150.getBlue() + " Date=" + objectRouge150.getPastDate().toString()+ "

"); // on vérifie rapidement les invariants de classes en essayant de créer un objet avec une valeur non cohérente red = 350; ImmutableObject objectRouge350 = new ImmutableObject(red, green, blue, firstJanuary2012); System.out.println("Red = " + objectRouge350.getRed() + " Green=" + objectRouge350.getGreen() + " Blue=" + objectRouge350.getBlue() + " Date=" + objectRouge350.getPastDate().toString()+ "

"); } }

Le résultat du main est :

On voit bien que l’intégrité de l’objet est préservé. Il ne peut être modifié par les méthodes de l’objet (pas de setter) . Il en est de même avec les références sur les paramètres de l’objet qui n’impacte pas l’objet.

Glossaire

Map/Set: Ces structures d’objets se basent sur le hashcode d’un objet si ce dernier est déjà utilisé au sein de la structure. Si le hashcode varie au cours du temps sur un objet cela peut poser problème dans ce type de structure.

Copie défensive : Cela correspond au fait de créer un nouvel objet à partir d’un objet passé en paramètre pour éviter que ce dernier soit modifiable par sa référence ou son pointeur.

Lazy initialisation: C’est la technique qui consiste à calculer, dans notre cas le hashcode, uniquement lors de l’appel de la méthode hashcode et non à l’initialisation de l’objet. Cela a un impact positif sur les performances de l’objet.

Invariants de classe : Ce sont les contraintes appliquées aux valeurs des paramètres d’un objet. Par exemple dans un formulaire demandant la date de naissance, il semble logique d’interdire une date future.

Merci à Amaury pour la relecture.

Retrouvez le premier article sur le sujet du clean code.

Sources :