L’absence de valeurs #
Les trois techniques les plus courantes pour représenter l’absence de valeurs sont généralement mauvaises:
- les valeurs passerelles
- le
null
- les exceptions
Stratégies inadaptées #
Valeurs passerelles (valeurs arbitraires) #
Ceci offre une très mauvaise séparation de la logique et du traitement de l’absence de valeur avec un risque de bugs accru. De plus, il devient très difficile de retrouver la source de cette erreur.
C’est la stratégie proposée par List<E>
et sa méthode indexOf
par exemple. Cette stratégie est très utilisée en C
également.
méthode | description |
---|---|
int indexOf(Object o) |
Returns the index of the first occurrence of the specified element in this list, or -1 if this list does not contain the element. |
Utilisation d’une référence null
#
Ceci oblige à constamment vérifier qu’une référence n’est pas nulle et provoque très fréquemment l’exception la plus courante : le NullPointerException
. Il devient également très difficile de retrouver qui est la cause de cette référence nulle.
Selon une étude (source OverOps: https://blog.takipi.com
), 97% des exceptions loguées sont causées par 10 exceptions les plus courantes. En tête se trouve le NullPointerException
loguées dans 70% des 1000 applications étudiées!
Un constat démontre généralement que le null
est très souvent utilisé à tort pour représenter une absence de valeurs.
C’est la stratégie adoptée par Map<K,V>
et sa méthode get
par exemple.
méthode | description |
---|---|
V get(Object key) |
Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key. |
Les exceptions #
Cette technique est sensiblement meilleure. Tous les outils sont mis à disposition de l’utilisateur pour vérifier un état avec d’appliquer une action, mais si l’utilisateur ne l’emploie pas correctement, le mieux est peut-être de lever une exception. Le programme peut planter, mais il sera beaucoup plus simple à identifier le code fautif et donc à corriger rapidement celui-ci.
C’est la stratégie adoptée par Deque<E>
qui s’utilise comme une queue ou comme une file.
méthode | description |
---|---|
public E getFirst() |
Retrieves, but does not remove, the first element of this deque. Throws: NoSuchElementException - if this deque is empty. |
L’absence de valeurs dans les collections #
Dans le cas des collections de tous types, c’est relativement simple. Il m’arrive pourtant de voir ce genre de code:
|
|
L’appelant qui s’attend à une liste voudra parcourir celle-ci:
|
|
Si une référence nulle est retournée, le code ci-dessus provoquera l’arrêt du programme.
Pour retourner une liste vide, plusieurs solutions existent:
// retourne une liste mutable:
return new ArrayList<>();
// retourne une liste non modifiable:
return List.of();
return Collections.emptyList();
Faites de même dans vos classes:
|
|
Le même raisonnement s’applique pour les tableaux statiques, et de manière générale, pour tous types de collections.
|
|
L’absence de valeurs pour un type particulier (hors collection) #
L’absence de valeur est une valeur qui existe, ou qui n’existe pas. C’est-à-dire que cette valeur est optionnelle.
Proposition #
Si une méthode doit retourner un utilisateur à l’aide d’un identifiant, mais qu’aucun identifiant ne correspond à un utilisateur, plusieurs problèmes peuvent survenir:
public User get(int id) {
...
/* que faut-il retourner si l'identifiant ne correspond à aucune utilisateur ? */
return ???;
}
|
|
Pour éviter de retourner null
ou une exception, une bonne idée, certes simpliste, consiste à encapsuler l’utilisateur à retourner dans une collection qui ne peut contenir au plus qu’un élément.
- si l’utilisateur à retrouver existe, la liste retournée contient uniquement celui-ci
- si l’identifiant ne correspond à aucun utilisateur, une liste vide est retournée
Nous avons donc deux états possibles ainsi que des méthodes à disposition pour vérifier l’état. Notre méthode deviendrait:
/* returns a list that contains at most one user */
List<User> get(int id) { ... }
Et son utilisation nous obligerait à vérifier si utilisateur existe dans la liste:
|
|
Pas mal, non ? A noter que j’ai déjà employé ce genre de stratégie avec des langages qui n’offre pas mieux.
Il deviendrait même possible d’utiliser le forEach
:
get(42).forEach( user -> System.out.println(user.email()) );
Si la liste est vide, il ne se passe rien, sinon l’action est appliquée (afficher l’email dans un terminal).
Défauts #
Cette approche comporte des défauts: une structure non adaptée est employée pour représenter un comportement sensiblement différent. La valeur optionnelle doit exister ou non, alors qu’une liste peut contenir entre zéro et une infinité d’éléments avec la possibilité d’ajouter des éléments, de les supprimer, de connaître le nombre d’éléments…
Remède #
Proposer une nouvelle structure qui comporte zéro ou une valeur au plus. En Java, c’est le type Optional<T>
. Celui-ci agit comme une collection qui peut avoir zéro ou un élément.
Notre méthode devient:
public Optional<User> get(int id) { ... }
Et voici plusieurs utilisations possibles:
|
|
Et l’équivalent du forEach
(humhum, intéressant):
maybeUser.ifPresent( user -> System.out.println(user.email()) );
Pour retourner un optionnel, il existe des fabriques. Imaginons que nous avons utilisé une Map
(dont la méthode get
peut retourner null
) pour stocker nos utilisateurs:
|
|
Remarquez l’analogie avec une liste vide (List.of()
) et une liste non vide (List.of(elem)
).
Vous pouvez directement employer la fabrique ofNullable
pour simplifier le code ci-dessus. Cette méthode retourne un optionnel vide si l’argument est null
.
|
|
Fail Fast et système de typage robuste #
Rappelez-vous de la philosophie du Fail Fast qui consistait à planter le plus rapidement possible tout en offrant une bonne visibilité du pourquoi. L'Optional
permet de planter encore plus vite : à la compilation.
Le chapitre choisi la conception orientée objet
propose un exercice
sur des réservations. Pour faire simple, une PendingReservation
peut se transformer en une Reservation
uniquement si elle est planifiée (contrainte imposée par l’argument et vérifiée à la compilation) et s’il y a au moins une ressource. Cette dernière contrainte est vérifiée lors de l’exécution et lève une exception le cas échéant:
|
|
Une bonne technique pour éviter un problème et une levée d’exception lors de l’exécution est de l’éviter à la compilation:
|
|
Retourner un Optional
contraint l’utilisateur à gérer cette incohérence. C’est un avantage de plus des fabriques : contrairement à un constructeur qui retourne un objet, une fabrique peut retourner un autre type. Dans notre cas, un optionnel.
Attention
Rappelez-vous qu’une liste vide représente déjà une absence de valeurs, évitez ce genre de retour à vos méthodes:Optional<List<User>> maybeUsers(); // ceci est suffisant: List<User> users();
Composition #
Un autre avantage que vous verrez dans les exercices en cours est la composition; chose impossible avec des exceptions. Il est par exemple possible de chaîner les actions:
Optional<Email> emailOfToto = getUser(42).map( user -> user.email() );
// ou un autre exemple
getUser(42).filter( u -> u.age() >= 18 )
.map( u -> u.email() )
.ifPresent( email -> System.out.println(email) );