Explication des Optionals

La plupart de l’API Sponge utilise le système Optional de Java sur les accesseurs d’objets, mais si vous n’avez jamais utilisé Optional avant, cela peut sembler être une façon assez particulière de faire les choses. Vous pouvez être tentés de demander : « Pourquoi ai-je besoin d’effectuer une étape en plus lorsque je récupère quelque chose depuis un objet de l’API ? »

Cette section donne un bref sommaire de l”Optional et explique comment - et peut-être plus important pourquoi - il est utilisé à travers l’API Sponge.

Commençons avec une petite histoire, et regardez comment ces accesseurs - particulièrement les « getters » - fonctionnent lorsqu’ils n’utilisent pas Optional.

1. Contrats Nullables Implicites et Pourquoi Ils Sont Nuls

Disons que nous avons un objet d’API simple Entity avec une méthode getFoo() qui retourne le Foo de l’entité.

../../_images/optionals1.png

Dans les anciens temps d’autrefois, notre plugin pouvait récupérer et utiliser le Foo depuis notre entité en utilisant le getter comme ceci :

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

Le problème survient parce que - lors du design de l’API - nous devons compter sur un contrat implicite sur la méthode getFoo par rapport à si la méthode peut (ou ne peut pas) retourner null. Ce contrat implicite peut être défini de deux manières :

  • Dans le javadoc - c’est mauvais parce que ça compte sur le fait que l’auteur du plugin lise la javadoc de la méthode, et le contrat peut ne pas être clair pour l’auteur du plugin

  • Utiliser des annotations nullable - ce n’est pas l’idéal parce qu’en général ces annotations demandent un outil pour être utilisées, en comptant par exemple sur l’IDE ou le compileur pour gérer ces annotations.

../../_images/optionals2.png

Assumons que la méthode getFoo() peut - dans le cadre de son contrat - retourner null. Cela signifie soudainement que notre code ci-dessus est dangereux puisqu’il peut résulter en une NullPointerException si entityFoo est null.

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

Assumons que notre auteur de plugin soit conscient de la nature du nullable de notre méthode getFoo et décide de corriger le problème avec des null checks. Assumons qu’il a défini une constante locale Foo, le code résultat ressemble à ceci :

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

Dans cet exemple, l’auteur du plugin est conscient que la méthode puisse retourner null et a une constante disponible avec une instance par défaut de Foo qui peut être utilisée à la place. Évidemment le plugin pourrait juste court-circuiter l’appel en entier, ou il pourrait essayer de récupérer Foo depuis quelque part d’autre. Le message clé est que gérer les nulls même dans des cas simples peut mener à du code spaghetti assez rapidement, et de plus, compte sur le fait que l’auteur du plugin va visiter explicitement le contrat de la méthode pour vérifier si un null check est nécessaire.

Toutefois, ce n’est pas le seul inconvénient. Considérons l’API sur un plus long terme et assumons qu’au moment où l’auteur écrit le plugin, il visite la javadoc de la méthode et voit que la méthode ne retourne jamais null (puisque toutes les Entity a toujours un Foo de disponible). Bien ! Pas besoin de null check compliqué !

Toutefois, assumons maintenant que dans une version ultérieure du jeu, les développeurs du jeu suppriment ou déprécient le concept de Foo. Les auteurs de l’API mettent à jour l’API par conséquent et déclarent qu’à partir de maintenant la méthode getFoo() peut retourner null et l’écrivent dans la javadoc de la méthode. Maintenant il y a un problème : même les auteurs de plugin assidus qui ont vérifiés le contrat de la méthode quand ils ont écrit leur code gèrent involontairement la méthode incorrectement : avec aucun null check dans le code, utiliser le Foo retourné depuis getFoo va lever une NPE.

Donc nous pouvons voir qu’accepter des contrats nullables implicites nous laisse avec une sélection de solutions atroces à choisir :

  • Les auteurs de plugins peuvent assumer que toutes les méthodes peuvent retourner null et coder par conséquent de manière défensive, toutefois nous avons déjà vu que cela mène à du code spaghetti plutôt rapidement.

  • Les auteurs de l’API peuvent définir un contrat nullable implicite sur chaque méthode de l’API, en essayant de gérer le problème de l’auteur du plugin, ce qui ne fait qu’aggraver l’approche précédente.

  • Les auteurs de l’API peuvent affirmer que tous les contrats nullables implicites qu’ils définissent ne sont jamais altérés à l’avenir. Cela signifie que dans l’éventualité où ils ont besoin de gérer la suppression d’une fonctionnalité du jeu de base, alors ils doivent soit :

  • Lever une exception - guère élégant mais certainement plus facile à diagnostiquer qu’un NPE en vrac qui peut être déclenché ailleurs de le base de code et être difficile à traquer

  • Retourner un objet « fake » ou une valeur invalide - cela signifie que le code du consommant (plugin) va continuer de fonctionner, mais crée un fardeau toujours croissant sur les développeurs de l’API puisque chaque fonctionnalité dépréciée nécessitera la création d’encore plus d’objets fake. Cela pourrait rapidement conduire à la situation où une grande partie de l’API est remplie d’objets inutiles dont le seul but est de supporter les parties de l’API qui ne sont plus en service.

Cela devrait être assez clair à partir de maintenant qu’il y a des maux de tête assez importants attachés à ces contrats nullables implicites, d’autant plus poignants lorsque l’API en question est une couche sur un produit de base extrêmement instable. Heureusement, il existe une meilleure façon :

2. Les Optionals et les Contrats Nullables Explicites

Comme mentionné ci-dessus, les APIs pour Minecraft se trouvent dans une situation difficile. En fin de compte, ils ont besoin de fournir une plateforme avec un montant raisonnable de stabilité par dessus une plateforme (le jeu) avec absolument aucun montant de stabilité. Donc toutes les APIs pour Minecraft ont besoin d’être designées avec pleine conscience que n’importe quel aspect du jeu est susceptible de changer à tout moment pour une raison quelconque de n’importe quel façon imaginable; jusqu’à et y compris être supprimé entièrement !

Cette volatilité est ce qui conduit au problème avec les contrats de méthodes nullables décrites ci-dessus.

Optional résoud les problèmes ci-dessus en remplaçant les contrats implicites par des contrats explicites. L’API ne publie jamais, « Voici votre objet, kthxbai », à la place il présente des accesseurs avec un « Voici une boîte qui peut ou non contenir l’objet que vous avez demandé, ymmv ».

../../_images/optionals3.png

En encodant la possibilité de retourner null en un contrat explicite, nous remplaçons la notion de null checking avec le concept plus nuancé de peut ne pas exister. Nous stipulons aussi ce contrat depuis le premier jour.

Donc qu’est-ce que cela signifie ?

En un mot, les auteurs de plugins n’ont plus besoin de se soucier de la possibilité que null soit retourné. Au lieu de cela, la possibilité même qu’un objet particulier ne soit pas disponible devient codé dans l’étoffe même du code de leur plugin. Cela a, en soi, le même niveau de sécurité que d’effectuer constemment des null-checks, mais avec les avantages d’avoir un code bien plus élégant et lisible.

Pour vour pourquoi, jetons un oeil à l’exemple ci-dessus, converti pour utiliser une méthode getFoo qui retourne un Optional<Foo> à la place :

public void someEventHandler(Entity someEntity) {
    Optional<Foo> entityFoo = someEntity.getFoo();
    if (entityFoo.isPresent()) {
        entityFoo.get().bar();
    }
}

Vous pouvez noter que cet exemple ressemble beaucoup à un null-check standard, toutefois l’utilisation d’un Optional comporte actuellement un peu plus d’informations dans la même quantité de code. Par exemple, il n’est pas nécessaire pour quelqu’un qui lit le code ci-dessus de vérifier le contrat de la méthode, il est clair que cette méthode peut ne peut pas retourner une valeur, et la gestion de l’absence de la valeur est explicite et claire.

Et alors ? Notre contrat explicite entraîne dans ce cas essentiellement la même quantité de code qu’un null check - bien qu’un soit contractuellement enforcé par le getter. « Hop là ! », dîtes-vous, « et alors ? »

Et bien l’emboîtement Optional nous permet de prendre certains aspects traditionnellement plus maladroits de null-checking et de les rendre plus élégants : examinons le code suivant :

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO);
    entityFoo.bar();
}

2 minutes ! Est-ce que nous venons de remplacer le fastidieux null-check-et-assignement-par-défaut de l’exemple ci-dessus avec une seule ligne de code ? Oui en effet, nous l’avons fait. En fait, pour les cas d’usage simple nous pouvons même renoncer à l’affectation :

public void someEventHandler(Entity someEntity) {
    someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO).bar();
}

Ceci est parfaitement sûr étant donné que MyPlugin.DEFAULT_FOO est toujours disponible.

Considérez l’exemple suivant avec deux entités, nous voulons utiliser Foo de la première entité à l’aide d’un contrat nullable implicite, ou si non disponible utiliser Foo de la seconde entity, et se retirer sur notre valeur par défaut si aucun n’est disponible :

public void someEventHandler(Entity someEntity, Entity entity2) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = entity2.getFoo();
    }
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

En utilisant Optional nous pouvons encoder ceci beaucoup plus proprement :

public void someEventHandler(Entity someEntity, Entity entity2) {
    someEntity.getFoo().orElse(entity2.getFoo().orElse(MyPlugin.DEFAULT_FOO)).bar();
}

Ce n’est que la pointe de l’iceberg de l”Optional. Dans Java 8, Optional supporte aussi les interfaces Consumer et Supplier, permettant aux lambdas d’êtres utilisées comme relais d”absence. Des exemples d’utilisation peuvent être trouvés sur la page Exemples d’utilisation.

Note

Une autre explication sur la raison derrière l’évitement des références null peut être trouvée sur Guava : Using And Avoiding Null Explained. Méfiez-vous la classe guava Optional mentionnée dans l’article lié est différente de celle de Java java.util.Optional et auront donc des noms de méthodes différentes de celles utilisées ici.