Fonction renvoyant vrai / faux vs vide en cas de réussite et levée d'une exception en cas d'échec

22

Je construis une API, une fonction qui télécharge un fichier. Cette fonction ne retournera rien / annulera si le fichier a été téléchargé correctement et lève une exception en cas de problème.

Pourquoi une exception et pas seulement une fausse? Parce qu'à l'intérieur d'une exception je peux spécifier la raison de l'échec (pas de connexion, nom de fichier manquant, mot de passe erroné, description de fichier manquante, etc.). Je voulais créer une exception personnalisée (avec une énumération pour aider l'utilisateur de l'API à gérer toutes les erreurs).

Est-ce une bonne pratique ou vaut-il mieux retourner un objet (avec un booléen à l'intérieur, un message d'erreur optionnel et l'énumération des erreurs)?

Accollativo
la source
5
Copie possible de la valeur
moucher
59
Le vrai et le faux vont bien ensemble. Le vide et l'exception vont bien ensemble. Le vrai et l'exception vont de pair comme les chats et les taxes. Un jour, je lirai peut-être votre code. S'il vous plaît, ne me faites pas ça.
candied_orange
4
J'aime bien les modèles où un objet est renvoyé qui encapsule le succès ou l'échec. Par exemple, Rust a Result<T, E>Test le résultat en cas de succès et El'erreur en cas d'échec. Lancer une exception (peut être - pas si sûr en Java) peut être coûteux car cela implique de dérouler la pile d'appels, mais la création d'un objet Exception est bon marché. Évidemment, respectez les modèles établis du langage que vous utilisez, mais ne combinez pas les booléens et les exceptions comme celle-ci.
Dan Pantry
4
Faites attention ici. Vous vous dirigez peut-être dans un terrier de lapin. Lorsque votre programme est capable de traiter un problème, il est généralement plus facile de le détecter et de le traiter en vérifiant les conditions préalables. Vous interceptez rarement une exception, examinez ses données dans le code et réessayez après avoir résolu le problème. En tant que tel, il est rarement utile d'avoir de très nombreux types d'exceptions. Si vous essayez de consigner des problèmes, cela a beaucoup plus de sens, mais ne vous attendez pas à écrire un tas de blocs catch avec des quantités importantes de code.
jpmc26
4
@DanPantry En Java, la pile d'appels est vérifiée lors de la création de l'exception, pas lors de son lancement. fillInStackTrace();est appelé dans le super constructeur, dans la classeThrowable
Simon Forsberg

Réponses:

35

Lancer une exception est simplement un moyen supplémentaire de faire en sorte qu'une méthode retourne une valeur. L'appelant peut vérifier une valeur de retour aussi facilement que détecter une exception et vérifier cela. Par conséquent, décider entre throwet returnnécessite d'autres critères.

Le lancement d'exceptions doit souvent être évité s'il met en danger l'efficacité de votre programme (la construction d'un objet d'exception et le déroulement de la pile d'appels représentent beaucoup plus de travail pour l'ordinateur que de simplement y insérer une valeur). Mais si le but de votre méthode est de télécharger un fichier, le goulot d'étranglement sera toujours les E / S du réseau et du système de fichiers, il est donc inutile d'optimiser la méthode de retour.

C'est également une mauvaise idée de lever des exceptions pour ce qui devrait être un simple flux de contrôle (par exemple lorsqu'une recherche réussit en trouvant sa valeur), car cela viole les attentes des utilisateurs d'API. Mais une méthode qui ne remplit pas son objectif est un cas exceptionnel (ou du moins elle devrait l'être), donc je ne vois aucune raison de ne pas lever d'exception. Et si vous faites cela, vous pourriez tout aussi bien en faire une exception personnalisée et plus informative (mais c'est une bonne idée d'en faire une sous-classe d'une exception standard plus générale comme IOException).

Kilian Foth
la source
7
S'il s'agit d'un résultat escompté pour l'échec d'une demande de téléchargement de fichier, je serais surpris par une exception levée en face (surtout s'il y a une valeur de retour dans la signature de la méthode).
hoffmale
1
Notez que la raison pour laquelle il étend une exception standard est que de nombreux appelants (probablement même la plupart) n'écriront pas de code pour essayer de résoudre le problème. En conséquence, la plupart des appelants voudront un simple «intercepter ce grand groupe d'exceptions», sans avoir à les répertorier tous explicitement.
jpmc26
4
@gbjbaanb C'est pourquoi les exceptions doivent être utilisées quand il y a vraiment une situation non réparable. Lorsque vous essayez d'ouvrir un fichier et que le fichier ne peut pas être lu, c'est un bon cas pour une exception. Lorsque vous vérifiez l'existence d'un fichier, un booléen doit être utilisé car il est fort probable que le fichier ne soit pas là.
Malcolm
21
Kilian dit, dans le slogan récursif "les exceptions sont pour des situations exceptionnelles", des situations exceptionnelles sont lorsque la fonction ne peut pas remplir son objectif. Si la fonction est censée lire un fichier et que le fichier ne peut pas être lu, c'est exceptionnel . Si la fonction est censée vous dire si un fichier existe ou non (sans parler du potentiel TOCTOU), alors le fichier non existant n'est pas exceptionnel car la fonction peut toujours faire ce qu'elle est censée faire. Si la fonction est supposée affirmer qu'un fichier existe, alors il n'existe pas est exceptionnel car c'est ce que signifie "affirmer".
Steve Jessop
10
Notez que la seule différence entre une fonction qui vous indique si un fichier existe et une fonction qui affirme qu'un fichier existe, est de savoir si le cas du fichier inexistant est exceptionnel. Les gens parlent parfois comme si c'était une propriété de la condition d'erreur, qu'elle soit ou non "exceptionnelle". Ce n'est pas une propriété de la condition d'erreur, c'est une propriété combinée de la condition d'erreur et du but de la fonction dans laquelle elle se produit. Sinon, nous ne prendrions jamais d'exceptions.
Steve Jessop
31

Il n'y a absolument aucune raison de revenir truesur le succès si vous ne revenez pas falsesur l'échec. À quoi devrait ressembler le code client?

if (result = tryMyAPICall()) {
    // business logic
}
else {
    // this will *never* happen anyways
}

Dans ce cas, l' appelant a quand même besoin d'un bloc try-catch, mais il peut alors mieux écrire:

try {
    result = tryMyAPICall();
    // business logic
    // will only reach this line when no exception
    // no reason to use an if-condition
} catch (SomeException se) { }

La truevaleur de retour est donc complètement hors de propos pour l'appelant. Il suffit donc de garder la méthode void.


En général, il existe trois façons de concevoir des modes de défaillance.

  1. Retour vrai / faux
  2. Utiliser void, lever (vérifié) l'exception
  3. renvoie un objet de résultat intermédiaire .

Retour true/false

Ceci est utilisé dans certaines API plus anciennes, principalement de style C. Les inconvénients sont évidents, vous n'avez aucune idée de ce qui s'est mal passé. PHP le fait assez souvent, conduisant à du code comme celui-ci:

if (xyz_parse($data) === FALSE)
   $error = xyz_last_error();

Dans des contextes multithread, c'est encore pire.

Lancer des exceptions (cochées)

C'est une belle façon de procéder. À certains moments, vous pouvez vous attendre à un échec. Java le fait avec des sockets. L'hypothèse de base est qu'un appel doit réussir, mais tout le monde sait que certaines opérations peuvent échouer. Les connexions de socket en font partie. Ainsi, l'appelant est obligé de gérer l'échec. C'est un design agréable, car il garantit que l'appelant gère réellement l'échec et lui donne un moyen élégant de gérer l'échec.

Renvoyer l'objet de résultat

C'est une autre belle façon de gérer cela. Son souvent utilisé pour l'analyse ou tout simplement les choses qui doivent être validées.

ValidationResult result = parser.validate(data);
if (result.isValid())
    // business logic
else
    error = result.getvalidationError();

Belle logique propre pour l'appelant également.

Il y a un peu de débat quand utiliser le deuxième cas et quand utiliser le troisième. Certaines personnes croient que les exceptions doivent être exceptionnelles et que vous ne devez pas concevoir avec la possibilité d'exceptions à l'esprit, et utiliserez presque toujours la troisième option. C'est bien. Mais nous avons vérifié les exceptions en Java, donc je ne vois aucune raison de ne pas les utiliser. J'utilise des expetions vérifiées lorsque l'hypothèse de base est que l'appel doit réussir (comme l'utilisation d'une socket), mais l'échec est possible, et j'utilise la troisième option lorsque son très peu clair si l'appel doit réussir (comme la validation des données). Mais il y a des opinions différentes à ce sujet.

Dans votre cas, j'irais avec void+ Exception. Vous vous attendez à ce que le téléchargement du fichier réussisse, et quand ce n'est pas le cas, c'est exceptionnel. Mais l'appelant est obligé de gérer ce mode d'échec et vous pouvez renvoyer une exception qui décrit correctement le type d'erreur qui s'est produite.

Polygnome
la source
5

Cela revient vraiment à savoir si une défaillance est exceptionnelle ou attendue .

Si une erreur n'est pas quelque chose que vous attendez généralement, il est raisonnablement probable que l'utilisateur de l'API appellera simplement votre méthode sans aucune gestion d'erreur particulière. Lancer une exception permet de faire bouillonner la pile à un endroit où elle se fait remarquer.

Si, d'autre part, les erreurs sont courantes, vous devez optimiser pour le développeur qui les vérifie, et les try-catchclauses sont un peu plus encombrantes qu'une if/elifou switchsérie.

Concevez toujours une API dans l'état d'esprit de quelqu'un qui l'utilise.

Xiong Chiamiov
la source
1
Dans mon cas, comme quelqu'un l'a souligné, cela devrait être un échec exceptionnel car l'utilisateur de l'API ne peut pas télécharger le fichier.
Accollativo
4

Ne renvoyez pas un booléen si vous n'avez jamais l'intention de revenir false. Faites juste la méthode voidet documentez qu'elle lèvera une exception ( IOExceptionconvient) en cas d'échec.

La raison en est que si vous retournez un booléen, l'utilisateur de votre API peut conclure qu'il peut le faire:

if (fileUpload(file) == false) {
    // Handle failure.
}

Cela ne fonctionnera pas, bien sûr; c'est-à-dire qu'il y a une incongruité entre le contrat de votre méthode et son comportement. Si vous lancez une exception vérifiée, l'utilisateur de la méthode doit gérer l'échec:

try {
    fileUpload(file);
} catch (IOException e) {
    // Handle failure.
}
JeroenHoek
la source
3

Si votre méthode a une valeur de retour, lever une exception peut surprendre ses utilisateurs. Si je vois une méthode renvoyer un booléen, je suis assez convaincu qu'elle renverra true si elle réussit et false si ce n'est pas le cas, et je structurerai mon code en utilisant une clause if-else. Si votre exception doit être basée sur une énumération de toute façon, vous pouvez également renvoyer une valeur enum à la place, similaire à Windows HResults, et conserver une valeur énumérée lorsque la méthode réussit.

Il est également ennuyeux d'avoir une exception de levée de méthode lorsque ce n'est pas la dernière étape d'une procédure. Devoir écrire un essai-attraper et détourner le flux de contrôle vers le bloc de capture est une bonne recette pour les spaghettis et doit être évité.

Si vous allez de l'avant avec des exceptions, essayez de renvoyer void à la place, les utilisateurs comprendront que cela réussit si aucune exception n'est levée, et aucun n'essaiera d'utiliser une clause if-else pour détourner le flux de contrôle.

ccControl
la source
1
L'énumération n'était qu'une idée pour aider l'utilisateur de l'API à gérer différents types d'erreurs sans analyser le message d'erreur. Donc, de votre point de vue dans mon cas, il vaut mieux retourner l'énumération et ajouter une énumération pour "OK".
Accollativo