web-dev-qa-db-fra.com

Conception par contrat en utilisant des assertions ou des exceptions?

Lorsque la programmation par contrat d'une fonction ou d'une méthode vérifie d'abord si ses conditions préalables sont remplies, avant de commencer à travailler sur ses responsabilités, non? Les deux façons les plus importantes d'effectuer ces vérifications sont par assert et par exception.

  1. assert échoue uniquement en mode débogage. Pour vous assurer qu'il est crucial de tester (à l'unité) toutes les conditions préalables distinctes du contrat pour voir si elles échouent réellement.
  2. l'exception échoue en mode débogage et libération. Cela présente l'avantage que le comportement de débogage testé est identique au comportement de publication, mais cela entraîne une pénalité de performances d'exécution.

Selon vous, lequel est préférable?

Voir la question released ici

119
andreas buykx

La désactivation de l'assertion dans les versions de version revient à dire "Je n'aurai jamais aucun problème dans une version de version", ce qui n'est souvent pas le cas. Assert ne doit donc pas être désactivé dans une version. Mais vous ne voulez pas non plus que la version se bloque chaque fois que des erreurs se produisent, n'est-ce pas?

Utilisez donc les exceptions et utilisez-les bien. Utilisez une bonne et solide hiérarchie d'exceptions et assurez-vous que vous interceptez et vous pouvez mettre un crochet sur l'exception en lançant votre débogueur pour l'attraper, et en mode de libération, vous pouvez compenser l'erreur plutôt qu'un plantage direct. C'est le chemin le plus sûr.

39
coppro

La règle générale est que vous devez utiliser des assertions lorsque vous essayez de détecter vos propres erreurs et des exceptions lorsque vous essayez de détecter des erreurs d'autres personnes. En d'autres termes, vous devez utiliser des exceptions pour vérifier les conditions préalables pour les fonctions API publiques et chaque fois que vous obtenez des données externes à votre système. Vous devez utiliser des assertions pour les fonctions ou données internes à votre système.

194
Dima

Le principe que je suis est le suivant: si une situation peut être évitée de manière réaliste en codant, utilisez une assertion. Sinon, utilisez une exception.

Les assertions visent à garantir le respect du contrat. Le contrat doit être équitable, de sorte que le client doit être en mesure de s’y conformer. Par exemple, vous pouvez indiquer dans un contrat qu'une URL doit être valide car les règles concernant ce qui est ou non une URL valide sont connues et cohérentes.

Les exceptions concernent les situations hors du contrôle du client et du serveur. Une exception signifie que quelque chose s'est mal passé, et il n'y a rien qui aurait pu être fait pour l'éviter. Par exemple, la connectivité réseau est hors du contrôle des applications, donc rien ne peut être fait pour éviter une erreur réseau.

Je voudrais ajouter que la distinction Assertion/Exception n'est pas vraiment la meilleure façon d'y penser. Ce à quoi vous voulez vraiment penser, c'est le contrat et comment il peut être appliqué. Dans mon exemple d'URL ci-dessus, la meilleure chose à faire est d'avoir une classe qui encapsule une URL et soit Null soit une URL valide. C'est la conversion d'une chaîne en URL qui applique le contrat et une exception est levée si elle n'est pas valide. Une méthode avec un paramètre URL est beaucoup plus claire qu'une méthode avec un paramètre String et une assertion qui spécifie une URL.

22
Ged Byrne

Les assertions permettent d'attraper quelque chose qu'un développeur a fait de mal (pas seulement vous-même - un autre développeur de votre équipe également). S'il est raisonnable qu'une erreur utilisateur puisse créer cette condition, cela devrait être une exception.

Pensez également aux conséquences. Une assertion ferme généralement l'application. S'il existe une attente réaliste de récupération de la condition, vous devez probablement utiliser une exception.

D'un autre côté, si le problème peut seulement être dû à une erreur de programmeur, alors utilisez une assertion, car vous voulez le savoir le plus tôt possible. Une exception peut être interceptée et traitée, et vous ne la découvrirez jamais. Et oui, vous devez désactiver les assertions dans le code de version, car vous souhaitez que l'application récupère s'il y a la moindre chance. Même si l'état de votre programme est profondément perturbé, l'utilisateur pourrait peut-être enregistrer son travail.

6
DJClayworth

Il n'est pas tout à fait vrai que "l'assertion échoue uniquement en mode débogage".

Dans Object Oriented Software Construction, 2nd Edition de Bertrand Meyer, l'auteur laisse une porte ouverte pour vérifier les conditions préalables en mode release. Dans ce cas, ce qui se passe lorsqu'une assertion échoue, c'est que ... une exception de violation d'assertion est déclenchée! Dans ce cas, il n'y a pas de reprise de la situation: quelque chose d'utile pourrait être fait cependant, et c'est de générer automatiquement un rapport d'erreur et, dans certains cas, de redémarrer l'application.

La motivation derrière cela est que les conditions préalables sont généralement moins coûteuses à tester que les invariants et les postconditions, et que dans certains cas, l'exactitude et la "sécurité" dans la version sont plus importantes que la vitesse. ie pour de nombreuses applications la vitesse n'est pas un problème, mais la robustesse (la capacité du programme à se comporter de manière sûre lorsque son comportement n'est pas correct, ie lorsqu'un contrat est rompu) est.

Devriez-vous toujours laisser les vérifications de précondition activées? Ça dépend. C'est à vous. Il n'y a pas de réponse universelle. Si vous créez un logiciel pour une banque, il pourrait être préférable d'interrompre l'exécution avec un message alarmant que de transférer 1 000 000 $ au lieu de 1 000 $. Mais que se passe-t-il si vous programmez un jeu? Peut-être avez-vous besoin de toute la vitesse que vous pouvez obtenir, et si quelqu'un obtient 1000 points au lieu de 10 à cause d'un bug que les conditions préalables n'ont pas détecté (car elles ne sont pas activées), pas de chance.

Dans les deux cas, vous devriez idéalement avoir détecté ce bogue pendant les tests et effectuer une partie importante de vos tests avec les assertions activées. Ce qui est discuté ici est quelle est la meilleure politique pour les rares cas où les conditions préalables échouent dans le code de production dans un scénario qui n'a pas été détecté plus tôt en raison de tests incomplets.

Pour résumer, vous pouvez avoir des assertions et toujours obtenir les exceptions automatiquement , si vous les laissez activées - au moins dans Eiffel. Je pense que pour faire la même chose en C++, vous devez le taper vous-même.

Voir aussi: Quand les assertions doivent-elles rester dans le code de production?

5
Daniel Daranas

J'ai exposé ici mon point de vue sur l'état de la question: Comment validez-vous l'état interne d'un objet? . Généralement, faites valoir vos prétentions et jetez-les pour violation par d'autres. Pour désactiver les assertions dans les versions, vous pouvez:

  • Désactiver les assertions pour les vérifications coûteuses (comme vérifier si une plage est commandée)
  • Gardez les vérifications triviales activées (comme la vérification d'un pointeur nul ou d'une valeur booléenne)

Bien sûr, dans les versions de version, les assertions ayant échoué et les exceptions non capturées doivent être traitées d'une autre manière que dans les versions de débogage (où il pourrait simplement appeler std :: abort). Écrivez un journal de l'erreur quelque part (éventuellement dans un fichier), informez le client qu'une erreur interne s'est produite. Le client pourra vous envoyer le fichier journal.

2

Il y avait un énorme thread concernant l'activation/désactivation des assertions dans les versions compilées sur comp.lang.c ++. Modéré, qui si vous avez quelques semaines, vous pouvez voir la diversité des opinions à ce sujet. :)

Contrairement à coppro , je pense que si vous n'êtes pas sûr qu'une assertion peut être désactivée dans une version, alors cela n'aurait pas dû être une assertion. Les assertions visent à protéger contre les invariants de programme qui sont brisés. Dans un tel cas, en ce qui concerne le client de votre code, il y aura l'un des deux résultats possibles:

  1. Mourir avec une sorte d'échec de type de système d'exploitation, entraînant un appel à l'abandon. (Sans affirmer)
  2. Mourir via un appel direct pour abandonner. (Avec assert)

Il n'y a aucune différence pour l'utilisateur, cependant, il est possible que les assertions ajoutent un coût de performance inutile dans le code qui est présent dans la grande majorité des exécutions où le code n'échoue pas.

La réponse à la question dépend en réalité beaucoup plus de qui seront les clients de l'API. Si vous écrivez une bibliothèque fournissant une API, vous avez besoin d'une certaine forme de mécanisme pour informer vos clients qu'ils ont mal utilisé l'API. À moins que vous ne fournissiez deux versions de la bibliothèque (une avec assert, une sans), alors assert est très peu probable le choix approprié.

Personnellement, cependant, je ne suis pas sûr que j'irais avec des exceptions pour ce cas non plus. Les exceptions sont mieux adaptées à l'endroit où une forme appropriée de récupération peut avoir lieu. Par exemple, il se peut que vous essayiez d'allouer de la mémoire. Lorsque vous interceptez une exception "std :: bad_alloc", il peut être possible de libérer de la mémoire et de réessayer.

2
Richard Corden

vous demandez la différence entre les erreurs de conception et d'exécution.

les assertions sont des notifications `` Hé programmeur, c'est cassé '', elles sont là pour vous rappeler des bugs que vous n'auriez pas remarqués lorsqu'ils se sont produits.

les exceptions sont les notifications `` bon utilisateur, quelque chose a mal tourné '' (vous pouvez évidemment coder pour les attraper afin que l'utilisateur ne soit jamais informé), mais elles sont conçues pour se produire au moment de l'exécution lorsque l'utilisateur Joe utilise l'application.

Donc, si vous pensez que vous pouvez supprimer tous vos bogues, utilisez uniquement des exceptions. Si vous pensez que vous ne pouvez pas ..... utilisez des exceptions. Vous pouvez toujours utiliser des assertions de débogage pour réduire le nombre d'exceptions bien sûr.

N'oubliez pas que la plupart des conditions préalables seront des données fournies par l'utilisateur, vous aurez donc besoin d'un bon moyen d'informer l'utilisateur que ses données n'étaient pas bonnes. Pour ce faire, vous devrez souvent renvoyer les données d'erreur dans la pile d'appels aux bits avec lesquels il interagit. Les assertions ne seront alors pas utiles - d'autant plus si votre application est à n niveaux.

Enfin, je n'utiliserais ni l'un ni l'autre - les codes d'erreur sont bien supérieurs aux erreurs qui, selon vous, se produiront régulièrement. :)

1
gbjbaanb

J'ai essayé de synthétiser plusieurs des autres réponses ici avec mes propres vues.

Utilisez des assertions dans les cas où vous souhaitez le désactiver en production, en préférant les laisser en place. La seule vraie raison de désactiver en production, mais pas en développement, est d'accélérer le programme. Dans la plupart des cas, cette accélération ne sera pas significative, mais le code est parfois critique en temps ou le test est coûteux en calcul. Si le code est essentiel à la mission, les exceptions peuvent être meilleures malgré le ralentissement.

S'il existe une réelle chance de récupération, utilisez une exception car les assertions ne sont pas conçues pour être récupérées. Par exemple, le code est rarement conçu pour récupérer des erreurs de programmation, mais il est conçu pour récupérer des facteurs tels que les pannes de réseau ou les fichiers verrouillés. Les erreurs ne doivent pas être traitées comme des exceptions simplement parce qu'elles sont hors du contrôle du programmeur. Au contraire, la prévisibilité de ces erreurs, par rapport aux erreurs de codage, les rend plus facilement récupérables.

Re argument selon lequel il est plus facile de déboguer des assertions: la trace de pile d'une exception correctement nommée est aussi facile à lire qu'une assertion. Un bon code ne doit intercepter que des types spécifiques d'exceptions, donc les exceptions ne doivent pas passer inaperçues en raison de leur interception. Cependant, je pense que Java vous oblige parfois à intercepter toutes les exceptions.

0
Casebash

Je préfère le second. Bien que vos tests se soient bien déroulés, Murphy indique que quelque chose d'inattendu se passera mal. Ainsi, au lieu d'obtenir une exception lors de l'appel de méthode erronée, vous finissez par retracer une NullPointerException (ou équivalent) 10 images de pile plus profondément.

0
jdmichal

Les réponses précédentes sont correctes: utilisez des exceptions pour les fonctions API publiques. La seule fois où vous voudrez peut-être plier cette règle est lorsque le chèque est coûteux en calcul. Dans ce cas, vous pouvez le mettre dans une assertion.

Si vous pensez qu'une violation de cette condition préalable est probable, conservez-la comme une exception ou remaniez la condition préalable.

0
Mike Elkins

Voir aussi cette question :

Dans certains cas, les assertions sont désactivées lors de la génération pour la publication. Vous pouvez ne pas avoir de contrôle sur cela (sinon, vous pourriez construire avec des assertions), donc ce pourrait être une bonne idée de le faire comme ça.

Le problème avec la "correction" des valeurs d'entrée est que l'appelant n'obtiendra pas ce qu'il attend, et cela peut entraîner des problèmes ou même des plantages dans des parties complètement différentes du programme, faisant du débogage un cauchemar.

Je jette généralement une exception dans l'instruction if pour reprendre le rôle de l'assertion au cas où ils seraient désactivés

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
0
Rik

La règle de base, pour moi, est d'utiliser des expressions assert pour trouver des erreurs internes et des exceptions pour les erreurs externes. Vous pouvez bénéficier de la discussion suivante de Greg de ici .

Les expressions d'assertion sont utilisées pour trouver des erreurs de programmation: soit des erreurs dans la logique du programme lui-même, soit des erreurs dans son implémentation correspondante. Une condition d'assertion vérifie que le programme reste dans un état défini. Un "état défini" est fondamentalement celui qui est d'accord avec les hypothèses du programme. Notez qu'un "état défini" pour un programme n'a pas besoin d'être un "état idéal" ou même "un état habituel", ou même un "état utile" mais plus sur ce point important plus tard.

Pour comprendre comment les assertions s'intègrent dans un programme, considérez une routine dans un programme C++ qui est sur le point de déréférencer un pointeur. La routine devrait-elle maintenant tester si le pointeur est NULL avant le déréférencement, ou doit-elle affirmer que le pointeur n'est pas NULL et ensuite continuer et le déréférencer indépendamment?

J'imagine que la plupart des développeurs voudraient faire les deux, ajouter l'assertion, mais aussi vérifier le pointeur pour une valeur NULL, afin de ne pas planter si la condition affirmée échoue. En surface, effectuer le test et le contrôle peut sembler la décision la plus sage

Contrairement à ses conditions affirmées, la gestion des erreurs d'un programme (exceptions) ne fait pas référence aux erreurs du programme, mais aux entrées que le programme obtient de son environnement. Ce sont souvent des "erreurs" de la part de quelqu'un, comme un utilisateur qui tente de se connecter à un compte sans taper de mot de passe. Et même si l'erreur peut empêcher la réussite de la tâche du programme, il n'y a pas d'échec du programme. Le programme ne parvient pas à connecter l'utilisateur sans mot de passe en raison d'une erreur externe - une erreur de la part de l'utilisateur. Si les circonstances étaient différentes et que l'utilisateur a tapé le mot de passe correct et que le programme ne l'a pas reconnu; alors que le résultat serait toujours le même, l'échec appartiendrait désormais au programme.

Le but du traitement des erreurs (exceptions) est double. La première consiste à communiquer à l'utilisateur (ou à un autre client) qu'une erreur dans l'entrée du programme a été détectée et ce que cela signifie. Le deuxième objectif est de restaurer l'application une fois l'erreur détectée, dans un état bien défini. Notez que le programme lui-même n'est pas en erreur dans cette situation. Certes, le programme peut être dans un état non idéal, ou même dans un état dans lequel il ne peut rien faire d'utile, mais il n'y a pas d'erreur de programmation. Au contraire, étant donné que l'état de récupération d'erreur est un état anticipé par la conception du programme, il en est un que le programme peut gérer.

PS: vous voudrez peut-être vérifier la question similaire: Assertion Exception Vs .

0
herohuyongtao

Vous devez utiliser les deux. Les assertions sont pour votre commodité en tant que développeur. Les exceptions captent des choses que vous avez manquées ou auxquelles vous ne vous attendiez pas pendant l'exécution.

Je suis devenu friand de fonctions de rapport d'erreurs de glib au lieu de vieilles affirmations simples. Ils se comportent comme des instructions assert mais au lieu d'arrêter le programme, ils renvoient simplement une valeur et laissent le programme continuer. Cela fonctionne étonnamment bien, et en bonus, vous pouvez voir ce qui arrive au reste de votre programme lorsqu'une fonction ne retourne pas "ce qu'elle est censée". S'il se bloque, vous savez que votre vérification d'erreur est laxiste ailleurs.

Dans mon dernier projet, j'ai utilisé ce style de fonctions pour implémenter la vérification des conditions préalables, et si l'une d'entre elles échouait, j'imprimais une trace de pile dans le fichier journal mais continuais de fonctionner. J'ai économisé des tonnes de temps de débogage lorsque d'autres personnes rencontraient un problème lors de l'exécution de ma version de débogage.

#ifdef DEBUG
#define RETURN_IF_FAIL(expr)      do {                      \
 if (!(expr))                                           \
 {                                                      \
     fprintf(stderr,                                        \
        "file %s: line %d (%s): precondition `%s' failed.", \
        __FILE__,                                           \
        __LINE__,                                           \
        __PRETTY_FUNCTION__,                                \
        #expr);                                             \
     ::print_stack_trace(2);                                \
     return;                                                \
 };               } while(0)
#define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
 if (!(expr))                                                   \
 {                                                              \
    fprintf(stderr,                                             \
        "file %s: line %d (%s): precondition `%s' failed.",     \
        __FILE__,                                               \
        __LINE__,                                               \
        __PRETTY_FUNCTION__,                                    \
        #expr);                                                 \
     ::print_stack_trace(2);                                    \
     return val;                                                \
 };               } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif

Si j'avais besoin d'une vérification d'exécution des arguments, je le ferais:

char *doSomething(char *ptr)
{
    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                            // Goes away when debug off.

    if( ptr != NULL )
    {
       ...
    }

    return ptr;
}
0
indiv