Pourquoi L'option Java 8 ne devrait-elle pas être utilisée dans les arguments
J'ai lu sur de nombreux sites Web Optional devrait être utilisé comme type de retour seulement, et non utilisé dans les arguments de méthode. J'ai du mal à trouver une raison logique. Par exemple, j'ai un morceau de logique qui a 2 paramètres optionnels. Par conséquent, je pense qu'il serait logique d'écrire ma signature de méthode comme ceci (solution 1):
public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {
// my logic
}
De nombreuses pages web spécifient Optional ne doit pas être utilisé comme arguments de méthode. Dans cet esprit, je pourrais utiliser la signature de méthode suivante et Ajouter un clair Commentaire Javadoc pour spécifier que les arguments peuvent être null, en espérant que les futurs responsables liront le Javadoc et donc effectueront toujours des vérifications null avant d'utiliser les arguments (solution 2):
public int calculateSomething(String p1, BigDecimal p2) {
// my logic
}
Alternativement, je pourrais remplacer ma méthode par quatre méthodes publiques pour fournir une interface plus agréable et la rendre plus évidente p1 et p2 sont facultatifs (solution 3):
public int calculateSomething() {
calculateSomething(null, null);
}
public int calculateSomething(String p1) {
calculateSomething(p1, null);
}
public int calculateSomething(BigDecimal p2) {
calculateSomething(null, p2);
}
public int calculateSomething(String p1, BigDecimal p2) {
// my logic
}
Maintenant, j'essaie d'écrire le code de la classe qui invoque ce morceau de logique pour chaque approche. J'ai d'abord récupérer les deux paramètres d'entrée d'un autre objet qui renvoie Optional
s et ensuite, j'invoque calculateSomething
. Par conséquent, si la solution 1 est utilisée, le code appelant ressemblerait à ceci:
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1, p2);
Si la solution 2 est utilisée, le code appelant ressemblerait à ceci:
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1.orElse(null), p2.orElse(null));
Si la solution 3 est appliquée, je pourrais utiliser le code ci-dessus ou je pourrais utiliser ce qui suit (mais c'est beaucoup plus de code):
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result;
if (p1.isPresent()) {
if (p2.isPresent()) {
result = myObject.calculateSomething(p1, p2);
} else {
result = myObject.calculateSomething(p1);
}
} else {
if (p2.isPresent()) {
result = myObject.calculateSomething(p2);
} else {
result = myObject.calculateSomething();
}
}
Donc ma question est: Pourquoi est-il considéré comme une mauvaise pratique à utiliser Optional
s comme méthode arguments (voir la solution 1)?{[24] } cela me semble être la solution la plus lisible et rend le plus évident que les paramètres pourraient être vides / null pour les futurs mainteneurs. (Je suis conscient que les concepteurs de Optional
avaient l'intention de l'utiliser uniquement comme type de retour, mais je ne trouve aucune raison logique de ne pas l'utiliser dans ce scénario).
18 réponses
Oh, ces styles de codage doivent être pris avec un peu de sel.
- (+) passer un résultat optionnel à une autre méthode, sans aucune analyse sémantique; laisser cela à la méthode, est tout à fait correct.
- ( - ) L'utilisation de paramètres optionnels provoquant une logique conditionnelle à l'intérieur des méthodes est littéralement contre-productive.
- ( - ) ayant besoin d'emballer un argument dans une option, est sous-optimal pour le compilateur, et fait un wrapping inutile.
- ( -) par rapport à nullable paramètres optionnels est plus coûteux.
En général: optionnel unifie deux états, qui doivent être démêlés. Donc mieux adapté pour le résultat que l'entrée, de la complexité du flux de données.
Le meilleurs post - j'ai vu sur le sujet a été écrit par Daniel Olszewski:
Bien qu'il puisse être tentant de considérer optionnel pour des paramètres de méthode non obligatoires, une telle solution est pâle en comparaison avec d'autres alternatives possibles. Pour illustrer le problème, examinez la déclaration du constructeur suivante:
public SystemMessage(String title, String content, Optional<Attachment> attachment) { // assigning field values }
À première vue, il peut sembler comme une bonne décision de conception. Après tout, nous explicitement marqué le paramètre de pièce jointe comme facultatif. Cependant, comme pour appeler le constructeur, le code client peut devenir un peu maladroit.
SystemMessage withoutAttachment = new SystemMessage("title", "content", Optional.empty()); Attachment attachment = new Attachment(); SystemMessage withAttachment = new SystemMessage("title", "content", Optional.ofNullable(attachment));
Au lieu de fournir la clarté, les méthodes d'usine de L'option classe seulement distraire le lecteur. Remarque il n'y a qu'une seule option paramètre, mais imaginez avoir deux ou trois. Oncle Bob certainement Je ne serais pas fier d'un tel code
Lorsqu'une méthode peut accepter des paramètres facultatifs, il est préférable d'adopter l'approche éprouvée et de concevoir un tel cas en utilisant la méthode surcharger. dans L'exemple de la classe SystemMessage, déclarant deux constructeurs distincts sont supérieurs à L'Utilisation facultative.
public SystemMessage(String title, String content) { this(title, content, null); } public SystemMessage(String title, String content, Attachment attachment) { // assigning field values }
Ce changement rend le code client beaucoup plus simple et plus facile à lire.
SystemMessage withoutAttachment = new SystemMessage("title", "content"); Attachment attachment = new Attachment(); SystemMessage withAttachment = new SystemMessage("title", "content", attachment);
Il n'y a presque aucune bonne raison de ne pas utiliser {[0] } comme paramètres. Les arguments contre cela reposent sur des arguments de l'autorité (voir Brian Goetz - son argument est que nous ne pouvons pas appliquer des options non nulles) ou que les arguments Optional
peuvent être nuls (essentiellement le même argument). Bien sûr, toute référence en Java peut être nulle, nous devons encourager les règles appliquées par le compilateur, pas la mémoire des programmeurs (ce qui est problématique et ne s'adapte pas).
Langages de programmation fonctionnels encourager Optional
paramètres. L'une des meilleures façons d'utiliser ceci est d'avoir plusieurs paramètres optionnels et d'utiliser liftM2
pour utiliser une fonction en supposant que les paramètres ne sont pas vides et en retournant une option (Voir http://www.functionaljava.org/javadoc/4.4/functionaljava/fj/data/Option.html#liftM2-fj.F - ). Java 8 a malheureusement implémenté une bibliothèque très limitée supportant optionnel.
En tant que programmeurs Java, nous ne devrions utiliser null que pour interagir avec les bibliothèques héritées.
Ce conseil est une variante de la règle empirique" soyez aussi peu précis que possible en ce qui concerne les intrants et aussi précis que possible en ce qui concerne les extrants".
Habituellement, si vous avez une méthode qui prend une valeur non nulle, vous pouvez la mapper sur Optional
, donc la version simple est strictement plus non spécifique en ce qui concerne les entrées. cependant Il y a un tas de raisons possibles pour lesquelles vous voudriez exiger un argument Optional
néanmoins:
- vous voulez que votre fonction soit utilisée dans conjonction avec une autre API qui renvoie un
Optional
- Votre fonction doit renvoyer autre chose qu'un
Optional
vide si la valeur donnée est vide vous pensez queOptional
est si génial que quiconque utilise votre API devrait être tenu d'en apprendre davantage à ce sujet ;-)
La Optional
est de un à éviter les retour null
. Il est toujours parfaitement possible de passer null
à une méthode.
Bien que ce ne soit pas encore vraiment officiel, vous pouvez utiliser les annotations JSR-308 style pour indiquer si vous acceptez ou non les valeurs null
dans la fonction. Notez que vous devriez avoir le bon outillage pour l'identifier réellement, et cela fournirait plus d'une vérification statique qu'une stratégie d'exécution exécutoire, mais cela aiderait.
public int calculateSomething(@NotNull final String p1, @NotNull final String p2) {}
Cela me semble un peu stupide, mais la seule raison à laquelle je peux penser est que les arguments d'objet dans les paramètres de méthode sont déjà facultatifs - ils peuvent être nuls. Par conséquent, forcer quelqu'un à prendre un objet existant et à l'envelopper dans une option est en quelque sorte inutile.
Cela étant dit, enchaîner des méthodes qui prennent / retournent des options est une chose raisonnable à faire, par exemple peut-être monad.
Ma prise est que Optional devrait être une monade et ceux-ci ne sont pas concevables en Java.
En programmation fonctionnelle, vous traitez des fonctions d'ordre pur et supérieur qui prennent et composent leurs arguments uniquement en fonction de leur "type de domaine métier". Composer des fonctions qui alimentent, ou dont le calcul doit être signalé, le monde réel (soi-disant effets secondaires) nécessite l'application de fonctions qui prennent soin de déballer automatiquement les valeurs des monades représentant le monde extérieur (État, Configuration, Futures, peut-être, soit, écrivain, etc...); cela s'appelle de levage. Vous pouvez la considérer comme une sorte de séparation des préoccupations.
Mélanger ces deux niveaux d'abstraction ne facilite pas la lisibilité, il vaut donc mieux l'éviter.
Une autre raison d'être prudent quand passer un Optional
comme paramètre est qu'une méthode devrait faire une chose... Si vous passez un paramètre Optional
, vous pourriez favoriser plus d'une chose, cela pourrait être similaire à passer un paramètre booléen.
public void method(Optional<MyClass> param) {
if(param.isPresent()) {
//do something
} else {
//do some other
}
}
Je pense que c'est parce que vous écrivez habituellement vos fonctions pour manipuler les données, puis les soulever à Optional
en utilisant map
et des fonctions similaires. Cela ajoute le comportement par défaut Optional
.
Bien sûr, il peut y avoir des cas, quand il est nécessaire d'écrire votre propre fonction auxiliaire qui fonctionne sur Optional
.
Je crois que la réponse de l'être est que vous devez d'abord vérifier si Optional est null lui-même et ensuite essayer d'évaluer la valeur qu'il enveloppe. Trop de validations inutiles.
Les options ne sont pas conçues à cet effet, comme expliqué joliment par Brian Goetz.
Vous pouvez toujours utiliser @ Nullable pour indiquer qu'un argument de méthode peut être null. L'utilisation d'une option Ne vous permet pas vraiment d'écrire votre logique de méthode plus proprement.
Accepter optionnel comme paramètres provoque un habillage inutile au niveau de l'appelant.
Par exemple dans le cas de:
public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {}
Supposons que vous ayez deux chaînes non nulles (ie. renvoyé d'une autre méthode):
String p1 = "p1";
String p2 = "p2";
Vous êtes obligé de les envelopper en option même si vous savez qu'ils ne sont pas vides.
Cela devient encore pire lorsque vous devez composer avec d'autres structures "mappables", c'est-à-dire. Eithers:
Either<Error, String> value = compute().right().map((s) -> calculateSomething(
< here you have to wrap the parameter in a Optional even if you know it's a
string >));
Réf:
Les méthodes ne devraient pas attendre Option comme paramètres, c'est presque toujours un code odeur qui a indiqué une fuite de contrôle flux de l'appelant à l'appelé, il devrait être de la responsabilité de l'appelant de vérifier le contenu D'une Option
Réf. https://github.com/teamdigitale/digital-citizenship-functions/pull/148#discussion_r170862749
Utiliser Optional
comme arguments de méthode nécessite également de null
Vérifier lui - même. Logique en double inutile. Par exemple: -
void doSomething(Optional<ISOCode> isoOpt, Optional<Product> prodOpt){
if(isoOpt != null){
isoOpt.orElse(ISOCode.UNKNOWN);
}
if(prodOpt != null){
prodOpt.orElseThrow(NullPointerException::new);
}
//more instructions
}
Imaginez si le nombre de paramètres Optional
augmente. Si vous oubliez la vérification null, NPE sera éventuellement lancé à l'exécution.
De plus, si votre méthode est publique, le client sera obligé d'envelopper ses paramètres dans des options, ce qui sera un fardeau surtout si le nombre d'arguments augmente.
Une autre approche, ce que vous pouvez faire est
// get your optionals first
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
// bind values to a function
Supplier<Integer> calculatedValueSupplier = () -> { // your logic here using both optional as state}
Une fois que vous avez construit une fonction (fournisseur dans ce cas), vous pourrez la transmettre comme toute autre variable et l'appeler en utilisant
calculatedValueSupplier.apply();
L'idée ici étant de savoir si vous avez une valeur optionnelle ou non sera un détail interne de votre fonction et ne sera pas en paramètre. Penser les fonctions en pensant à optionnel comme paramètre est en fait une technique très utile que j'ai trouvé.
Quant à votre question de savoir si vous devriez réellement le faire ou non est basée sur vos préférences, mais comme d'autres l'ont dit, cela rend votre API pour le moins moche.
Au début, j'ai également préféré passer des options en tant que paramètre, mais si vous passez d'une perspective API-Designer à une perspective API-User, vous voyez les inconvénients.
Pour votre exemple, où chaque paramètre est facultatif, je suggère de changer la méthode de calcul en une propre classe comme suit:
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
MyCalculator mc = new MyCalculator();
p1.map(mc::setP1);
p2.map(mc::setP2);
int result = mc.calculate();
Je sais que cette question porte plus sur l'opinion que sur des faits concrets. Mais je suis récemment passé d'un développeur. net à un développeur java, donc je n'ai que récemment rejoint la partie facultative. En outre, je préférerais indiquer ceci comme un commentaire, mais puisque mon niveau de point ne me permet pas de commenter, je suis obligé de mettre ceci comme une réponse à la place.
Ce que j'ai fait, qui m'a bien servi en règle générale. Est d'utiliser des optionnels pour les types de retour, et d'utiliser uniquement des optionnels comme paramètres, si j'ai besoin à la fois de la valeur de L'option, et de la météo ou non L'option avait une valeur dans la méthode.
Si Je ne me soucie que de la valeur, je vérifie isPresent avant d'appeler la méthode, si j'ai une sorte de journalisation ou une logique différente dans la méthode qui dépend si la valeur existe, alors je passerai volontiers dans L'option.
C'est parce que nous avons des exigences différentes pour un utilisateur D'API et un développeur D'API.
Un développeur est responsable de fournir une spécification précise et une implémentation correcte. Par conséquent, si le développeur est déjà conscient qu'un argument est facultatif, l'implémentation doit le traiter correctement, qu'il soit null ou facultatif. L'API doit être aussi simple que possible pour l'utilisateur, et null est le plus simple.
D'autre part, le résultat est passé à partir du Développeur D'API à l'utilisateur. Cependant, la spécification est complète et verbeuse, il y a toujours une chance que l'utilisateur ne l'ignore pas ou simplement paresseux pour y faire face. Dans ce cas, le résultat optionnel oblige l'utilisateur à écrire du code supplémentaire pour gérer un résultat vide possible.
Tout d'Abord, si vous utilisez la méthode 3, vous pouvez remplacer ces 14 dernières lignes de code avec ceci:
int result = myObject.calculateSomething(p1.orElse(null), p2.orElse(null));
Les quatre variations que vous avez écrites sont des méthodesconvenience . Vous ne devriez les utiliser que quand ils sont plus pratiques. C'est aussi la meilleure approche. De cette façon, L'API est très claire quels membres sont nécessaires et lesquels ne le sont pas. si vous ne voulez pas écrire quatre méthodes, vous pouvez clarifier les choses par la façon dont vous nommez vos paramètres:
public int calculateSomething(String p1OrNull, BigDecimal p2OrNull)
De cette façon, il est clair que les valeurs null sont autorisées.
Votre utilisation de p1.orElse(null)
illustre à quel point notre code est verbeux lors de l'utilisation D'Optional, ce qui explique pourquoi je l'évite. Facultatif a été écrit pour la programmation fonctionnelle. Les flux en ont besoin. Vos méthodes ne devraient probablement jamais retourner en option sauf s'il est nécessaire de les utiliser dans la programmation fonctionnelle. Il existe des méthodes, comme la méthode Optional.flatMap()
, qui nécessite une référence à une fonction qui renvoie optionnelle. Voici son signature:
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)
C'est donc généralement la seule bonne raison d'écrire une méthode qui renvoie optionnelle. Mais même là, cela peut être évité. Vous pouvez passer un getter qui ne retourne pas optionnel à une méthode comme flatMap (), en l'enveloppant dans une autre méthode qui convertit la fonction au bon type. La méthode wrapper ressemble à ceci:
public static <T, U> Function<? super T, Optional<U>> optFun(Function<T, U> function) {
return t -> Optional.ofNullable(function.apply(t));
}
Supposons Donc que vous avez un getter comme ceci: String getName()
Vous ne pouvez pas le passer à flatMap comme ceci:
opt.flatMap(Widget::getName) // Won't work!
Mais vous pouvez le passer comme ceci:
opt.flatMap(optFun(Widget::getName)) // Works great!
En dehors de la programmation fonctionnelle, les options doivent être évitées.
Brian Goetz l'a dit le mieux quand il a dit ceci:
La raison pour laquelle Optional a été ajouté à Java est que ceci:
return Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
.stream()
.filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
.filter(m -> Arrays.equals(m.getParameterTypes(), parameterClasses))
.filter(m -> Objects.equals(m.getReturnType(), returnType))
.findFirst()
.getOrThrow(() -> new InternalError(...));
Est plus propre que ceci:
Method matching =
Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
.stream()
.filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
.filter(m -> Arrays.equals(m.getParameterTypes(), parameterClasses))
.filter(m -> Objects.equals(m.getReturnType(), returnType))
.getFirst();
if (matching == null)
throw new InternalError("Enclosing method not found");
return matching;