Conception par contrat à l'aide d'assertions ou d'exceptions? [fermé]

123

Lors de la programmation par contrat, une fonction ou 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 méthodes les plus importantes pour effectuer ces vérifications sont peu assertà peu exception.

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

Selon vous, lequel est préférable?

Voir la question répétée ici

andreas buykx
la source
3
L'intérêt de la conception par contrat est que vous n'avez pas besoin (et ne devriez sans doute pas) vérifier les conditions préalables au moment de l'exécution. Vous vérifiez l'entrée avant de la passer dans la méthode avec les conditions préalables, c'est ainsi que vous respectez votre fin de contrat. Si l'entrée est invalide ou viole votre fin de contrat, le programme échouera généralement de toute façon dans le cours normal de ses actions (que vous voulez).
void.pointer
Bonne question, mais je pense que vous devriez vraiment changer la réponse acceptée (comme le montrent les votes aussi)!
DaveFar
Pour toujours plus tard, je sais, mais cette question devrait-elle réellement avoir la balise c ++? Je cherchais cette réponse, à utiliser dans un autre langage (Delpih) et je n'imagine pas de langage comportant des exceptions et des assertions qui ne suivraient pas les mêmes règles. (Toujours en train d'apprendre les directives de Stack Overflow.)
Eric G
Réponse très succincte donnée dans cette réponse : "En d'autres termes, les exceptions concernent la robustesse de votre application tandis que les assertions traitent de son exactitude."
Shmuel Levine du

Réponses:

39

Désactiver l'assert 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. Donc, assert ne doit pas être désactivé dans une version de version. Mais vous ne voulez pas non plus que la version de version plante chaque fois que des erreurs se produisent, n'est-ce pas?

Alors utilisez les exceptions et utilisez-les bien. Utilisez une bonne hiérarchie d'exceptions solide et assurez-vous que vous attrapez et que vous pouvez mettre un crochet sur le lancement d'exceptions dans votre débogueur pour l'attraper, et en mode de libération, vous pouvez compenser l'erreur plutôt qu'un simple crash. C'est la manière la plus sûre d'aller.

coppro
la source
4
Les assertions sont à tout le moins utiles dans les cas où la vérification de l'exactitude serait inefficace ou inefficace à mettre en œuvre correctement.
Casebash
89
Le but des assertions n'est pas de corriger les erreurs, mais d'alerter le programmeur. Les garder activés dans les versions de version est inutile pour cette raison: qu'auriez-vous gagné en ayant un déclenchement d'assert? Le développeur ne pourra pas intervenir et le déboguer. Les assertions sont une aide au débogage, elles ne remplacent pas les exceptions (et les exceptions ne remplacent pas non plus les assertions). Les exceptions alertent le programme d'une condition d'erreur. Assert alerte le développeur.
jalf
12
Mais une assertion doit être utilisée lorsque les données internes ont été corrompues après la correction - si une assertion se déclenche, vous ne pouvez faire aucune hypothèse sur l'état du programme car cela signifie que quelque chose ne va pas. Si une assertion s'est déclenchée, vous ne pouvez pas supposer qu'aucune donnée n'est valide. C'est pourquoi une version de version devrait affirmer - non pas pour dire au programmeur où se trouve le problème, mais pour que le programme puisse s'arrêter et ne pas risquer de plus gros problèmes. Le programme doit simplement faire ce qu'il peut pour faciliter la récupération plus tard, lorsque les données peuvent être fiables.
coppro
5
@jalf, Bien que vous ne puissiez pas mettre de hook dans votre débogueur dans les versions de version, vous pouvez tirer parti de la journalisation afin que les développeurs voient les informations relatives à votre assertion échouer. Dans ce document ( martinfowler.com/ieeeSoftware/failFast.pdf ), Jim Shore souligne: "N'oubliez pas, une erreur qui se produit sur le site du client l'a fait à travers votre processus de test. Vous aurez probablement du mal à le reproduire. Ces erreurs sont le plus difficile à trouver, et une affirmation bien placée expliquant le problème pourrait vous épargner des jours d'efforts. "
StriplingWarrior
5
Personnellement, je préfère les affirmations pour les approches de conception par contrat. Les exceptions sont défensives et effectuent la vérification des arguments à l'intérieur de la fonction. De plus, les conditions préalables de dbc ne disent pas "Je ne travaillerai pas si vous utilisez des valeurs hors de la plage de travail" mais "Je ne garantirai pas de fournir la bonne réponse, mais je peux toujours le faire". Les affirmations fournissent au développeur des informations indiquant qu'il appelle une fonction avec une violation de condition, mais ne les empêchent pas de l'utiliser s'ils sentent qu'ils savent mieux. La violation pourrait entraîner des exceptions, mais je vois cela comme une chose différente.
Matt_JD
194

La règle de base 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 les erreurs des autres. En d'autres termes, vous devez utiliser des exceptions pour vérifier les conditions préalables des 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 les données internes à votre système.

Dima
la source
qu'en est-il de sérialiser / désérialiser assis dans différents modules / applications et éventuellement se désynchroniser? Je veux dire du côté du lecteur, c'est toujours mon erreur si j'essaie de lire les choses de la mauvaise façon, donc j'ai tendance à utiliser des assertions, mais d'un autre côté, j'ai des données externes, qui peuvent éventuellement changer de format sans préavis.
Slava
Si les données sont externes, vous devez utiliser des exceptions. Dans ce cas particulier, vous devriez probablement aussi attraper ces exceptions, et les gérer d'une manière raisonnable, plutôt que de simplement laisser votre programme mourir. De plus, ma réponse est une règle empirique et non une loi de la nature. :) Vous devez donc considérer chaque cas individuellement.
Dima
Si votre fonction f (int * x) contient une ligne x-> len, alors f (v) où v est prouvé être nul est garanti de planter. De plus, s'il est prouvé que même plus tôt sur v est nul et que f (v) est prouvé comme étant appelé, vous avez une contradiction logique. C'est la même chose que d'avoir a / b où b est finalement prouvé être 0. Idéalement, un tel code ne devrait pas être compilé. Désactiver les vérifications d'hypothèses est complètement insensé, sauf si le problème est le coût des vérifications, car cela masque l'endroit où une hypothèse a été violée. Il doit au moins être enregistré. Vous devriez quand même avoir une conception de redémarrage en cas de panne.
Rob
22

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

Les affirmations visent à garantir que le contrat est respecté. Le contrat doit être équitable, de sorte que le client doit être en mesure de garantir sa conformité. Par exemple, vous pouvez indiquer dans un contrat qu'une URL doit être valide car les règles concernant ce qui est et ce qui n'est pas une URL valide sont connues et cohérentes.

Les exceptions concernent les situations qui échappent au contrôle du client et du serveur. Une exception signifie que quelque chose a mal tourné et que rien n'aurait pu être fait pour l'éviter. Par exemple, la connectivité réseau est hors du contrôle des applications, il n'y a donc rien qui puisse ê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 qui est 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.

Ged Byrne
la source
6

Les affirmations servent à détecter quelque chose qu'un développeur a mal fait (pas seulement vous-même - un autre développeur de votre équipe également). S'il est raisonnable qu'une erreur de l'utilisateur puisse créer cette condition, il devrait s'agir d'une exception.

Pensez également aux conséquences. Une assertion ferme généralement l'application. S'il y a une attente réaliste à partir de laquelle la condition pourrait être récupérée, vous devriez probablement utiliser une exception.

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

DJClayworth
la source
5

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

Dans Object Oriented Software Construction, 2e édition 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 récupération de la situation: quelque chose d'utile pourrait cependant être fait, 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 ceci est que les conditions préalables sont généralement moins chères à tester que les invariants et les post-conditions, et que dans certains cas, l'exactitude et la «sécurité» dans la version de la version sont plus importantes que la vitesse. c'est-à-dire que 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, c'est-à-dire lorsqu'un contrat est rompu).

Devez-vous toujours laisser les vérifications des conditions préalables 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 peut être préférable d'interrompre l'exécution avec un message alarmant plutôt que de transférer 1 000 000 $ au lieu de 1 000 $. Mais que faire 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 vous devriez effectuer une partie importante de vos tests avec les assertions activées. Ce qui est discuté ici est la meilleure stratégie 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 - du 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?

Daniel Daranas
la source
1
Votre point est définitivement valable. Le SO n'a pas précisé une langue particulière - dans le cas de C # assert norme est System.Diagnostics.Debug.Assert, qui n'échouer que dans une version de débogage, et seront supprimées au moment de la compilation dans une version Release.
yoyo
2

Il y avait un énorme fil de discussion concernant l'activation / la désactivation des assertions dans les versions de version sur comp.lang.c ++. Modéré, qui si vous avez quelques semaines, vous pouvez voir à quel point les opinions sont variées à ce sujet. :)

Contrairement à coppro , je crois que si vous n'êtes pas sûr qu'une assertion puisse être désactivée dans une version de version, alors cela n'aurait pas dû être une assertion. Les affirmations visent à protéger contre la rupture des invariants de programme. 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 OS, entraînant un appel à abandonner. (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 fait beaucoup plus de qui seront les clients de l'API. Si vous écrivez une bibliothèque fournissant une API, vous avez besoin d'une forme de mécanisme pour informer vos clients qu'ils ont utilisé l'API de manière incorrecte. À moins que vous ne fournissiez deux versions de la bibliothèque (une avec assertions, une sans), 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 aux endroits 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 rencontrez une exception 'std :: bad_alloc', il peut être possible de libérer de la mémoire et de réessayer.

Richard Corden
la source
2

J'ai exposé ici mon point de vue sur l'état de la question: Comment valider l'état interne d'un objet? . En règle générale, faites valoir vos revendications et jetez-les pour violation par d'autres. Pour désactiver les assertions dans les versions de version, vous pouvez faire:

  • 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 interceptées devraient être géré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.

Johannes Schaub - litb
la source
1

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 bogues que vous n'auriez pas remarqués lorsqu'ils se sont produits.

les exceptions sont les notifications `` hé utilisateur, quelque chose a mal tourné '' (évidemment, vous pouvez 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 pouvoir éliminer tous vos bogues, n'utilisez que des exceptions. Si vous pensez que vous ne pouvez pas ..... utiliser 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 des données d'erreur dans la pile d'appels aux bits avec lesquels il interagit. Les assertions ne seront alors pas utiles - doublement si votre application est à n-tiers.

Enfin, je n'utiliserais ni l'un ni l'autre - les codes d'erreur sont bien supérieurs pour les erreurs que vous pensez se produire régulièrement. :)

gbjbaanb
la source
0

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

jdmichal
la source
0

Les réponses précédentes sont correctes: utilisez des exceptions pour les fonctions API publiques. Le seul moment où vous voudrez peut-être contourner cette règle est lorsque la vérification est coûteuse en calcul. Dans ce cas, vous pouvez le mettre dans une assert.

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

Mike Elkins
la source
0

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

Je suis devenu friand des fonctions de rapport d'erreurs de glib au lieu d'assertions anciennes. 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 prime, vous voyez ce qui arrive au reste de votre programme lorsqu'une fonction ne renvoie pas "ce qu'elle est censée faire". S'il se bloque, vous savez que votre vérification des erreurs est laxiste ailleurs sur la route.

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 je continuerais à fonctionner. M'a sauvé 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 à l'exécution des arguments, je ferais ceci:

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;
}
indiv
la source
Je ne pense pas avoir vu dans OP question quoi que ce soit lié à C ++. Je pense que cela ne devrait pas être inclus dans votre réponse.
ForceMagic
@ForceMagic: La question avait la balise C ++ en 2008 lorsque j'ai posté cette réponse, et en fait la balise C ++ a été supprimée il y a seulement 5 heures. Quoi qu'il en soit, le code illustre un concept indépendant de la langue.
indiv
0

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

Utilisez des assertions pour les cas où vous souhaitez le désactiver en production, dans l'erreur de les laisser. 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 parfois le code est critique en termes de temps ou le test est coûteux en calcul. Si le code est essentiel à la mission, les exceptions peuvent être les meilleures malgré le ralentissement.

S'il existe une chance réelle 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 des pannes de réseau ou des fichiers verrouillés. Les erreurs ne doivent pas être traitées comme des exceptions simplement parce qu'elles échappent au contrôle du programmeur. Au contraire, la prévisibilité de ces erreurs, par rapport aux erreurs de codage, les rend plus faciles à récupérer.

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

Casebash
la source
0

La règle d'or, pour moi, est d'utiliser des expressions d'assert pour trouver les erreurs internes et les exceptions pour les erreurs externes. Vous pouvez profiter de la discussion suivante de Greg à partir d' ici .

Les expressions d'assert 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 en 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. Maintenant, la routine doit-elle tester si le pointeur est NULL avant le déréférencement, ou doit-elle affirmer que le pointeur n'est pas NULL, puis continuer et le déréférencer malgré tout?

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

Contrairement à ses conditions affirmées, la gestion des erreurs (exceptions) d'un programme ne se réfère pas 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, par exemple un utilisateur qui tente de se connecter à un compte sans saisir 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 à se 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 le programme n'a pas réussi à le reconnaître; alors même si le résultat serait toujours le même, l'échec appartiendrait désormais au programme.

Le but de la gestion 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 qu'elle signifie. Le deuxième objectif est de restaurer l'application après la détection de l'erreur, 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, puisque 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 consulter la question similaire: Exception Vs Assertion .

herohuyongtao
la source
-1

Voir aussi cette question :

Dans certains cas, les assertions sont désactivées lors de la construction pour la publication. Vous n'avez peut-être pas le contrôle sur cela (sinon, vous pouvez 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 à quoi il s'attend, et cela peut entraîner des problèmes ou même des plantages dans des parties totalement différentes du programme, faisant du débogage un cauchemar.

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

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