La condition redondante vérifie-t-elle les meilleures pratiques?

16

Je développe des logiciels depuis trois ans, mais je me suis récemment réveillé à quel point je suis ignorant des bonnes pratiques. Cela m'a amené à commencer à lire le livre Clean Code , qui bouleverse ma vie pour le mieux, mais j'ai du mal à obtenir un aperçu de certaines des meilleures approches pour écrire mes programmes.

J'ai un programme Python dans lequel je ...

  1. utilisez argparse required=Truepour appliquer deux arguments, qui sont tous deux des noms de fichiers. le premier est le nom du fichier d'entrée, le second est le nom du fichier de sortie
  2. avoir une fonction readFromInputFilequi vérifie d'abord qu'un nom de fichier d'entrée a été entré
  3. avoir une fonction writeToOutputFilequi vérifie d'abord qu'un nom de fichier de sortie a été entré

Mon programme est suffisamment petit pour que je puisse croire que les vérifications # 2 et # 3 sont redondantes et doivent être supprimées, libérant ainsi les deux fonctions d'une ifcondition inutile . Cependant, j'ai également été amené à croire que "la double vérification est ok" et peut être la bonne solution dans un programme où les fonctions pourraient être appelées à partir d'un emplacement différent où l'analyse des arguments ne se produit pas.

(De plus, si la lecture ou l'écriture échoue, j'ai un try exceptdans chaque fonction pour déclencher un message d'erreur approprié.)

Ma question est: est-il préférable d'éviter toute vérification de condition redondante? La logique d'un programme doit-elle être si solide que les vérifications ne doivent être effectuées qu'une seule fois? Y a-t-il de bons exemples qui illustrent cela ou l'inverse?

EDIT: Merci à tous pour les réponses! J'ai appris quelque chose de chacun. Voir autant de perspectives me permet de mieux comprendre comment aborder ce problème et déterminer une solution en fonction de mes besoins. Je vous remercie!

thèse
la source
Voici une version très généralisée de votre question: softwareengineering.stackexchange.com/questions/19549/… . Je ne dirais pas qu'il s'agit d'un doublon car il a une focalisation plus large, mais peut-être que cela aide.
Doc Brown

Réponses:

15

Ce que vous demandez est appelé "robustesse", et il n'y a pas de bonne ou de mauvaise réponse. Cela dépend de la taille et de la complexité du programme, du nombre de personnes qui y travaillent et de l'importance de détecter les défaillances.

Dans les petits programmes que vous écrivez seul et uniquement pour vous-même, la robustesse est généralement une préoccupation bien moindre que lorsque vous allez écrire un programme complexe composé de plusieurs composants, peut-être écrit par une équipe. Dans de tels systèmes, il existe des frontières entre les composants sous forme d'API publiques, et à chaque frontière, c'est souvent une bonne idée de valider les paramètres d'entrée, même si "la logique du programme doit être si solide que ces contrôles sont redondants. ". Cela facilite la détection des bogues et contribue à réduire les temps de débogage.

Dans votre cas, vous devez décider vous-même du type de cycle de vie que vous attendez de votre programme. S'agit-il d'un programme que vous prévoyez d'utiliser et de maintenir au fil des ans? Ensuite, l'ajout d'une vérification redondante est probablement préférable, car il n'est pas improbable que votre code soit refactorisé à l'avenir et que vos fonctions readet writepuissent être utilisées dans un contexte différent.

Ou s'agit-il d'un petit programme uniquement à des fins d'apprentissage ou de plaisir? Ensuite, ces doubles vérifications ne seront pas nécessaires.

Dans le cadre du "Clean Code", on pourrait se demander si une double vérification viole le principe DRY. En fait, parfois, c'est le cas, au moins dans une certaine mesure: la validation des entrées peut être interprétée comme faisant partie de la logique métier d'un programme, et le faire à deux endroits peut entraîner les problèmes de maintenance habituels causés par la violation de DRY. Robustesse vs DRY est souvent un compromis - la robustesse nécessite une redondance dans le code, tandis que DRY essaie de minimiser la redondance. Et avec la complexité croissante du programme, la robustesse devient de plus en plus importante que d'être DRY dans la validation.

Enfin, permettez-moi de donner un exemple de ce que cela signifie dans votre cas. Supposons que vos besoins changent en quelque chose comme

  • le programme doit également fonctionner avec un argument, le nom du fichier d'entrée, si aucun nom de fichier de sortie n'est donné, il est automatiquement construit à partir du nom du fichier d'entrée en remplaçant le suffixe.

Cela signifie-t-il que vous devrez probablement modifier votre double validation à deux endroits? Probablement pas, une telle exigence entraîne un changement lors de l'appel argparse, mais aucun changement dans writeToOutputFile: cette fonction nécessitera toujours un nom de fichier. Donc, dans votre cas, je voterais pour faire la validation d'entrée deux fois, le risque d'avoir des problèmes de maintenance en raison du fait d'avoir deux endroits à changer est à mon humble avis beaucoup plus faible que le risque d'avoir des problèmes de maintenance en raison d'erreurs masquées causées par trop peu de vérifications.

Doc Brown
la source
"... les frontières entre les composants sous forme d'API publiques ..." J'observe que les "classes sautent les frontières" pour ainsi dire. Il faut donc une classe; une classe de domaine métier cohérente. Je déduis de cet OP que le principe omniprésent de "c'est simple, donc pas besoin d'une classe" est à l'œuvre ici. Il pourrait y avoir une classe simple enveloppant l '"objet principal", appliquant des règles métier telles que "un fichier doit avoir un nom" qui non seulement DRYs le code existant mais le maintient SEC à l'avenir.
radarbob
@radarbob: ce que j'ai écrit ne se limite pas à la POO ou aux composants sous forme de classes. Cela s'applique également aux bibliothèques arbitraires avec une API publique, orientée objet ou non.
Doc Brown
5

La redondance n'est pas le péché. La redondance inutile est.

  1. Si readFromInputFile()et writeToOutputFile()sont des fonctions publiques (et selon les conventions de dénomination Python, car leurs noms ne commencent pas par deux traits de soulignement), les fonctions pourraient un jour être utilisées par quelqu'un qui éviterait complètement l'argparse. Cela signifie que lorsqu'ils abandonnent les arguments, ils ne voient pas votre message d'erreur d'argparse personnalisé.

  2. Si readFromInputFile()et writeToOutputFile()vérifiez les paramètres eux-mêmes, vous obtenez à nouveau un message d'erreur personnalisé qui explique la nécessité de noms de fichiers.

  3. Si readFromInputFile()et writeToOutputFile()ne vérifiez pas les paramètres eux-mêmes, aucun message d'erreur personnalisé ne s'affiche. L'utilisateur devra déterminer lui-même l'exception qui en résulte.

Tout se résume à 3. Écrivez du code qui utilise réellement ces fonctions en évitant argparse et produisez le message d'erreur. Imaginez que vous n'avez pas du tout regardé à l'intérieur de ces fonctions et que vous vous fiez simplement à leurs noms pour fournir suffisamment de compréhension à utiliser. Quand c'est tout ce que vous savez, y a-t-il un moyen d'être dérouté par l'exception? Faut-il un message d'erreur personnalisé?

Il est difficile de désactiver la partie de votre cerveau qui se souvient de l'intérieur de ces fonctions. À tel point que certains recommandent d'écrire le code d'utilisation avant le code utilisé. De cette façon, vous arrivez au problème en sachant déjà à quoi les choses ressemblent de l'extérieur. Vous n'avez pas à faire TDD pour cela, mais si vous faites TDD, vous arriverez déjà de l'extérieur en premier.

candied_orange
la source
4

La mesure dans laquelle vous rendez vos méthodes autonomes et réutilisables est une bonne chose. Cela signifie que les méthodes devraient être indulgentes dans ce qu'elles acceptent et qu'elles devraient avoir des résultats bien définis (précis dans ce qu'elles renvoient). Cela signifie également qu'ils devraient être en mesure de gérer avec élégance tout ce qui leur est transmis et de ne faire aucune hypothèse sur la nature de l'entrée, la qualité, le calendrier, etc.

Si un programmeur a l'habitude d'écrire des méthodes qui font des hypothèses sur ce qui se passe, basées sur des idées comme "si cela est cassé, nous avons de plus grandes choses à craindre" ou "le paramètre X ne peut pas avoir la valeur Y parce que le reste de le code l'empêche ", alors tout d'un coup vous n'avez plus vraiment de composants indépendants et découplés. Vos composants dépendent essentiellement d'un système plus large. C'est une sorte de couplage serré subtil et conduit à une augmentation exponentielle du coût total de possession à mesure que la complexité du système augmente.

Notez que cela peut signifier que vous validez les mêmes informations plusieurs fois. Mais c'est OK. Chaque composant est responsable de sa propre validation à sa manière . Ce n'est pas une violation de DRY, car les validations sont effectuées par des composants indépendants découplés, et une modification de la validation dans l'un ne doit pas nécessairement être répliquée exactement dans l'autre. Il n'y a pas de redondance ici. X a la responsabilité de vérifier ses entrées pour ses propres besoins et d'en transmettre à Y. Y a sa propre responsabilité de vérifier ses propres entrées pour ses besoins .

Brad Thomas
la source
1

Supposons que vous ayez une fonction (en C)

void readInputFile (const char* path);

Et vous ne pouvez trouver aucune documentation sur le chemin. Et puis vous regardez la mise en œuvre et il est dit

void readInputFile (const char* path)
{
    assert (path != NULL && strlen (path) > 0);

Non seulement cela teste l'entrée de la fonction, mais cela indique également à l'utilisateur de la fonction que le chemin n'est pas autorisé à être NULL ou une chaîne vide.

gnasher729
la source
0

En général, la double vérification n'est pas toujours bonne ou mauvaise. Il y a toujours de nombreux aspects de la question dans votre cas particulier dont dépend la question. Dans ton cas:

  • Quelle est la taille du programme? Plus il est petit, plus il est évident que l'appelant fait la bonne chose. Lorsque votre programme s'agrandit, il devient plus important de spécifier exactement quelles sont les conditions préalables et postconditions de chaque routine.
  • les arguments sont déjà vérifiés par le argparsemodule. C'est souvent une mauvaise idée d'utiliser une bibliothèque et de faire son travail vous-même. Pourquoi utiliser la bibliothèque alors?
  • Quelle est la probabilité que votre méthode soit réutilisée dans un contexte où l'appelant ne vérifie pas les arguments? Plus il est probable, plus il est important de valider les arguments.
  • Qu'est - ce qui se passe si un argument ne va manquer? Si vous ne trouvez pas de fichier d'entrée, le traitement s'arrêtera probablement complètement. C'est probablement un mode de défaillance évident qui est facile à rectifier. Les erreurs insidieuses sont celles où le programme continue joyeusement de fonctionner et produit de mauvais résultats sans que vous vous en rendiez compte .
Kilian Foth
la source
0

Vos contre-vérifications semblent avoir lieu à des endroits où elles sont rarement utilisées. Ces vérifications renforcent donc simplement votre programme:

Un chèque de trop ne fera pas de mal, un de moins le pourrait.

Cependant, si vous vérifiez à l'intérieur d'une boucle qui se répète souvent, vous devriez penser à supprimer la redondance, même si la vérification elle-même n'est généralement pas coûteuse par rapport à ce qui suit après la vérification.

qwerty_so
la source
Et puisque vous l'avez déjà dedans, cela ne vaut pas la peine d'être retiré, à moins qu'il ne soit dans une boucle ou quelque chose.
StarWeaver
0

Vous pourriez peut-être changer votre point de vue:

En cas de problème, quel est le résultat? Cela nuira-t-il à votre application / à l'utilisateur?

Bien sûr, vous pourriez toujours vous demander si plus ou moins de contrôles sont meilleurs ou pires, mais c'est une question plutôt scolaire. Et puisque vous traitez avec des logiciels du monde réel , il y a des conséquences réelles.

Du contexte que vous donnez:

  • un fichier d'entrée A
  • un fichier de sortie B

Je suppose que vous faites la transformation de A à B . Si A et B sont petits et la transformation est petite, quelles sont les conséquences?

1) Vous avez oublié de préciser où lire: alors le résultat n'est rien . Et le temps d'exécution sera plus court que prévu. Vous regardez le résultat - ou mieux: recherchez un résultat manquant, voyez que vous avez invoqué la commande de manière incorrecte, recommencez et tout va bien à nouveau

2) Vous avez oublié de spécifier le fichier de sortie. Il en résulte différents scénarios:

a) L'entrée est lue immédiatement. Ensuite, la transformation démarre et le résultat doit être écrit, mais à la place, vous recevez une erreur. Selon le temps, votre utilisateur doit attendre (en fonction de la masse de données à traiter), cela peut être gênant.

b) L'entrée est lue étape par étape. Ensuite, le processus d'écriture se termine immédiatement comme en (1) et l'utilisateur recommence.

Une vérification bâclée peut être considérée comme OK dans certaines circonstances. Cela dépend entièrement de votre cas d'utilisation et de votre intention.

De plus: vous devez éviter la paranoïa et ne pas faire trop de doublons.

Thomas Junk
la source
0

Je dirais que les tests ne sont pas redondants.

  • Vous avez deux fonctions publiques qui nécessitent un nom de fichier comme paramètre d'entrée. Il convient de valider leurs paramètres. Les fonctions pourraient potentiellement être utilisées dans n'importe quel programme qui a besoin de leurs fonctionnalités.
  • Vous avez un programme qui nécessite deux arguments qui doivent être des noms de fichiers. Il arrive d'utiliser les fonctions. Il convient que le programme vérifie ses paramètres.

Bien que les noms de fichiers soient vérifiés deux fois, ils sont vérifiés à des fins différentes. Dans un petit programme où vous pouvez faire confiance aux paramètres des fonctions qui ont été vérifiés, les contrôles dans les fonctions peuvent être considérés comme redondants.

Une solution plus robuste aurait un ou deux validateurs de nom de fichier.

  • Pour un fichier d'entrée, vous souhaiterez peut-être vérifier que le paramètre spécifie un fichier lisible.
  • Pour un fichier de sortie, vous souhaiterez peut-être vérifier que le paramètre est un fichier accessible en écriture ou un nom de fichier valide qui peut être créé et écrit.

J'utilise deux règles pour savoir quand effectuer des actions:

  • Faites-les le plus tôt possible. Cela fonctionne bien pour les choses qui seront toujours nécessaires. Du point de vue de ce programme, il s'agit de la vérification des valeurs argv, et les validations ultérieures dans la logique des programmes seraient redondantes. Si les fonctions sont déplacées vers une bibliothèque, elles ne sont plus redondantes, car la bibliothèque ne peut pas garantir que tous les appelants ont validé les paramètres.
  • Faites-les le plus tard possible. Cela fonctionne extrêmement bien pour les choses qui seront rarement nécessaires. Du point de vue de ce programme, ce sont les vérifications des paramètres de fonction.
BillThor
la source
0

Le chèque est redondant. Pour résoudre ce problème, vous devez supprimer readFromInputFile et writeToOutputFile et les remplacer par readFromStream et writeToStream.

Au moment où le code reçoit le flux de fichiers, vous savez que vous avez un flux valide connecté à un fichier valide ou tout autre élément auquel un flux peut être connecté. Cela évite les contrôles redondants.

Vous pourriez alors demander, eh bien, vous devez toujours ouvrir le flux quelque part. Oui, mais cela se produit en interne dans la méthode d'analyse des arguments. Vous avez là deux vérifications, une pour vérifier qu'un nom de fichier est requis, l'autre est une vérification que le fichier pointé par le nom de fichier est un fichier valide dans le contexte donné (par exemple, le fichier d'entrée existe, le répertoire de sortie est accessible en écriture). Ce sont différents types de contrôles, donc ils ne sont pas redondants et ils se produisent dans la méthode d'analyse des arguments (périmètre d'application) plutôt que dans l'application principale.

Lie Ryan
la source