Je me demande si je devrais me défendre contre la valeur de retour d'un appel de méthode en validant qu'il répond à mes attentes même si je sais que la méthode que j'appelle répondra à ces attentes.
DONNÉ
User getUser(Int id)
{
User temp = new User(id);
temp.setName("John");
return temp;
}
DEVRAIS-JE
void myMethod()
{
User user = getUser(1234);
System.out.println(user.getName());
}
OR
void myMethod()
{
User user = getUser(1234);
// Validating
Preconditions.checkNotNull(user, "User can not be null.");
Preconditions.checkNotNull(user.getName(), "User's name can not be null.");
System.out.println(user.getName());
}
Je pose cette question au niveau conceptuel. Si je connais le fonctionnement interne de la méthode que j'appelle. Soit parce que je l'ai écrit ou que je l'ai inspecté. Et la logique des valeurs possibles qu'elle renvoie répond à mes conditions préalables. Est-il "préférable" ou "plus approprié" d'ignorer la validation, ou dois-je tout de même me défendre contre des valeurs erronées avant de poursuivre avec la méthode que j'implémente actuellement, même si elle doit toujours réussir.
Cela dépend de la probabilité que getUser et myMethod changent, et plus important encore, quelle est leur probabilité de changer indépendamment l'un de l'autre.
Si vous savez d'une manière ou d'une autre que getUser ne changera jamais, jamais, jamais à l'avenir, alors oui c'est une perte de temps de le valider, autant que de perdre du temps à valider que i
a une valeur de 3 immédiatement après un i = 3;
déclaration. En réalité, vous ne le savez pas. Mais vous savez certaines choses:
Ces deux fonctions "cohabitent-elles"? En d'autres termes, ont-ils les mêmes personnes qui les gèrent, font-ils partie du même fichier et sont-ils donc susceptibles de rester "synchronisés" les uns avec les autres? Dans ce cas, il est probablement exagéré d'ajouter des vérifications de validation, car cela signifie simplement plus de code qui doit changer (et potentiellement générer des bogues) chaque fois que les deux fonctions changent ou sont refactorisées en un nombre différent de fonctions.
La fonction getUser fait-elle partie d'une API documentée avec un contrat spécifique, et myMethod est-il simplement un client de ladite API dans une autre base de code? Si c'est le cas, vous pouvez lire cette documentation pour savoir si vous devez valider les valeurs de retour (ou pré-valider les paramètres d'entrée!) Ou s'il est vraiment sûr de suivre aveuglément le chemin heureux. Si la documentation ne le dit pas clairement, demandez au responsable de corriger cela.
Enfin, si cette fonction particulière a soudainement et de manière inattendue changé son comportement dans le passé, d'une manière qui a cassé votre code, vous avez le droit d'être paranoïaque à son sujet. Les bugs ont tendance à se regrouper.
Notez que tout ce qui précède s'applique même si vous êtes l'auteur original des deux fonctions. Nous ne savons pas si ces deux fonctions devraient "vivre ensemble" pour le reste de leur vie, ou si elles se sépareront lentement en modules séparés, ou si vous vous êtes tiré une balle dans le pied avec un bug dans les anciennes versions. versions de getUser. Mais vous pouvez probablement faire une supposition assez décente.
La réponse d'Ixrec est bonne, mais je vais adopter une approche différente car je pense que cela vaut la peine d'être considéré. Aux fins de cette discussion, je vais parler d'assertions, car c'est le nom par lequel votre Preconditions.checkNotNull()
est traditionnellement connue.
Ce que je voudrais suggérer, c'est que les programmeurs surestiment souvent le degré auquel ils sont certains que quelque chose se comportera d'une certaine manière, et sous-estimeront les conséquences de ne pas se comporter de cette façon. De plus, les programmeurs ne réalisent souvent pas la puissance des assertions comme documentation. Par la suite, d'après mon expérience, les programmeurs affirment les choses beaucoup moins souvent qu'ils ne le devraient.
Ma devise est:
La question que vous devriez toujours vous poser n'est pas "devrais-je affirmer cela?" mais "y a-t-il quelque chose que j'ai oublié d'affirmer?"
Naturellement, si vous êtes absolument sûr que quelque chose se comporte d'une certaine manière, vous vous abstiendrez d'affirmer que c'est effectivement le cas se comporter de cette façon, et c'est pour la plupart raisonnable. Il n'est pas vraiment possible d'écrire un logiciel si vous ne pouvez faire confiance à rien.
Mais il existe même des exceptions à cette règle. Curieusement, pour prendre l'exemple d'Ixrec, il existe un type de situation où il est parfaitement souhaitable de suivre int i = 3;
En affirmant que i
est en effet dans une certaine plage. C'est la situation où i
est susceptible d'être modifié par quelqu'un qui essaie divers scénarios de simulation, et le code qui suit s'appuie sur i
ayant une valeur dans une certaine plage, et il n'est pas immédiatement évident en regardant rapidement le code suivant quelle est la plage acceptable. Donc, int i = 3; assert i >= 0 && i < 5;
Peut à première vue sembler absurde, mais si vous y réfléchissez, il vous indique que vous pouvez jouer avec i
, et il vous indique également la plage dans laquelle vous devez rester. Une assertion est le meilleur type de documentation, car elle est appliquée par la machine , c'est donc une garantie parfaite, et il est refactorisé avec le code , il reste donc toujours pertinent.
Votre exemple getUser(int id)
n'est pas un parent conceptuel très éloigné de l'exemple assign-constant-and-assert-in-range
. Vous voyez, par définition, des bugs se produisent lorsque les choses présentent un comportement différent de celui que nous attendions. Certes, nous ne pouvons pas tout affirmer, donc parfois il y aura un débogage. Mais c'est un fait bien établi dans l'industrie que plus nous détectons rapidement une erreur, moins cela coûte cher. Les questions que vous devez poser ne sont pas seulement de savoir si vous êtes sûr que getUser()
ne renverra jamais null, (et ne renverra jamais un utilisateur avec un nom nul,) mais aussi, quels types de mauvaises choses se produiront avec le reste du code s'il en fait un jour retourne quelque chose d'inattendu, et comme c'est facile en regardant rapidement le reste du code pour savoir précisément ce qu'on attend de getUser()
.
Si le comportement inattendu entraînerait la corruption de votre base de données, vous devriez peut-être affirmer même si vous êtes sûr à 101% que cela ne se produira pas. Si un nom d'utilisateur null
provoquait une erreur cryptique étrange introuvable, des milliers de lignes plus loin et des milliards de cycles d'horloge plus tard, il est peut-être préférable d'échouer le plus tôt possible.
Donc, même si je ne suis pas en désaccord avec la réponse d'Ixrec, je vous suggère de considérer sérieusement le fait que les affirmations ne coûtent rien, donc il n'y a vraiment rien à perdre, seulement à gagner, en les utilisant généreusement.
n numéro définitif.
Un appelant ne doit jamais vérifier si la fonction qu'il appelle respecte son contrat. La raison est simple: il y a potentiellement de très nombreux appelants et il est peu pratique et sans doute même erroné d'un point de vue conceptuel pour chaque appelant de dupliquer la logique de vérification des résultats d'autres fonctions.
Si des contrôles doivent être effectués, chaque fonction doit vérifier ses propres post-conditions. Les conditions postérieures vous donnent l'assurance que vous avez correctement mis en œuvre la fonction et les utilisateurs pourront se fier au contrat de votre fonction.
Si une certaine fonction n'est pas censée retourner null, cela doit être documenté et faire partie du contrat de cette fonction. C'est le but de ces contrats: offrir aux utilisateurs des invariants sur lesquels ils peuvent compter pour qu'ils rencontrent moins d'obstacles lors de l'écriture de leurs propres fonctions.
Je dirais que la prémisse de votre question est fausse: pourquoi utiliser une référence nulle pour commencer?
Le modèle d'objet nul est un moyen de renvoyer des objets qui ne font essentiellement rien: plutôt que de renvoyer null pour votre utilisateur, renvoyez un objet qui représente "aucun utilisateur".
Cet utilisateur serait inactif, aurait un nom vide et d'autres valeurs non nulles indiquant "rien ici". Le principal avantage est que vous n'avez pas besoin de salir votre programme pour annuler les vérifications, la journalisation, etc. vous utilisez simplement vos objets.
Pourquoi un algorithme devrait-il se soucier d'une valeur nulle? Les étapes doivent être les mêmes. Il est tout aussi facile et beaucoup plus clair d'avoir un objet nul qui renvoie zéro, une chaîne vide, etc.
Voici un exemple de vérification de code pour null:
User u = getUser();
String displayName = "";
if (u != null) {
displayName = u.getName();
}
return displayName;
Voici un exemple de code utilisant un objet nul:
return getUser().getName();
Qu'est-ce qui est plus clair? Je crois que le deuxième est.
Bien que la question soit balisée Java , il convient de noter que le modèle d'objet nul est vraiment utile en C++ lors de l'utilisation de références. Java sont plus comme des pointeurs C++ et autorisent null. Les références C++ ne peuvent pas contenir nullptr
. Si l'on souhaite avoir quelque chose d'analogue à un null référence en C++, le modèle d'objet nul est le seul moyen que je connaisse pour y parvenir.
Si null est une valeur de retour valide de la méthode getUser, votre code doit gérer cela avec un If, pas une exception - ce n'est pas un cas exceptionnel.
Si null n'est pas une valeur de retour valide de la méthode getUser, alors il y a un bogue dans getUser - la méthode getUser doit lever une exception ou sinon être corrigée.
Si la méthode getUser fait partie d'une bibliothèque externe ou ne peut pas être modifiée autrement et que vous souhaitez que des exceptions soient levées dans le cas d'un retour nul, encapsulez la classe et la méthode pour effectuer les vérifications et lever l'exception afin que chaque instance de l'appel à getUser est cohérent dans votre code.
En général, je dirais que cela dépend principalement des trois aspects suivants:
robustesse: la méthode appelante peut-elle supporter la valeur null
? Si une valeur null
peut entraîner un RuntimeException
, je recommanderais une vérification - à moins que la complexité soit faible et que les méthodes appelées/appelantes soient créées par le même auteur et que null
soit pas prévu (par exemple les deux private
dans le même paquet).
responsabilité du code: si le développeur A est responsable de la méthode getUser()
et que le développeur B l'utilise (par exemple dans le cadre d'une bibliothèque), je recommande fortement de valider sa valeur. Tout simplement parce que le développeur B peut ne pas être au courant d'une modification qui entraîne une valeur de retour potentielle null
.
complexité: plus la complexité globale du programme ou de l'environnement est élevée, plus je recommanderais de valider la valeur de retour. Même si vous êtes sûr que cela ne peut pas être null
aujourd'hui dans le contexte de la méthode d'appel, il se peut que vous deviez changer getUser()
pour un autre cas d'utilisation. Une fois que certains mois ou années ont disparu et que des milliers de lignes de codes ont été ajoutées, cela peut être un piège.
De plus, je recommande de documenter les valeurs de retour potentielles null
dans un commentaire JavaDoc. En raison de la mise en évidence des descriptions JavaDoc dans la plupart des IDE, cela peut être un avertissement utile pour quiconque utilise getUser()
.
/** Returns ....
* @param: id id of the user
* @return: a User instance related to the id;
* null, if no user with identifier id exists
**/
User getUser(Int id)
{
...
}