contrats / assertions de code: qu'en est-il des chèques en double?

10

Je suis un grand fan de rédiger des assertions, des contrats ou tout autre type de chèques disponibles dans la langue que j'utilise. Une chose qui me dérange un peu est que je ne sais pas quelle est la pratique courante pour traiter les chèques en double.

Exemple de situation: j'écris d'abord la fonction suivante

void DoSomething( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  //code using obj
}

puis quelques heures plus tard j'écris une autre fonction qui appelle la première. Comme tout est encore frais en mémoire, je décide de ne pas dupliquer le contrat, car je sais que je DoSomethingvérifierai déjà un objet nul:

void DoSomethingElse( object obj )
{
  //no Requires here: DoSomething will do that already
  DoSomething( obj );
  //code using obj
}

Le problème évident: DoSomethingElsedépend maintenant DoSomethingde la vérification que obj n'est pas nul. Donc DoSomething, je devrais jamais décider de ne plus vérifier, ou si je décide d'utiliser une autre fonction, obj pourrait ne plus être vérifié. Ce qui m'amène à écrire cette implémentation après tout:

void DoSomethingElse( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  DoSomething( obj );
  //code using obj
}

Toujours en sécurité, pas de soucis, sauf que si la situation se développe, le même objet peut être vérifié plusieurs fois et c'est une forme de duplication et nous savons tous que ce n'est pas si bon.

Quelle est la pratique la plus courante pour de telles situations?

stijn
la source
3
ArgumentBullException? C'est un nouveau :)
un CVn
lol @ mes compétences de frappe ... je vais le modifier.
stijn

Réponses:

13

Personnellement, je vérifierais null dans n'importe quelle fonction qui échouera si elle obtient un null, et pas dans une fonction qui ne le fera pas.

Donc, dans votre exemple ci-dessus, si doSomethingElse () n'a pas besoin de déréférencer obj, je ne vérifierais pas obj pour null.

Si DoSomething () fait déréférencer obj, il doit vérifier la valeur null.

Si les deux fonctions le déréférencent, elles doivent toutes les deux vérifier. Donc, si DoSomethingElse déréférence obj, il doit vérifier la valeur null, mais DoSomething doit également vérifier la valeur null car il peut être appelé à partir d'un autre chemin.

De cette façon, vous pouvez laisser le code assez propre et toujours garantir que les contrôles sont au bon endroit.

Luke Graham
la source
1
Je suis complètement d'accord. Les conditions préalables de chaque méthode doivent être autonomes. Imaginez que vous réécrivez de DoSomething()telle sorte que la condition préalable ne soit plus requise (peu probable dans ce cas particulier, mais pourrait se produire dans une situation différente), et supprimez la vérification de la condition préalable. Maintenant, une méthode apparemment totalement indépendante est rompue en raison de la condition préalable manquante. Je vais prendre un peu de duplication de code pour plus de clarté sur des échecs étranges comme celui d'un désir d'enregistrer quelques lignes de code, n'importe quel jour.
un CVn
2

Génial! Je constate que vous avez découvert les contrats de code pour .NET. Les contrats de code vont beaucoup plus loin que vos assertions moyennes, dont le vérificateur statique est le meilleur exemple. Cela peut ne pas être disponible si vous n'avez pas installé Visual Studio Premium ou une version supérieure, mais il est important de comprendre l'intention derrière cela si vous allez utiliser des contrats de code.

Lorsque vous appliquez un contrat à une fonction, il s'agit littéralement d' un contrat . Cette fonction garantit un comportement conforme au contrat et ne peut être utilisée que conformément à la définition du contrat.

Dans votre exemple donné, la DoSomethingElse()fonction n'est pas à la hauteur du contrat spécifié par DoSomething(), car null peut être transmis, et le vérificateur statique indiquera ce problème. La façon de résoudre ce problème consiste à ajouter le même contrat à DoSomethingElse().

Maintenant, cela signifie qu'il y aura duplication, mais cette duplication est nécessaire lorsque vous choisissez d' exposer la fonctionnalité à travers deux fonctions. Ces fonctions, bien que privées, peuvent également être appelées à différents endroits de votre classe, donc la seule façon de garantir que l'argument ne sera jamais nul à partir d'un appel donné est de dupliquer les contrats.

Cela devrait vous faire reconsidérer pourquoi vous avez divisé le comportement en deux fonctions en premier lieu. J'ai toujours pensé ( contrairement à la croyance populaire ) que vous ne devriez pas diviser les fonctions qui ne sont appelées qu'à partir d'un seul endroit . En exposant l'encapsulation en appliquant les contrats, cela devient encore plus évident. Il semble que j'ai trouvé une argumentation supplémentaire pour ma cause! Je vous remercie! :)

Steven Jeuris
la source
concernant votre dernier paragraphe: dans le code actuel, les deux fonctions étaient membres de deux classes différentes, c'est pourquoi elles sont divisées. En dehors de cela, j'étais dans la situation suivante tant de fois: écrire une longue fonction, décider de ne pas la diviser. Plus tard, découvrez qu'une partie de la logique est dupliquée ailleurs, alors divisez-la quand même. Ou un an plus tard, relisez-le et trouvez-le illisible, alors divisez-le quand même. Ou lors du débogage: fonctions partagées = moins de coups sur la touche F10. Il y a plus de raisons, donc personnellement je préfère le fractionnement, même si cela signifie que cela peut parfois être trop extrême.
stijn
(1) "Plus tard, découvrir qu'une partie de la logique est dupliquée ailleurs" . C'est pourquoi je trouve plus important de toujours "évoluer vers une API" que de simplement diviser des fonctions. Pensez constamment à la réutilisation, pas seulement au sein de la classe actuelle. (2) "Ou un an plus tard, relisez-le et trouvez-le illisible" Parce que les fonctions ont des noms, c'est mieux? Vous avez encore plus de lisibilité si vous utilisez ce qu'un commentateur sur mon blog décrit comme des "paragraphes de code". (3) "fonctions partagées = moins de coups sur la touche F10" ... Je ne vois pas pourquoi.
Steven Jeuris
(1) d'accord (2) la lisibilité est une préférence personnelle, donc ce n'est pas vraiment quelque chose à discuter pour moi .. (3) passer par une fonction de 20 lignes nécessite d'appuyer sur F10 20 fois. Passer par une fonction qui a 10 de ces lignes dans une fonction split me donne le choix de ne devoir frapper F10 que 11 fois. Oui, dans le premier cas, je peux mettre des points d'arrêt ou sélectionner «sauter au curseur», mais c'est encore plus un effort que dans le deuxième cas.
stijn
@stijn: (2) d'accord; p (3) merci d'avoir clarifié!
Steven Jeuris