La lecture de cet billet : Editorial: NPEs must die, m'a posé une nouvelle fois la question de la programmation défensive. Dans son éditorial, l'auteur nous explique qu'en java une NullPointerException ne casse pas le système, ce qui pousse les programmeurs fainéants (et trop le sont :( ), à ne rien faire. Il nous dit ensuite :

This is just plain bad coding. An NPE tells you almost nothing about what really went wrong. Instead, Java developers should check objects before invoking methods on them, and throw a more specific and meaningful runtime exception, such as an IllegalArgumentException, with details about what value was incorrect.

Rien à dire sur le principe de guidage en indiquant précisément le problème (quelle valeur n'est pas bonne et pourquoi). En revanche, "check objects before invoking methods on them" amène à la programmation défensive.

Prenons une fonction qui indique si une chaine de caractères est contenue deux fois dans une autre. Une implémentation possible serait :

public static boolean contientDeuxFois(
     String contenant, 
     String contenueDeuxFois) {
     return contenant.indexOf(contenueDeuxFois, 
            contenant.indexOf(contenueDeuxFois)) != -1;
}

Si je suis ce que préconise l'auteur, j'ajoute un bloc :

if (contenant == null) {
     throw new IllegalArgumentException("contenant blah blah...");
}
if (contenueDeuxFois == null) {
     throw new IllegalArgumentException("contenueDeuxFois blah...");
}

Où est le problème ?
1) on augmente le nombre de lignes, sans avoir beaucoup de valeur ajoutée. Car nous aurons une IllegalArgumentException avec une stacktrace :

java.lang.IllegalArgumentException: contenant blah..
	at MaClasse.contientDeuxFois(MaClasse.java:35)
	at MaClasseTest.testContient(MaClasseTest.java:39)
...

au lieu d'une NullPointerException :

java.lang.NullPointerException
	at java.lang.String.indexOf(String.java:1733)
	at java.lang.String.indexOf(String.java:1715)
	at MaClasse.contientDeuxFois(MaClasse.java:35)
	at MaClasseTest.testcontientDeuxFois(MaClasseTest.java:39)
...

Le soucis n'est pas tant l'exception que ce qu'on indique dans le message. On aurait pu aussi lancer une NullPointerException avec le même message on aurait eu la fonction, la ligne, et on avait une information supplémentaire par le nom de l'exception.

Il est bon également de savoir lire une stacktrace pour en extraire l'information nécessaire à comprendre rétrospectivement ce qu'il s'est passé. Ici, c'est comprendre que nous sommes dans les deux premières lignes de la stacktrace dans la classe String.indexOf() de java. Donc c'est bien la chaine contenueDeuxFois qui était nulle. Sinon nous aurions eu une NullPointerException directement sur la ligne 35 de MaClasse, et pas dans indexOf de String.

2) Risques de duplication : dans un programme important, il est tout à fait possible que le cas du null ait été traité plus haut, on n'a pas forcément la connaissance de tous les chemins parcourus jusqu'à cette méthode. Par conséquent, si cette pratique défensive est généralisée dans une équipe, il est fortement probable qu'à de multiples endroits dans le code on retrouve des tests de nullité similaires.

3) il n'y a pas de réflexion métier. Quand on développe en TDD une fonction comme celle-ci, à un moment on va se demander "et si contenant est nulle, et si contenueDeuxFois est nulle, que doit renvoyer la fonction ?". Par exemple si l'une ou l'autre des chaines est nulle, d'un point de vue métier la fonction peut renvoyer false (l'implémentation est plus simple dans ce cas, car on a qu'un seul if).

Quelles sont les solutions ?

  • la fonction peut renvoyer une valeur qui du point de vue métier est cohérente avec le cas d'une entrée nulle
  • la fonction peut renvoyer un NullPointer envoyée explicitement avec un message clair
  • la fonction peut renvoyer une exception métier explicite qui hérite de NullPointerException ou IllegalArgumentException et qui donnera une information métier
  • dans l'ensemble du programme on peut choisir pour des objets complexes le pattern null object
  • la fonction peut envoyer une autre exception java qui donne une meilleure information (cas de l'IllegalArgumentException, IllegalStateException si l'état d'un objet est corrompu par une valeur nulle, etc.)
  • éventuellement, la fonction peut laisser remonter une NullPointerException si le développeur pense qu'elle sera facile à interpréter et qu'elle est suffisamment explicite

Et j'en oublie surement.

On peut constater qu'il y a beaucoup de possibilités de traitement du cas de la référence d'un objet nulle pour pouvoir tirer une règle générale. Chaque éventualité de référence nulle doit amener le(s) programmeur(s) à se poser des questions. Les réponses doivent mener à la solution la plus adaptée.