Un bon style de code pour introduire des vérifications de données partout?

10

J'ai un projet suffisamment grand pour que je ne puisse plus garder tous les aspects en tête. Je traite avec un certain nombre de classes et de fonctions, et je transmets des données.

Avec le temps, j'ai remarqué que je continuais à recevoir des erreurs, car j'avais oublié la forme précise que les données devaient avoir lorsque je les transmettais à différentes fonctions ( par exemple, une fonction accepte et génère un tableau de chaînes, une autre fonction, que j'ai écrite beaucoup plus tard, accepte les chaînes qui sont conservées dans un dictionnaire, etc., donc je dois transformer les chaînes avec lesquelles je travaille, de les avoir dans un tableau à les avoir dans un dictionnaire ).

Pour éviter d'avoir toujours à comprendre ce qui s'est cassé où, j'ai commencé à traiter chaque fonction et classe comme étant une "entité isolée" dans le sens où elle ne peut pas s'appuyer sur du code extérieur lui donnant la bonne entrée et doit effectuer elle-même des vérifications d'entrée (ou, dans certains cas, refonte des données, si les données sont fournies sous une forme incorrecte).

Cela a considérablement réduit le temps que je passe à m'assurer que les données que je transmets "tiennent" dans chaque fonction, car les classes et les fonctions elles-mêmes m'avertissent désormais lorsqu'une entrée est mauvaise (et parfois même corrige cela) et je ne le fais pas avoir à aller avec un débogueur à travers le code entier pour comprendre où quelque chose s'est détraqué.

D'un autre côté, cela a également augmenté le code global.
Ma question est, si ce style de code est approprié pour résoudre ce problème?
Bien sûr, la meilleure solution serait de refactoriser complètement le projet et de s'assurer que les données ont une structure uniforme pour toutes les fonctions - mais comme ce projet est en croissance constante, je finirais par dépenser plus et m'inquiéter du code propre que d'ajouter réellement de nouvelles choses .

(FYI: Je suis toujours un débutant, alors veuillez excuser si cette question était naïve; mon projet est en Python.)

user7088941
la source
3
@gnat C'est similaire, mais au lieu de répondre à ma question, il fournit des conseils ("soyez aussi défensif que possible") pour cette instance spécifique mentionnée par OP, ce qui est différent de ma requête plus générale.
user7088941
2
"mais comme ce projet est en croissance constante, je finirais par dépenser plus et m'inquiéter du code propre plutôt que d'ajouter de nouvelles choses" - cela ressemble beaucoup à ce que vous devez commencer à vous soucier du code propre. Sinon, vous constaterez que votre productivité ralentit et ralentit, car chaque nouvelle fonctionnalité est de plus en plus difficile à ajouter en raison du code existant. Le refactoring n'a pas besoin d'être "complet", si l'ajout de quelque chose de nouveau est difficile à cause du code existant qu'il touche, refactorisez juste ce code touchant et notez ce que vous aimeriez revoir plus tard
matt freake
3
C'est un problème que les gens rencontrent souvent en utilisant des langues faiblement typées. Si vous ne voulez pas ou ne pouvez pas passer à une langue plus strictement typée, la réponse est simplement "oui, ce style de code est approprié pour résoudre ce problème" . Question suivante?
Doc Brown
1
Dans un langage strictement typé, avec des types de données appropriés définis, le compilateur l'aurait fait pour vous.
SD

Réponses:

4

Une meilleure solution consiste à tirer davantage parti des fonctionnalités et des outils du langage Python.

Par exemple, dans la fonction 1, l'entrée attendue est un tableau de chaînes, où la première chaîne indique le titre de quelque chose et la seconde une référence bibliographique. Dans la fonction 2, l'entrée attendue est toujours un tableau de chaînes, mais maintenant les rôles des chaînes sont inversés.

Ce problème est atténué avec un namedtuple. Il est léger et donne une signification sémantique facile aux membres de votre tableau.

Pour profiter de la vérification automatique de type sans changer de langue, vous pouvez profiter de l' indication de type . Un bon IDE peut l'utiliser pour vous faire savoir quand vous faites quelque chose de stupide.

Vous semblez également inquiet de voir les fonctions devenir obsolètes lorsque les exigences changent. Cela peut être détecté par des tests automatisés .

Bien que je ne dise pas que la vérification manuelle n'est jamais appropriée, une meilleure utilisation des fonctionnalités linguistiques disponibles peut vous aider à résoudre ce problème de manière plus maintenable.


la source
+1 pour m'avoir pointé namedtupleet toutes les autres belles choses. Je ne savais pas namedtuple- et même si je connaissais les tests automatisés, je ne l'ai jamais vraiment utilisé beaucoup et je ne savais pas à quel point cela pourrait m'aider dans ce cas. Tout cela semble vraiment être aussi bon qu'une analyse statique. (Les tests automatisés pourraient même être meilleurs, car je peux attraper toutes les choses subtiles qui ne seraient pas prises dans une analyse statique!) Si vous en connaissez d'autres, faites-le moi savoir. Je vais laisser la question ouverte encore un peu, mais si aucune autre réponse ne vient, j'accepterai la vôtre.
user7088941
9

OK, le problème réel est décrit dans un commentaire sous cette réponse:

Par exemple, dans la fonction 1, l'entrée attendue est un tableau de chaînes, où la première chaîne indique le titre de quelque chose et la seconde une référence bibliographique. Dans la fonction 2, l'entrée attendue est toujours un tableau de chaînes, mais maintenant les rôles des chaînes sont inversés

Le problème ici est l'utilisation d'une liste de chaînes où l'ordre signifie la sémantique. Il s'agit d'une approche propice aux erreurs. Au lieu de cela, vous devez créer une classe personnalisée avec deux champs nommés titleet bibliographical_reference. De cette façon, vous n'allez pas les mélanger et vous éviterez ce problème à l'avenir. Bien sûr, cela nécessite une refactorisation si vous utilisez déjà des listes de chaînes dans de nombreux endroits, mais croyez-moi, ce sera moins cher à long terme.

L'approche courante dans les langages de types dynamiques est le "typage de canard", ce qui signifie que vous ne vous souciez pas vraiment du "type" de l'objet passé, vous ne vous souciez que s'il prend en charge les méthodes que vous appelez. Dans votre cas, vous lirez simplement le champ appelé bibliographical_referencelorsque vous en aurez besoin. Si ce champ n'existe pas sur l'objet passé, vous obtiendrez une erreur, ce qui indique que le mauvais type est passé à la fonction. C'est une vérification de type aussi bonne que n'importe quelle autre.

JacquesB
la source
Parfois, le problème est encore plus subtil: je passe le type correct, mais la "structure interne" de mon entrée gâche la fonction: par exemple, dans la fonction 1, l'entrée attendue est un tableau de chaînes, où la première chaîne désigne le titre de quelque chose et le second une référence bibliographique. Dans la fonction 2, l'entrée attendue est toujours un tableau de chaînes, mais maintenant les rôles des chaînes sont inversés: la première chaîne doit être la référence bibliographique et la seconde doit être la référence bibliographique. Je suppose que ces contrôles sont appropriés?
user7088941
1
@ user7088941: Le problème que vous décrivez pourrait être facilement résolu en ayant une classe avec deux champs: "title" et "bibliographical_reference". Vous ne mélangerez pas cela. S'appuyer sur l'ordre dans une liste de chaînes semble très sujet aux erreurs. C'est peut-être le problème sous-jacent?
JacquesB
3
Telle est la réponse. Python est un langage orienté objet, pas un langage orienté liste de dictionnaires de chaînes vers entiers (ou autre). Alors, utilisez des objets. Les objets sont responsables de la gestion de leur propre état et de l'application de leurs propres invariants, les autres objets ne peuvent jamais les corrompre (s'ils sont correctement conçus). Si des données non structurées ou semi-structurées pénètrent dans votre système de l'extérieur, vous validez et analysez une fois aux limites du système et convertissez-les en objets riches dès que possible.
Jörg W Mittag
3
"J'éviterais vraiment une refactorisation constante" - ce blocage mental est votre problème. Un bon code ne résulte que de la refactorisation. Beaucoup de refactoring. Soutenu par des tests unitaires. Surtout lorsque les composants doivent être étendus ou évolués.
Doc Brown
2
Je l'ai maintenant. +1 pour toutes les bonnes idées et commentaires. Et merci à tous pour leurs commentaires incroyablement utiles! (Pendant que j'utilisais certaines classes / objets, je les ai entrecoupées avec les listes mentionnées, ce qui, comme je le vois maintenant, n'était pas une bonne idée. La question restait de savoir comment le mettre en œuvre au mieux, où j'ai utilisé les suggestions concrètes de la réponse de JETM. , ce qui a vraiment fait une différence radicale en termes de vitesse pour atteindre un état sans bogue.)
user7088941
3

Tout d'abord, ce que vous ressentez en ce moment, c'est l' odeur de code - essayez de vous rappeler ce qui vous a amené à devenir conscient de l'odeur et à affiner votre nez "mental", car plus tôt vous remarquerez une odeur de code, plus tôt - et plus facilement - vous pouvez résoudre le problème sous-jacent.

Pour éviter d'avoir toujours à comprendre ce qui a éclaté, j'ai commencé à traiter chaque fonction et classe comme étant une "entité isolée" dans le sens où elle ne peut pas compter sur du code extérieur lui donnant la bonne entrée et doit effectuer elle-même des vérifications d'entrée.

La programmation défensive - comme cette technique est appelée - est un outil valide et souvent utilisé. Cependant, comme pour tout, il est important d'utiliser la bonne quantité, trop peu de chèques et vous n'attraperez pas de problèmes, trop et votre code sera trop gonflé.

(ou, dans certains cas, refonte des données, si les données sont fournies sous une forme incorrecte).

Cela pourrait être une moins bonne idée. Si vous remarquez qu'une partie de votre programme appelle une fonction avec des données incorrectement formatées, CORRIGEZ CETTE PARTIE , ne modifiez pas la fonction appelée pour pouvoir digérer les mauvaises données de toute façon.

Cela a considérablement réduit le temps que je passe à m'assurer que les données que je transmets "tiennent" dans chaque fonction, car les classes et les fonctions elles-mêmes m'avertissent désormais lorsqu'une entrée est mauvaise (et parfois même corrige cela) et je ne le fais pas avoir à aller avec un débogueur à travers le code entier pour comprendre où quelque chose s'est détraqué.

L'amélioration de la qualité et de la maintenabilité de votre code est un gain de temps à long terme (en ce sens, je dois à nouveau mettre en garde contre la fonctionnalité d'autocorrection que vous avez intégrée à certaines de vos fonctions - elles pourraient être une source insidieuse de bogues. Tout simplement parce que votre le programme ne plante pas et ne brûle pas ne signifie pas qu'il fonctionne correctement ...)

Pour répondre enfin à votre question: Oui, une programmation défensive (c'est-à-dire vérifier la validité des paramètres fournis) est - à un degré sain - une bonne stratégie. Cela dit , comme vous l'avez dit vous-même, votre code est incohérent, et je vous recommande fortement de passer du temps à refactoriser les parties qui sentent - vous avez dit que vous ne voulez pas vous soucier du code propre tout le temps, en passant plus de temps sur "nettoyage" que sur les nouvelles fonctionnalités ... Si vous ne gardez pas votre code propre, vous pourriez passer deux fois plus de temps à "éviter" de ne pas garder un code propre pour éliminer les bogues ET aurez du mal à implémenter de nouvelles fonctionnalités - la dette technique peut vous écraser.

CharonX
la source
1

C'est bon. J'avais l'habitude de coder dans FoxPro, où j'avais un bloc TRY..CATCH presque dans chaque grande fonction. Maintenant, je code en JavaScript / LiveScript et vérifie rarement les paramètres dans les fonctions "internes" ou "privées".

"Combien vérifier" dépend plus du projet / langage choisi que de votre compétence en code.

Michael Quad
la source
1
Je suppose que c'était TRY ... CATCH ... IGNORE. Vous avez fait le contraire de ce que le PO demande. À mon humble avis, leur objectif est d'éviter les incohérences pendant que le vôtre veillait à ce que le programme ne explose pas en en frappant un.
maaartinus
1
@maaartinus c'est correct. Les langages de programmation nous donnent généralement des constructions qui sont simples à utiliser pour empêcher l'application de l'explosion - mais les langages de programmation des constructions nous donnent pour éviter les incohérences semblent être beaucoup plus difficiles à utiliser: à ma connaissance, refactorisez constamment tout et utilisez les classes qui conteneurisent le mieux flux d'informations dans votre application. C'est précisément ce que je demande - existe-t-il un moyen plus facile de résoudre ce problème.
user7088941
@ user7088941 C'est pourquoi j'évite les langues faiblement typées. Python est juste fantastique, mais pour quelque chose de plus grand, je ne peux pas garder une trace de ce que j'ai fait ailleurs. Par conséquent, je préfère Java, qui est plutôt verbeux (pas tellement avec les fonctionnalités de Lombok et Java 8), a un typage strict et des outils pour l'analyse statique. Je vous suggère d'essayer un langage strictement typé car je ne sais pas comment le résoudre autrement.
maaartinus
Il ne s'agit pas d'un paramètre typé strict / lâche. Il s'agit de savoir que ce paramètre est correct. Même si vous utilisez (entier 4 octets), vous devrez peut-être vérifier s'il se trouve dans une plage de 0 à 10 par exemple. Si vous savez que ce paramètre est toujours 0..10, vous n'avez pas besoin de le vérifier. FoxPro n'a pas de tableaux associatifs par exemple, il est très difficile de fonctionner avec ses variables, leur portée et ainsi de suite .. c'est pourquoi vous devez vérifier check check ..
Michael Quad
1
@ user7088941 Ce n'est pas OO, mais il y a la règle "échec rapide". Chaque méthode non privée doit vérifier ses arguments et lancer quand quelque chose ne va pas. Aucun essai, aucune tentative pour le réparer, il suffit de le faire exploser. Bien sûr, à un niveau supérieur, l'exception est enregistrée et gérée. Comme vos tests trouvent la plupart des problèmes à l'avance et qu'aucun problème n'est masqué, le code converge vers une solution sans bogue beaucoup plus rapidement que lorsqu'il est tolérant aux erreurs.
maaartinus