Comment dois-je gérer les entrées utilisateur non valides?


12

Je réfléchis à ce problème depuis un certain temps et je serais curieux d'avoir des opinions d'autres développeurs.

J'ai tendance à avoir un style de programmation très défensif. Mon bloc ou méthode typique ressemble à ceci:

T foo(par1, par2, par3, ...)
{
    // Check that all parameters are correct, return undefined (null)
    // or throw exception if this is not the case.

    // Compute and (possibly) return result.
}

De plus, pendant le calcul, je vérifie tous les pointeurs avant de les déréférencer. Mon idée est que, s'il y a un bug et qu'un pointeur NULL devrait apparaître quelque part, mon programme devrait gérer cela correctement et refuser simplement de continuer le calcul. Bien sûr, il peut signaler le problème avec un message d'erreur dans le journal ou un autre mécanisme.

Pour le dire de manière plus abstraite, mon approche est

if all input is OK --> compute result
else               --> do not compute result, notify problem

D'autres développeurs, dont certains de mes collègues, utilisent une autre stratégie. Par exemple, ils ne vérifient pas les pointeurs. Ils supposent qu'un morceau de code doit recevoir une entrée correcte et qu'il ne devrait pas être responsable de ce qui se passe si l'entrée est incorrecte. De plus, si une exception de pointeur NULL plante le programme, un bogue sera trouvé plus facilement pendant les tests et aura plus de chances d'être corrigé.

Ma réponse est normalement: mais que se passe-t-il si le bogue n'est pas trouvé pendant les tests et apparaît lorsque le produit est déjà utilisé par le client? Quelle est la meilleure façon pour le bogue de se manifester? Doit-il s'agir d'un programme qui n'exécute pas une certaine action, mais qui peut continuer à fonctionner, ou d'un programme qui se bloque et doit être redémarré?

Résumer

Laquelle des deux approches pour gérer les entrées erronées conseilleriez-vous?

Inconsistent input --> no action + notification

ou

Inconsistent input --> undefined behaviour or crash

Éditer

Merci pour les réponses et suggestions. Je suis aussi fan de design par contrat. Mais même si je fais confiance à la personne qui a écrit le code appelant mes méthodes (c'est peut-être moi-même), il peut toujours y avoir des bugs, conduisant à une mauvaise saisie. Mon approche consiste donc à ne jamais supposer qu'une méthode a été correctement saisie.

De plus, j'utiliserais un mécanisme pour attraper le problème et en informer. Sur un système de développement, il ouvrirait par exemple une boîte de dialogue pour avertir l'utilisateur. Dans un système de production, il suffit d'écrire quelques informations dans le journal. Je ne pense pas que des vérifications supplémentaires puissent entraîner des problèmes de performances. Je ne sais pas si les affirmations sont suffisantes, si elles sont désactivées dans un système de production: peut-être qu'une situation se produira dans une production qui ne s'était pas produite pendant les tests.

Quoi qu'il en soit, j'ai été vraiment surpris que de nombreuses personnes suivent l'approche opposée: elles laissent l'application se planter "à dessein" car elles soutiennent que cela facilitera la recherche de bogues pendant les tests.


Codez toujours défensivement. Finalement, pour des raisons de performances, vous pouvez mettre un commutateur pour désactiver certains tests en mode de publication.
deadalnix

Aujourd'hui, j'ai corrigé un bug lié à une vérification de pointeur NULL manquante. Un objet a été créé lors de la déconnexion de l'application et le constructeur a utilisé un getter pour accéder à un autre objet qui n'était plus là. L'objet n'était pas censé être créé à ce stade. Il a été créé en raison d'un autre bug: un minuteur n'a pas été arrêté lors de la déconnexion -> un signal a été envoyé -> le destinataire a tenté de créer un objet -> le constructeur a demandé et utilisé un autre objet -> pointeur NULL -> crash ). Je ne voudrais vraiment pas qu'une telle situation désagréable plante mon application.
Giorgio

1
Règle de réparation: lorsque vous devez échouer, échouez bruyamment et dès que possible.
deadalnix

"Règle de réparation: lorsque vous devez échouer, échouez bruyamment et dès que possible.": Je suppose que tous les BSOD de Windows sont une application de cette règle. :-)
Giorgio

Réponses:


8

Vous avez raison. Soyez paranoïaque. Ne faites pas confiance à un autre code, même s'il s'agit de votre propre code. Vous oubliez des choses, vous faites des changements, le code évolue. Ne faites pas confiance au code extérieur.

Un bon point a été souligné ci-dessus: que faire si les entrées ne sont pas valides mais que le programme ne plante pas? Ensuite, vous obtenez des ordures dans la base de données et des erreurs sur toute la ligne.

Lorsqu'on me demande un nombre (par exemple, le prix en dollars ou le nombre d'unités), j'aime entrer "1e9" et voir ce que fait le code. Cela peut arriver.

Il y a quatre décennies, en obtenant mon BS en informatique de UCBerkeley, on nous a dit qu'un bon programme consiste à gérer les erreurs à 50%. Soyez paranoïaque.


Oui, à mon humble avis, c'est l'une des rares situations dans lesquelles être paranoïaque est une caractéristique et non un problème.
Giorgio

"Que se passe-t-il si les entrées ne sont pas valides mais que le programme ne plante pas? Ensuite, vous obtenez des ordures dans la base de données et des erreurs sur la ligne.": Au lieu de planter, le programme pourrait refuser d'effectuer l'opération et retourner un résultat indéfini. Undefined se propagera à travers le calcul et aucune ordure ne sera produite. Mais le programme n'a pas besoin de planter pour y parvenir.
Giorgio

Oui, mais - mon point est que le programme doit DETECTER l'entrée invalide et y faire face. Si l'entrée n'est pas vérifiée, cela fonctionnera dans le système et les choses désagréables viendront plus tard. Même planter, c'est mieux que ça!
Andy Canfield,

Je suis totalement d'accord avec vous: ma méthode ou fonction typique commence par une séquence de vérifications pour s'assurer que les données d'entrée sont correctes.
Giorgio

Aujourd'hui, j'ai encore une fois confirmé que la stratégie "tout vérifier, ne rien faire" est souvent une bonne idée. Un de mes collègues avait une exception de pointeur NULL en raison d'une vérification manquante. Il s'est avéré que dans ce contexte, il était correct d'avoir un pointeur NULL car certaines données n'avaient pas été chargées, et il était correct de vérifier le pointeur et de ne rien faire quand il est NULL. :-)
Giorgio

7

Vous avez déjà la bonne idée

Laquelle des deux approches pour gérer les entrées erronées conseilleriez-vous?

Saisie incohérente -> aucune action + notification

ou mieux

Saisie incohérente -> action correctement gérée

Vous ne pouvez pas vraiment adopter une approche de programmation (vous pouvez), mais vous vous retrouveriez avec une conception formelle qui fait les choses par habitude plutôt que par choix conscient.

Tempérer le dogmatisme avec pragmatisme.

Steve McConnell l'a dit le mieux

Steve McConnell a écrit à peu près le livre ( Code Complete ) sur la programmation défensive et c'est l'une des méthodes qu'il a conseillé de toujours valider vos entrées.

Je ne me souviens pas si Steve l'a mentionné, mais vous devriez envisager de le faire pour les méthodes et fonctions non privées , et uniquement pour d'autres lorsque cela est jugé nécessaire.


2
Au lieu de public, je suggérerais, toutes les méthodes non privées pour couvrir de manière défensive les langues qui ont protégé, partagé ou aucun concept de restriction d'accès (tout est public, implicitement).
JustinC

3

Il n'y a pas de réponse "correcte" ici, en particulier sans spécifier la langue, le type de code et le type de produit dans lequel le code peut entrer. Considérer:

  • La langue compte. Dans Objective-C, il est souvent correct d'envoyer des messages à nil; rien ne se passe, mais le programme ne plante pas non plus. Java n'a pas de pointeurs explicites, donc aucun pointeur n'est là une grande préoccupation. En C, vous devez être un peu plus prudent.

  • Être paranoïaque équivaut à des soupçons ou une méfiance déraisonnables et injustifiés. Ce n'est probablement pas mieux pour les logiciels que pour les gens.

  • Votre niveau de préoccupation doit être proportionnel au niveau de risque dans le code et à la difficulté probable d'identifier les problèmes qui apparaissent. Que se passe-t-il dans le pire des cas? L'utilisateur redémarre le programme et continue là où il s'était arrêté? L'entreprise perd des millions de dollars?

  • Vous ne pouvez pas toujours identifier une mauvaise entrée. Vous pouvez comparer religieusement vos pointeurs à zéro, mais cela n'attrape qu'une seule des 2 ^ 32 valeurs possibles, presque toutes mauvaises.

  • Il existe de nombreux mécanismes différents pour traiter les erreurs. Encore une fois, cela dépend dans une certaine mesure de la langue. Vous pouvez utiliser des macros d'assertion, des instructions conditionnelles, des tests unitaires, la gestion des exceptions, une conception soignée et d'autres techniques. Aucun d'entre eux n'est infaillible, et aucun n'est adapté à chaque situation.

Donc, cela se résume principalement à l'endroit où vous voulez mettre la responsabilité. Si vous écrivez une bibliothèque pour une utilisation par d'autres, vous voudrez probablement être aussi prudent que possible sur les entrées que vous obtenez, et faites de votre mieux pour émettre des erreurs utiles lorsque cela est possible. Dans vos propres fonctions et méthodes privées, vous pouvez utiliser des assertions pour détecter des erreurs idiotes, mais sinon, confier à l'appelant (c'est vous) la responsabilité de ne pas passer les ordures.


+1 - Bonne réponse. Ma principale préoccupation est qu'une mauvaise entrée peut provoquer un problème qui apparaît sur un système de production (quand il est trop tard pour y remédier). Bien sûr, je pense que vous avez tout à fait raison de dire que cela dépend des dommages qu'un tel problème peut causer à l'utilisateur.
Giorgio

La langue joue un grand rôle. En PHP, la moitié du code de votre méthode finit par vérifier de quel type est la variable et prendre l'action appropriée. En Java, si la méthode accepte un int, vous ne pouvez pas lui transmettre autre chose, donc votre méthode finit par être plus claire.
chap

1

Il devrait certainement y avoir une notification, telle qu'une exception levée. Il sert de signal à d'autres codeurs qui pourraient mal utiliser le code que vous avez écrit (en essayant de l'utiliser pour quelque chose qu'il n'était pas censé faire) que leur entrée est invalide ou entraîne des erreurs. Ceci est très utile pour rechercher les erreurs, alors que si vous retournez simplement null, leur code continuera jusqu'à ce qu'ils essaient d'utiliser le résultat et obtiennent une exception à partir d'un code différent.

Si votre code rencontre une erreur lors d'un appel à un autre code (peut-être une mise à jour de base de données échouée) qui dépasse la portée de ce morceau de code particulier, vous n'avez vraiment aucun contrôle sur lui et votre seul recours est de lever une exception expliquant ce vous savez (seulement ce que le code que vous avez appelé vous dit). Si vous savez que certaines entrées conduiront inévitablement à un tel résultat, vous ne pouvez tout simplement pas prendre la peine d'exécuter votre code et lever une exception indiquant quelle entrée n'est pas valide et pourquoi.

Sur une note plus liée à l'utilisateur final, il est préférable de renvoyer quelque chose de descriptif et de simple pour que tout le monde puisse le comprendre. Si votre client appelle et dit "le programme s'est écrasé, corrigez-le", vous avez beaucoup de travail à rechercher ce qui s'est mal passé et pourquoi, et en espérant pouvoir reproduire le problème. L'utilisation correcte des exceptions peut non seulement empêcher un plantage, mais également fournir des informations précieuses. Un appel d'un client disant "Le programme me donne une erreur. Il dit" XYZ n'est pas une entrée valide pour la méthode M, parce que Z est trop grand ", ou quelque chose de ce genre, même s'ils n'ont aucune idée de ce que cela signifie, vous savoir exactement où chercher. De plus, selon les pratiques commerciales de votre entreprise, il se peut que ce ne soit même pas vous qui résolvez ces problèmes, il est donc préférable de leur laisser une bonne carte.

Donc, la version courte de ma réponse est que votre première option est la meilleure.

Inconsistent input -> no action + notify caller

1

J'ai eu du mal avec ce même problème en passant par une classe universitaire en programmation. Je me suis penché vers le côté paranoïaque et j'ai tendance à tout vérifier, mais on m'a dit que c'était un comportement erroné.

On nous apprenait "Design par contrat". L'accent est mis sur le fait que les conditions préalables, les invariants et les post-conditions doivent être spécifiés dans les commentaires et les documents de conception. En tant que personne implémentant ma partie du code, je dois faire confiance à l'architecte logiciel et le responsabiliser en suivant les spécifications qui incluraient les conditions préalables (quelles entrées mes méthodes doivent être capables de gérer et quelles entrées je ne serai pas envoyées) . Une vérification excessive de chaque appel de méthode entraîne un ballonnement.

Les assertions doivent être utilisées lors des itérations de génération pour vérifier l'exactitude du programme (validation des conditions préalables, invariants, conditions de publication). Les assertions seraient alors désactivées dans la compilation de production.


0

L'utilisation des "assertions" est la voie à suivre pour informer les autres développeurs qu'ils font mal, dans les méthodes "privées" bien sûr . Leur activation / désactivation n'est qu'un indicateur à ajouter / supprimer au moment de la compilation et, en tant que tel, il est facile de supprimer des assertions du code de production. Il existe également un excellent outil pour savoir si vous vous trompez d'une manière ou d'une autre dans vos propres méthodes.

En ce qui concerne la vérification des paramètres d'entrée dans les méthodes publiques / protégées, je préfère travailler de manière défensive et vérifier les paramètres et lever InvalidArgumentException ou similaire. C'est pourquoi il y en a pour. Cela dépend aussi si vous écrivez une API ou non. S'il s'agit d'une API, et encore plus s'il s'agit de sources fermées, mieux valider tout pour que les développeurs sachent précisément ce qui n'a pas fonctionné. Sinon, si la source est disponible pour d'autres développeurs, elle n'est pas en noir / blanc. Soyez juste cohérent avec vos choix.

Edit: juste pour ajouter que si vous regardez par exemple le JDK Oracle, vous verrez qu'ils ne vérifient jamais "null" et laissent le code se bloquer. Puisqu'il va de toute façon lancer une NullPointerException, pourquoi s'embêter à vérifier la valeur null et à lever une exception explicite. Je suppose que cela a du sens.


En Java, vous obtenez une exception de pointeur nul. En C ++, un pointeur nul bloque l'application. Il y a peut-être d'autres exemples: division par zéro, index hors plage, etc.
Giorgio
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.