Comment concevoir des exceptions

11

Je me bats avec une question très simple:

Je travaille maintenant sur une application serveur, et j'ai besoin d'inventer une hiérarchie pour les exceptions (certaines exceptions existent déjà, mais un cadre général est nécessaire). Comment puis-je même commencer à faire cela?

Je pense suivre cette stratégie:

1) Qu'est-ce qui ne va pas?

  • Quelque chose est demandé, ce qui n'est pas autorisé.
  • Quelque chose est demandé, il est autorisé, mais cela ne fonctionne pas, en raison de paramètres incorrects.
  • Quelque chose est demandé, c'est permis, mais ça ne marche pas, à cause d'erreurs internes.

2) Qui lance la demande?

  • L'application client
  • Une autre application serveur

3) Remise des messages: comme nous traitons avec une application serveur, il s'agit de recevoir et d'envoyer des messages. Et si l'envoi d'un message se passe mal?

En tant que tel, nous pouvons obtenir les types d'exceptions suivants:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (dans le cas où le serveur ne sait pas d'où vient la demande)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

Il s'agit d'une approche très générale pour définir la hiérarchie des exceptions, mais je crains de manquer de cas évidents. Avez-vous des idées sur les domaines que je ne couvre pas, connaissez-vous des inconvénients de cette méthode ou existe-t-il une approche plus générale de ce type de question (dans ce dernier cas, où puis-je la trouver)?

Merci d'avance

Dominique
la source
5
Vous n'avez pas mentionné ce que vous voulez réaliser avec votre hiérarchie de classes d'exception (et ce n'est pas du tout évident). Journalisation significative? Permettre aux clients de réagir raisonnablement aux différentes exceptions? Ou quoi?
Ralf Kleberhoff du
2
Il est probablement utile de travailler à travers quelques histoires et cas d'utilisation en premier, et de voir ce qui sort. Par exemple: le client demande X , n'est pas autorisé. Le client demande X , mais la demande n'est pas valide. Travaillez avec eux en pensant à qui devrait gérer l'exception, ce qu'ils peuvent en faire (demander, réessayer, peu importe) et les informations dont ils ont besoin pour bien le faire. Ensuite , une fois que vous savez quelles sont vos exceptions concrètes et quelles sont les informations dont les gestionnaires ont besoin pour les traiter, vous pouvez les former en une belle hiérarchie.
Inutile
1
De pertinence, je suppose: softwareengineering.stackexchange.com/questions/278949/…
Martin Ba
1
Je n'ai jamais vraiment compris le désir de vouloir utiliser autant de types d'exceptions différents. En règle générale, pour la plupart des catchblocs que j'utilise, je n'ai pas beaucoup plus recours à l'exception que le message d'erreur qu'il contient. Je n'ai vraiment rien de différent que je puisse faire pour une exception impliquée dans l'échec de la lecture d'un fichier comme un échec d'allocation de mémoire pendant le processus de lecture, j'ai donc tendance à attraper std::exceptionet à signaler le message d'erreur qu'il contient, peut-être à décorer avec "Failed to open file: %s", ex.what()un tampon de pile avant de l'imprimer.
3
De plus, je ne peux pas anticiper tous les types d'exceptions qui seront levés en premier lieu. Je peux peut-être les anticiper maintenant, mais mes collègues peuvent en introduire de nouvelles à l'avenir, par exemple. C'est donc un peu désespéré pour moi de connaître, pour l'instant et pour toujours, tous les différents types d'exceptions qui pourraient être levées au cours d'un processus. opération. Donc, je capture juste super généralement avec un seul bloc de capture. J'ai vu des exemples de personnes utilisant de nombreux catchblocs différents dans un seul site de récupération, mais souvent c'est juste pour ignorer le message à l'intérieur de l'exception et imprimer un message plus localisé ...

Réponses:

5

Remarques générales

(un peu biaisé)

Je n'opterais généralement pas pour une hiérarchie d'exceptions détaillée.

Chose la plus importante: une exception indique à votre appelant que votre méthode n'a pas pu terminer son travail. Et votre interlocuteur doit en être averti, afin qu'il ne continue pas simplement. Cela fonctionne avec n'importe quelle exception, quelle que soit la classe d'exception que vous choisissez.

Le deuxième aspect est la journalisation. Vous souhaitez trouver des entrées de journal significatives en cas de problème. Cela n'a pas non plus besoin de classes d'exception différentes, seulement des messages texte bien conçus (je suppose que vous n'avez pas besoin d'un automate pour lire vos journaux d'erreurs ...).

Le troisième aspect est la réaction de votre interlocuteur. Que peut faire votre correspondant lorsqu'il reçoit une exception? Ici, il peut être judicieux d'avoir différentes classes d'exceptions, de sorte que l'appelant peut décider de réessayer le même appel, d'utiliser une solution différente (par exemple, utiliser une source de secours à la place) ou d'abandonner.

Et vous souhaitez peut-être utiliser vos exceptions comme base pour informer l'utilisateur final du problème. Cela signifie créer un message convivial en plus du texte admin pour le fichier journal, mais n'a pas besoin de classes d'exception différentes (bien que cela puisse rendre la génération de texte plus facile ...).

Un aspect important pour la journalisation (et pour les messages d'erreur utilisateur) est la possibilité de modifier l'exception avec des informations de contexte en la rattrapant sur une couche, en ajoutant des informations de contexte, par exemple des paramètres de méthode, et en la relançant.

Votre hiérarchie

Qui lance la demande? Je ne pense pas que vous aurez besoin des informations qui ont lancé la demande. Je ne peux même pas imaginer comment vous savez au fond d'une pile d'appels.

Gestion des messages : ce n'est pas un aspect différent, mais juste des cas supplémentaires pour "Qu'est-ce qui ne va pas?".

Dans un commentaire, vous parlez d'un indicateur " pas de journalisation " lors de la création d'une exception. Je ne pense pas qu'à l'endroit où vous créez et lâchez une exception, vous pouvez prendre une décision fiable de consigner ou non cette exception.

La seule situation que je peux imaginer est qu'une couche supérieure utilise votre API d'une manière qui produira parfois des exceptions, et cette couche sait alors qu'elle ne doit déranger aucun administrateur avec l'exception, donc elle avale silencieusement l'exception. Mais c'est une odeur de code: une exception attendue est une contradiction en soi, un indice pour changer l'API. Et c'est la couche supérieure qui devrait décider, pas le code générateur d'exceptions.

Ralf Kleberhoff
la source
D'après mon expérience, un code d'erreur en combinaison avec un texte convivial fonctionne très bien. Les administrateurs peuvent utiliser le code d'erreur pour trouver des informations supplémentaires.
Sjoerd
1
J'aime l'idée derrière cette réponse en général. D'après mon expérience, les exceptions ne devraient vraiment jamais se transformer en une bête trop complexe. Le but principal d'une exception est de permettre au code appelant de résoudre un problème spécifique et d'obtenir les informations de débogage / nouvelle tentative pertinentes sans jouer avec la réponse de la fonction.
greggle138
2

La principale chose à garder à l'esprit lors de la conception d'un modèle de réponse aux erreurs est de s'assurer qu'il est utile aux appelants. Cela s'applique que vous utilisiez des exceptions ou des codes d'erreur définis, mais nous nous limiterons à illustrer avec des exceptions.

  • Si votre langage ou votre infrastructure fournit déjà des classes d'exceptions générales, utilisez-les là où elles sont appropriées et où elles sont raisonnablement prévisibles . Ne pas définir vos propres ArgumentNullExceptionou des ArgumentOutOfRangeclasses exception. Les appelants ne s'attendront pas à les attraper.

  • Définissez une MyClientServerAppExceptionclasse de base pour englober les erreurs uniques dans le contexte de votre application. Ne lancez jamais une instance de la classe de base. Une réponse d'erreur ambiguë est LA PIRE CHOSE JAMAIS . S'il y a une "erreur interne", expliquez ce qu'est cette erreur.

  • Pour la plupart, la hiérarchie sous la classe de base doit être large, pas profonde. Il vous suffit de l'approfondir dans les situations où il est utile à l'appelant. Par exemple, s'il y a 5 raisons pour lesquelles un message peut échouer du client au serveur, vous pouvez définir une ServerMessageFaultexception, puis définir une classe d'exception pour chacune de ces 5 erreurs en dessous. De cette façon, l'appelant peut simplement attraper la superclasse s'il en a besoin ou s'il le souhaite. Essayez de limiter cela à des cas spécifiques et raisonnables.

  • N'essayez pas de définir toutes vos classes d'exceptions avant qu'elles ne soient réellement utilisées. Vous finirez par refaire l'essentiel. Lorsque vous rencontrez un cas d'erreur lors de l'écriture de code, décidez de la meilleure façon de décrire cette erreur. Idéalement, il devrait être exprimé dans le contexte de ce que l'appelant essaie de faire.

  • En ce qui concerne le point précédent, n'oubliez pas que ce n'est pas parce que vous utilisez des exceptions pour répondre aux erreurs que vous devez utiliser uniquement des exceptions pour les états d'erreur. Gardez à l'esprit que lever des exceptions est généralement coûteux et que le coût des performances peut varier d'une langue à l'autre. Avec certaines langues, le coût est plus élevé en fonction de la profondeur de la pile d'appels, donc si une erreur est profonde dans une pile d'appels, vérifiez si vous ne pouvez pas utiliser de types primitifs simples (codes d'erreur entiers ou drapeaux booléens) pour pousser l'erreur sauvegarde la pile, afin qu'elle puisse être levée plus près de l'appel de l'appelant.

  • Si vous incluez la journalisation dans le cadre de la réponse d'erreur, il devrait être facile pour les appelants d'ajouter des informations de contexte à un objet d'exception. Commencez à partir de l'endroit où les informations sont utilisées dans le code de journalisation. Décidez combien d'informations sont nécessaires pour que le journal soit utile (sans être un mur de texte géant). Ensuite, travaillez en arrière pour vous assurer que les classes d'exception peuvent être facilement fournies avec ces informations.

Enfin, à moins que votre application ne puisse absolument gérer avec élégance une erreur de mémoire insuffisante, n'essayez pas de traiter ces erreurs ou d'autres exceptions d'exécution catastrophiques. Laissez simplement le système d'exploitation s'en occuper, car en réalité, c'est tout ce que vous pouvez faire.

Mark Benningfield
la source
2
À l'exception de votre avant-dernière puce (sur le coût des exceptions), la réponse est bonne. Cette balle sur le coût des exceptions est trompeuse, car dans toutes les manières courantes d'implémenter des exceptions au niveau bas, le coût de lever une exception est complètement indépendant de la profondeur de la pile d'appels. D'autres méthodes de rapport d'erreurs peuvent être meilleures si vous savez que l'erreur sera gérée par l'appelant immédiat, mais pas pour retirer quelques fonctions de la pile d'appels avant de lever une exception.
Bart van Ingen Schenau
@BartvanIngenSchenau: J'ai essayé de ne pas lier cela à une langue spécifique. Dans certains langages ( Java, par exemple ), la profondeur de la pile d'appels affecte le coût de l'instanciation. Je vais modifier pour refléter qu'il n'est pas si coupé et séché.
Mark Benningfield
0

Eh bien, je vous recommande avant tout de créer une Exceptionclasse de base pour toutes les exceptions vérifiées qui peuvent être levées par votre application. Si votre application a été appelée DuckType, créez une DuckTypeExceptionclasse de base.

Cela vous permet d'intercepter toute exception de votre DuckTypeExceptionclasse de base pour la gestion. À partir de là, vos exceptions devraient se ramifier avec des noms descriptifs qui peuvent mieux mettre en évidence le type de problème. Par exemple, "DatabaseConnectionException".

Permettez-moi d'être clair, ces exceptions doivent toutes être vérifiées pour les situations qui pourraient arriver que vous voudriez probablement gérer avec élégance dans votre programme. En d'autres termes, vous ne pouvez pas vous connecter à la base de données, donc un DatabaseConnectionExceptionmessage est jeté que vous pouvez attraper pour attendre et réessayer après un certain temps.

Vous ne verriez pas d' exception vérifiée pour un problème très inattendu tel qu'une requête SQL non valide ou une exception de pointeur nul, et je vous encourage à laisser ces exceptions transcender la plupart des clauses catch (ou interceptées et renvoyées si nécessaire) jusqu'à ce que vous arriviez au principal contrôleur lui-même, qui peut alors intercepter RuntimeExceptionuniquement à des fins de journalisation.

Ma préférence personnelle est de ne pas renvoyer une RuntimeExceptionexception non vérifiée , car, par la nature d'une exception non vérifiée, vous ne vous y attendriez pas et, par conséquent, la renvoyer sous une autre exception cache des informations. Cependant, si c'est votre préférence, vous pouvez toujours attraper un RuntimeExceptionet lancer un DuckTypeInternalExceptionqui contrairement à DuckTypeExceptiondérive RuntimeExceptionet n'est donc pas coché.

Si vous préférez, vous pouvez sous-catégoriser vos exceptions à des fins d'organisation, par exemple DatabaseExceptionpour tout ce qui concerne la base de données, mais j'encouragerais toujours ces sous-exceptions à dériver de votre exception de base DuckTypeExceptionet à être abstraites et donc dérivées avec des noms descriptifs explicites.

En règle générale, vos captures d'essai devraient être de plus en plus génériques lorsque vous montez dans la pile d'appels pour gérer les exceptions, et encore une fois, dans votre contrôleur principal, vous ne géreriez pas un DatabaseConnectionExceptionmais plutôt le simple DuckTypeExceptiondont dérivent toutes vos exceptions vérifiées.

Neil
la source
2
Notez que la question est étiquetée "C ++".
Martin Ba
0

Essayez de simplifier cela.

La première chose qui vous aidera à penser dans une autre stratégie est la suivante: intercepter de nombreuses exceptions est très similaire à l'utilisation d' exceptions vérifiées de Java (désolé, je ne suis pas développeur C ++). Ce n'est pas bon pour de nombreuses raisons, donc j'essaie toujours de ne pas les utiliser, et votre stratégie d'exception de la hiérarchie m'en rappelle beaucoup.

Donc, je vous recommande une autre stratégie flexible: utilisez des exceptions non vérifiées et des erreurs de code.

Par exemple, consultez ce code Java:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

Et votre exception unique :

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Cette stratégie que j'ai trouvée sur ce lien et vous pouvez trouver l'implémentation Java ici , où vous pouvez voir plus de détails à ce sujet, car le code ci-dessus est simplifié.

Comme vous devez séparer les différentes exceptions entre les applications "client" et "autre serveur", vous pouvez avoir plusieurs classes de code d'erreur implémentant l'interface ErrorCode.

Dherik
la source
2
Notez que la question est étiquetée "C ++".
Sjoerd
J'ai édité pour adapter l'idée.
Dherik
0

Les exceptions sont les gotos sans restriction et doivent être utilisées avec prudence. La meilleure stratégie pour eux est de les restreindre. La fonction appelante doit gérer toutes les exceptions levées par les fonctions qu'elle appelle ou le programme meurt. Seule la fonction appelante a le contexte correct pour gérer l'exception. Avoir des fonctions plus haut dans l'arborescence d'appels les gère sont des gotos sans restriction.

Les exceptions ne sont pas des erreurs. Ils se produisent lorsque les circonstances empêchent le programme de terminer une branche du code et indiquent une autre branche à suivre.

Les exceptions doivent être dans le contexte de la fonction appelée. Par exemple: une fonction qui résout des équations quadratiques . Il doit gérer deux exceptions: division_by_zero et square_root_of_negative_number. Mais ces exceptions n'ont pas de sens pour les fonctions appelant ce solveur d'équations quadratiques. Ils se produisent en raison de la méthode utilisée pour résoudre les équations et les relancer simplement expose les composants internes et rompt l'encapsulation. Au lieu de cela, division_by_zero doit être renommé not_quadratic et square_root_of_negative_number et no_real_roots.

Il n'est pas nécessaire d'avoir une hiérarchie d'exceptions. Une énumération des exceptions (qui les identifie) levées par une fonction est suffisante car la fonction appelante doit les gérer. Leur permettre de traiter l'arborescence des appels est un goto hors contexte (sans restriction).

shawnhcorey
la source