Qu'est-ce qu'un «bon nombre» d'exceptions à implémenter pour ma bibliothèque?

20

Je me suis toujours demandé combien de classes d'exceptions différentes je devais implémenter et lancer pour différentes pièces de mon logiciel. Mon développement particulier est généralement lié à C ++ / C # / Java, mais je pense que c'est une question pour tous les langages.

Je veux comprendre quel est le bon nombre d'exceptions différentes à lever et ce que la communauté des développeurs attend d'une bonne bibliothèque.

Les compromis que je vois comprennent:

  • Plus de classes d'exceptions peuvent permettre des niveaux de grain très fins de la gestion des erreurs pour les utilisateurs de l'API (sujets à la configuration de l'utilisateur ou à des erreurs de données ou à des fichiers introuvables)
  • Plus de classes d'exceptions permettent d'inclure des informations spécifiques aux erreurs dans l'exception, plutôt qu'un simple message de chaîne ou un code d'erreur
  • Plus de classes d'exceptions peuvent signifier plus de maintenance de code
  • Plus de classes d'exceptions peuvent signifier que l'API est moins accessible aux utilisateurs

Les scénarios dans lesquels je souhaite comprendre l'utilisation des exceptions incluent:

  • Pendant l'étape de «configuration», qui peut inclure le chargement de fichiers ou la définition de paramètres
  • Pendant une phase de type «opération» où la bibliothèque peut exécuter des tâches et effectuer certains travaux, peut-être dans un autre thread

D'autres modèles de rapport d'erreurs sans utiliser d'exceptions, ou moins d'exceptions (à titre de comparaison) peuvent inclure:

  • Moins d'exceptions, mais incorporation d'un code d'erreur pouvant être utilisé comme recherche
  • Renvoyer des codes d'erreur et des drapeaux directement à partir des fonctions (parfois impossible à partir des threads)
  • Implémentation d'un système d'événement ou de rappel en cas d'erreur (évite le déroulement de la pile)

En tant que développeurs, que préférez-vous voir?

S'il y a BEAUCOUP d'exceptions, vous dérangez-vous quand même de les traiter séparément?

Avez-vous une préférence pour les types de gestion des erreurs en fonction du stade de fonctionnement?

Duvet
la source
5
"l'API est moins accessible aux utilisateurs" - peut être gérée en ayant plusieurs classes d'exception qui héritent toutes d'une classe de base commune pour l'API. Ensuite, juste au début, vous pouvez voir de quelle API provient l'exception sans avoir à vous soucier de tous les détails. Au moment où vous terminez votre premier programme à l'aide de l'API, alors si vous avez besoin de plus d'informations sur l'erreur, vous commencez à regarder ce que les différentes exceptions signifient réellement. Bien sûr, vous devez simultanément bien jouer avec la hiérarchie d'exceptions standard de la langue que vous utilisez.
Steve Jessop
point pertinent Steve
Fuzz

Réponses:

18

Je reste simple.

Une bibliothèque a un type d'exception de base étendu à partir de std ::: runtime_error (qui provient de C ++ s'applique comme approprié à d'autres langages). Cette exception prend une chaîne de message pour que nous puissions nous connecter; chaque point de lancement a un message unique (généralement avec un ID unique).

C'est à peu près ça.

Remarque 1 : dans les situations où une personne interceptant l'exception peut corriger les exceptions et redémarrer l'action. J'ajouterai des exceptions dérivées pour les choses qui peuvent potentiellement être corrigées de manière unique à un emplacement distant. Mais cela est très très rare (rappelez-vous que le receveur est peu susceptible d'être proche du point de lancer, donc résoudre le problème va être difficile (mais tout dépend de la situation)).

Remarque 2 : Parfois, la bibliothèque est si simple qu'elle ne vaut pas la peine de lui donner sa propre exception et std :: runtime_error fera l'affaire. Il n'est important d'avoir une exception que si la possibilité de la distinguer de std :: runtime_error peut donner à l'utilisateur suffisamment d'informations pour faire quelque chose avec.

Remarque 3 : Dans une classe, je préfère généralement les codes d'erreur (mais ceux-ci ne s'échapperont jamais dans l'API publique de ma classe).

En regardant vos compromis:

Les compromis que je vois comprennent:

Plus de classes d'exceptions peuvent permettre des niveaux de grain très fins de la gestion des erreurs pour les utilisateurs de l'API (sujets à la configuration de l'utilisateur ou à des erreurs de données ou à des fichiers introuvables)

Plus d'exceptions vous donnent-elles vraiment un contrôle plus fin du grain? La question est de savoir si le code de capture peut vraiment corriger l'erreur en fonction de l'exception. Je suis sûr qu'il y a des situations comme ça et dans ces cas, vous devriez avoir une autre exception. Mais toutes les exceptions que vous avez énumérées ci-dessus, la seule correction utile est de générer un gros avertissement et d'arrêter l'application.

Plus de classes d'exceptions permettent d'inclure des informations spécifiques aux erreurs dans l'exception, plutôt qu'un simple message de chaîne ou un code d'erreur

C'est une bonne raison d'utiliser des exceptions. Mais les informations doivent être utiles à la personne qui les met en cache. Peuvent-ils utiliser les informations pour effectuer des actions correctives? Si l'objet est interne à votre bibliothèque et ne peut pas être utilisé pour influencer l'une des API, les informations sont inutiles. Vous devez être très précis sur le fait que les informations transmises ont une valeur utile pour la personne qui peut les capturer. La personne qui l'attrape est généralement en dehors de votre API publique, alors personnalisez vos informations afin qu'elles puissent être utilisées avec des éléments de votre API publique.

Si tout ce qu'ils peuvent faire est de consigner l'exception, il est préférable de simplement lancer un message d'erreur plutôt que beaucoup de données. Comme le receveur crée généralement un message d'erreur avec les données. Si vous générez le message d'erreur, il sera cohérent dans tous les capteurs, si vous autorisez le receveur à générer le message d'erreur, vous pourriez obtenir la même erreur différemment selon qui appelle et intercepte.

Moins d'exceptions, mais incorporation d'un code d'erreur pouvant être utilisé comme recherche

Vous devez déterminer la météo pour que le code d'erreur puisse être utilisé de manière significative. Si c'est le cas, vous devriez avoir sa propre exception. Sinon, vos utilisateurs doivent maintenant implémenter des instructions switch à l'intérieur de catch (ce qui va à l'encontre du fait que catch gère automatiquement les choses).

Si ce n'est pas le cas, pourquoi ne pas utiliser un message d'erreur dans l'exception (pas besoin de diviser le code et le message, cela rend la recherche difficile).

Renvoyer des codes d'erreur et des drapeaux directement à partir des fonctions (parfois impossible à partir des threads)

Le retour de codes d'erreur est excellent en interne. Il vous permet de corriger des bugs de temps en temps et vous devez vous assurer de corriger tous les codes d'erreur et de les prendre en compte. Mais les divulguer à travers votre API publique est une mauvaise idée. Le problème est que les programmeurs oublient souvent de vérifier les états d'erreur (au moins à une exception près, une erreur non vérifiée forcera l'application à quitter une erreur non gérée corrompra généralement toutes vos données).

Implémentation d'un système d'événement ou de rappel en cas d'erreur (évite le déroulement de la pile)

Cette méthode est souvent utilisée en conjonction avec un autre mécanisme de gestion des erreurs (pas comme alternative). Pensez à votre programme Windows. Un utilisateur lance une action en sélectionnant un élément de menu. Cela génère une action sur la file d'attente des événements. La file d'attente d'événements affecte finalement un thread pour gérer l'action. Le thread est censé gérer l'action et éventuellement revenir au pool de threads et attendre une autre tâche. Ici, une exception doit être interceptée à la base par le thread chargé de la tâche. Le résultat de la capture de l'exception entraînera généralement la génération d'un événement pour la boucle principale, ce qui entraînera éventuellement l'affichage d'un message d'erreur pour l'utilisateur.

Mais à moins que vous ne puissiez continuer face à l'exception, la pile va se dérouler (pour le thread au moins).

Martin York
la source
+1; Tho "Sinon, vos utilisateurs doivent maintenant implémenter des instructions switch à l'intérieur de catch (ce qui va à l'encontre de l'intérêt d'avoir catch automatiquement à gérer les choses)." - Le déroulement de la pile d'appels (si vous pouvez toujours l'utiliser) et la gestion forcée des erreurs sont toujours de grands avantages. Mais cela nuira certainement à la possibilité de dérouler la pile d'appels lorsque vous devrez l'attraper au milieu et recommencer.
Merlyn Morgan-Graham
9

Je commence généralement par:

  1. Une classe d'exception pour les bogues d'argument . Par exemple, "l'argument ne doit pas être nul", "l'argument doit être positif", etc. Java et C # ont des classes prédéfinies pour celles-ci; en C ++, je crée généralement une seule classe, dérivée de std :: exception.
  2. Une classe d'exception pour les bogues de précondition . Ce sont des tests plus complexes, comme "l'index doit être plus petit que la taille".
  3. Une classe d'exception pour les bogues d'assertion . Celles-ci servent à vérifier l'état des méthodes à mi-chemin de la constance. Par exemple, lors de l'itération sur une liste pour compter les éléments négatifs, nuls ou positifs, à la fin, ces trois doivent correspondre à la taille.
  4. Une classe d'exception de base pour la bibliothèque elle-même. Au début, lancez simplement cette classe. Seulement lorsque le besoin s'en fait sentir, commencez à ajouter des sous-classes.
  5. Je préfère ne pas envelopper les exceptions, mais je sais que les opinions divergent sur ce point (et sont très corrélées au langage utilisé). Si vous encapsulez, vous aurez besoin de classes d'exception d'encapsulage supplémentaires .

Comme les classes pour les 3 premiers cas sont des aides au débogage, elles ne sont pas destinées à être gérées par le code. Au lieu de cela, ils ne devraient être capturés que par un gestionnaire de niveau supérieur qui affiche les informations de sorte que l'utilisateur puisse les copier-coller au développeur (ou mieux encore: appuyez sur le bouton "envoyer le rapport"). Incluez donc des informations utiles pour le développeur: fichier, fonction, numéro de ligne et un message qui identifie clairement la vérification qui a échoué.

Comme les 3 premiers cas sont les mêmes pour chaque projet, en C ++, je les copie généralement du projet précédent. Parce que beaucoup font exactement la même chose, les concepteurs de C # et Java ont ajouté des classes standard pour ces cas à la bibliothèque standard. [MISE À JOUR:] Pour les programmeurs paresseux: une classe pourrait suffire et avec un peu de chance votre bibliothèque standard a déjà une classe d'exception appropriée. Je préfère ajouter des informations comme le nom de fichier et le numéro de lin, que les classes par défaut en C ++ ne fournissent pas. [Fin de la mise à jour]

Selon la bibliothèque, le quatrième cas peut avoir une seule classe ou devenir une poignée de classes. Je préfère l'approche agile pour commencer simple, en ajoutant des sous-classes lorsque le besoin s'en fait sentir.

Pour une argumentation détaillée sur mon quatrième cas, voir la réponse de Loki Astari . Je suis totalement d'accord avec sa réponse détaillée.

Sjoerd
la source
+1; Il existe une nette différence entre la façon dont vous devez écrire des exceptions d'écriture dans un framework et la façon dont vous devez écrire des exceptions dans une application. La distinction est un peu floue, mais vous la mentionnez (renvoyant à Loki pour le cas de la bibliothèque).
Merlyn Morgan-Graham