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 incorrect, 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)?
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, le choix entre throw
et return
requiert 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 il 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
).
Il n'y a absolument aucune raison de renvoyer true
en cas de succès si vous ne retournez pas false
en cas d'é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) { }
Ainsi, la valeur de retour true
est complètement hors de propos pour l'appelant. Gardez donc simplement la méthode void
.
En général, il existe trois façons de concevoir des modes de défaillance.
void
, exception throw (cochée)Retourne 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 fait cela 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.
Jetez (vérifié) les exceptions
C'est une belle façon de le faire. À certains moments, vous pouvez vous attendre à un échec. Java fait cela avec les sockets. L'hypothèse de base est qu'un appel devrait réussir, mais tout le monde sait que certaines opérations peuvent échouer. Les connexions de sockets 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 donne à l'appelant un moyen élégant de gérer l'échec.
Retourner 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 devraient être exceptionnelles et que vous ne devriez pas concevoir avec la possibilité d'exceptions à l'esprit, et utiliserez presque toujours la troisième option. C'est bon. 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 devrait réussir (comme utiliser 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.
Cela revient vraiment à savoir si un échec est exceptionnel ou attend.
Si une erreur n'est pas quelque chose que vous attendez généralement, il est raisonnablement probable que l'utilisateur de l'API appelle simplement votre méthode sans aucune gestion d'erreur particulière. Lancer une exception permet de faire bouillonner la pile à un endroit où elle est remarquée.
Si, d'autre part, les erreurs sont courantes, vous devez optimiser pour le développeur qui les recherche et try-catch
les clauses sont un peu plus lourdes qu'un if/Elif
ou switch
série.
Concevez toujours une API dans l'état d'esprit de quelqu'un qui l'utilise.
Ne renvoyez pas de booléen si vous n'avez jamais l'intention de renvoyer false
. Faites juste la méthode void
et documentez qu'elle lèvera une exception (IOException
convient) 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.
}
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 retournera 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 énumérée à 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 lancement de méthode lorsque ce n'est pas la dernière étape d'une procédure. Devoir écrire un essai-capture et détourner le flux de contrôle vers le bloc de capture est une bonne recette pour les spaghettis, et devrait ê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.