Dois-je ajouter du code redondant maintenant juste au cas où il serait nécessaire dans le futur?

114

À tort ou à raison, je suis actuellement convaincu que je devrais toujours essayer de rendre mon code aussi robuste que possible, même si cela implique d'ajouter du code / des contrôles redondants qui, je le sais , ne seront d'aucune utilité pour le moment, mais pourrait être x nombre d'années sur la ligne.

Par exemple, je travaille actuellement sur une application mobile qui contient ce morceau de code:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

En ce qui concerne plus particulièrement la section deux, je sais que si la section un est vraie, la section deux le sera également. Je ne vois aucune raison pour laquelle la première section serait fausse et la deuxième section évaluerait la vérité, ce qui rend la deuxième ifdéclaration superflue.

Toutefois, il se peut qu’à l’avenir cette seconde ifdéclaration soit réellement nécessaire et pour une raison connue.

Certaines personnes peuvent commencer par regarder cela et penser que je programme en pensant au futur, ce qui est évidemment une bonne chose. Mais je connais quelques cas où ce type de code m'a "caché" des bogues. Cela signifie qu'il m'a fallu encore plus de temps pour comprendre pourquoi la fonction xyzagit abcalors qu'elle devrait l'être def.

D'autre part, il y a eu de nombreux cas où ce type de code a rendu beaucoup plus facile l'amélioration du code avec de nouveaux comportements, car je n'ai pas à revenir en arrière et à m'assurer que toutes les vérifications pertinentes sont en place.

Existe-t-il des instructions générales concernant ce type de code? (J'aimerais aussi savoir si cela serait considéré comme une bonne ou une mauvaise pratique?)

NB: Ceci pourrait être considéré comme similaire à cette question, mais contrairement à cette question, j'aimerais une réponse en supposant qu'il n'y a pas de délai.

TLDR: Dois-je aller jusqu'à ajouter du code redondant afin de le rendre potentiellement plus robuste à l'avenir?

KidCode
la source
95
En développement logiciel, cela ne devrait jamais arriver tout le temps.
Roman Reiner
34
Si vous savez que cela if(rows.Count == 0)n'arrivera jamais, vous pouvez alors déclencher une exception - et vérifier ensuite pourquoi votre hypothèse est devenue fausse.
knut
9
Non pertinent pour la question, mais je soupçonne que votre code contient un bogue. Lorsque les lignes sont nulles, une nouvelle liste est créée et (je devine) jetée. Mais lorsque les lignes ne sont pas nulles, une liste existante est modifiée. Une meilleure solution pourrait être d’insister pour que le client transmette une liste vide ou non.
Theodore Norvell
9
Pourquoi serait rowsjamais nul? Il n'y a jamais de bonne raison, du moins dans .NET, pour qu'une collection soit nulle. Vide , bien sûr, mais pas nul . Je lève une exception si rowsest null, parce que cela signifie que l'appelant manque de logique.
Kyralessa

Réponses:

176

À titre d’exercice, vérifions d’abord votre logique. Bien que, comme nous le verrons, vous avez des problèmes plus importants que tout problème logique.

Appelez la première condition A et la deuxième condition B.

Vous dites d'abord:

En ce qui concerne plus particulièrement la section deux, je sais que si la section un est vraie, la section deux le sera également.

C'est-à-dire: A implique B ou, en termes plus fondamentaux (NOT A) OR B

Puis:

Je ne vois aucune raison pour laquelle la première section serait fausse et la deuxième section évaluerait la vérité, ce qui rendrait la deuxième si redondante.

C'est: NOT((NOT A) AND B). Appliquer la loi de Demorgan pour obtenir (NOT B) OR Aqui est B implique A.

Par conséquent, si vos deux affirmations sont vraies, alors A implique B et B implique A, ce qui signifie qu'elles doivent être égales.

Donc oui, les chèques sont redondants. Vous semblez avoir quatre chemins de code dans le programme, mais en fait vous n'en avez que deux.

Alors maintenant, la question est: comment écrire le code? La vraie question est: quel est le contrat déclaré de la méthode ? La question de savoir si les conditions sont redondantes est un fouillis rouge. La vraie question est "ai-je conçu un contrat raisonnable, et ma méthode met-elle clairement en œuvre ce contrat?"

Regardons la déclaration:

public static CalendarRow AssignAppointmentToRow(
    Appointment app,    
    List<CalendarRow> rows)

C'est public, il doit donc résister aux mauvaises données des appelants arbitraires.

Il retourne une valeur, il devrait donc être utile pour sa valeur de retour, pas pour son effet secondaire.

Et pourtant, le nom de la méthode est un verbe, suggérant qu’elle est utile pour ses effets secondaires.

Le contrat du paramètre de liste est:

  • Une liste nulle est OK
  • Une liste avec un ou plusieurs éléments est OK
  • Une liste sans éléments est fausse et ne devrait pas être possible.

Ce contrat est fou . Imaginez écrire la documentation pour cela! Imaginez écrire des cas de test!

Mon conseil: recommencez. Cette API a une interface de machine à bonbons écrite partout. (L'expression est tirée d'une vieille histoire sur les machines à confiserie de Microsoft, où les prix et les sélections sont des nombres à deux chiffres, et il est très facile de taper "85", qui est le prix du produit 75, et vous obtenez Amusant fait: oui, c'est ce que j'ai fait par erreur lorsque j'essayais de faire sortir de la gomme d'un distributeur automatique chez Microsoft!)

Voici comment concevoir un contrat raisonnable:

Rendez votre méthode utile soit pour son effet secondaire, soit pour sa valeur de retour, pas pour les deux.

N'acceptez pas de types mutables comme des entrées, comme des listes; si vous avez besoin d’une séquence d’informations, prenez un IEnumerable. Seulement lire la séquence; n'écrivez pas dans une collection transmise sauf s'il est très clair qu'il s'agit du contrat de la méthode. En prenant IEnumerable, vous envoyez un message à l'appelant lui indiquant que vous n'allez pas modifier sa collection.

Ne jamais accepter les nulls; une séquence nulle est une abomination. Demander à l'appelant de transmettre une séquence vide si cela a du sens, jamais jamais nul.

Si votre interlocuteur enfreint votre contrat, bloquez-le immédiatement pour lui apprendre que vous êtes une entreprise sérieuse et attraper ses bogues lors des tests et non de la production.

Concevez d’abord le contrat de manière à être aussi raisonnable que possible, puis mettez-le clairement en œuvre. C'est le moyen de pérenniser un design.

Maintenant, je n'ai parlé que de votre cas spécifique et vous avez posé une question générale. Voici donc quelques conseils généraux supplémentaires:

  • S'il existe un fait que vous pouvez déduire en tant que développeur mais que le compilateur ne peut pas, utilisez une assertion pour documenter ce fait. Si un autre développeur, comme vous ou l'un de vos collègues à l'avenir, enfreint cette hypothèse, l'affirmation vous le dira.

  • Obtenez un outil de couverture de test. Assurez-vous que vos tests couvrent chaque ligne de code. S'il y a du code non couvert, il vous manque un test ou vous avez un code mort. Le code mort est étonnamment dangereux parce qu’il n’est généralement pas destiné à être mort! On pense immédiatement à l'incroyable horrible faille de sécurité d'Apple "goto fail" il y a quelques années.

  • Obtenez un outil d'analyse statique. Heck, obtenez plusieurs; chaque outil a sa propre spécialité et aucun n'est un sur-ensemble des autres. Faites attention quand il vous dit qu'il y a du code inaccessible ou redondant. Encore une fois, ce sont probablement des insectes.

Si cela ressemble à ce que je dis: premièrement, concevez bien le code, et deuxièmement, testez-le pour vous assurer qu'il est correct aujourd'hui, eh bien, c'est ce que je dis. En agissant de la sorte, il sera beaucoup plus facile de faire face à l'avenir. La partie la plus difficile de l’avenir concerne tous les codes de machine à confiserie écrits par le passé. Faites-le bien aujourd'hui et les coûts seront moins élevés à l'avenir.

Eric Lippert
la source
24
Je n'avais jamais vraiment pensé à de telles méthodes auparavant, j'y pense maintenant, mais il me semble toujours essayer de couvrir toutes les éventualités, alors qu'en réalité, si la personne / méthode qui appelle ma méthode ne me donne pas ce dont j'ai besoin, alors je ne devrais pas. Ne pas essayer de réparer leurs erreurs (quand je ne sais pas vraiment ce qu’ils avaient l’intention), je devrais me retirer. Merci pour cela, une leçon précieuse a été apprise!
KidCode
4
Le fait qu'une méthode retourne une valeur n'implique pas qu'elle ne devrait pas non plus être utile pour ses effets secondaires. Dans un code concurrent, la capacité des fonctions à renvoyer les deux est souvent vitale (imaginez-en CompareExchangesans!), Et même dans des scénarios non simultanés, par exemple, "ajoutez un enregistrement s'il n'existe pas, et renvoyez le résultat transmis. record ou celui qui existait "peut être plus pratique que toute approche qui n'utilise ni effet secondaire ni valeur de retour.
Supercat
5
@KidCode Ouais, Eric est vraiment doué pour expliquer clairement les choses, même des sujets très compliqués. :)
Mason Wheeler
3
@supercat Bien sûr, mais il est également plus difficile de raisonner. Tout d'abord, vous voudrez probablement regarder quelque chose qui ne modifie pas un état global, évitant ainsi les problèmes de simultanéité et la corruption d'état. Lorsque cela n’est pas raisonnable, vous voudrez quand même isoler les deux, ce qui vous indique clairement les endroits où la concurrence est un problème (et donc considérés comme très dangereux) et les endroits où elle est gérée. C’est l’une des idées centrales du document original sur la POO et elle est au cœur de l’utilité des acteurs. Pas de règle religieuse - préférez simplement séparer les deux quand cela a du sens. C'est habituellement le cas.
Luaan
5
Ceci est un post très utile pour à peu près tout le monde!
Adrian Buzea
89

Ce que vous faites dans le code que vous montrez ci-dessus n’est pas tant une vérification pour le futur que pour un codage défensif .

Les deux ifdéclarations testent des choses différentes. Les deux sont des tests appropriés en fonction de vos besoins.

La section 1 teste et corrige un nullobjet. Remarque secondaire: la création de la liste ne crée aucun élément enfant (par exemple, CalendarRow).

La section 2 recherche et corrige les erreurs des utilisateurs et / ou de la mise en œuvre. Ce List<CalendarRow>n'est pas parce que vous avez un élément que la liste contient des éléments. Les utilisateurs et les implémenteurs feront des choses que vous ne pouvez pas imaginer simplement parce qu'ils sont autorisés à le faire, que cela soit logique pour vous ou non.

Adam Zuckerman
la source
1
En fait, je prends "astuces d'utilisateur stupide" pour signifier entrée. OUI vous ne devriez jamais faire confiance à l'entrée. Même juste en dehors de votre classe. Valider! Si vous pensez qu'il ne s'agit que d'une préoccupation future, je serais heureux de vous pirater aujourd'hui.
candied_orange
1
@CandiedOrange, c'était l'intention, mais le libellé ne traduisait pas la tentative d'humour. J'ai changé le libellé.
Adam Zuckerman
3
Juste une petite question ici, s'il s'agit d'une erreur d'implémentation / de mauvaises données, ne devrais-je pas simplement m'écraser au lieu d'essayer de corriger leurs erreurs?
KidCode
4
@KidCode Chaque fois que vous tentez de récupérer, "corrigez leurs erreurs", vous devez faire deux choses: revenir à un état correct connu et ne pas perdre discrètement une entrée précieuse. Suivre cette règle dans ce cas soulève la question suivante: une entrée de liste à zéro ligne est-elle une entrée utile?
candied_orange
7
Pas du tout d'accord avec cette réponse. Si une fonction reçoit une entrée invalide, cela signifie par définition qu'il y a un bogue dans le programme. La bonne approche consiste à lever une exception sur une entrée non valide afin de détecter le problème et de corriger le bogue. L'approche que vous décrivez va simplement masquer les bogues, ce qui les rend plus insidieux et plus difficile à détecter. Le codage défensif signifie que vous ne faites pas confiance automatiquement aux entrées, mais ne les validez pas, mais cela ne signifie pas que vous devez faire des suppositions aléatoires afin de "réparer" les entrées invalides ou inattendues.
JacquesB
35

Je suppose que cette question est fondamentalement au goût. Oui, c’est une bonne idée d’écrire du code robuste, mais le code de votre exemple est une légère violation du principe KISS (comme le sera un grand nombre de ce code "évolutif").

Personnellement, je n'aurais probablement pas la peine de rendre le code à l'épreuve des balles pour l'avenir. Je ne connais pas l'avenir et, en tant que tel, un tel code "à l'épreuve des balles" est condamné à échouer lamentablement lorsque le futur arrive de toute façon.

Au lieu de cela, je préférerais une approche différente: expliquez les hypothèses que vous expliquez à l'aide de la assert()macro ou d'installations similaires. De cette façon, lorsque le futur nous attend, il vous indiquera précisément où vos hypothèses ne sont plus conformes.

cmaster
la source
4
J'aime votre point de ne pas savoir ce que l'avenir nous réserve. Pour le moment, tout ce que je fais est vraiment de deviner ce qui pourrait être un problème, puis de chercher à nouveau la solution.
KidCode
3
@KidCode: Bonne observation. Votre propre pensée ici est en réalité beaucoup plus intelligente que la plupart des réponses, y compris celle que vous avez acceptée.
JacquesB
1
J'aime cette réponse. Maintenez le code minimal pour qu'un futur lecteur puisse facilement comprendre pourquoi des vérifications sont nécessaires. Si un futur lecteur voit des chèques pour des choses qui paraissent inutiles, il risque de perdre du temps à essayer de comprendre pourquoi il est là. Un futur humain peut être en train de modifier cette classe, plutôt que d'autres qui l'utilisent. Aussi, n'écrivez pas de code que vous ne pouvez pas déboguer , ce qui sera le cas si vous essayez de gérer des cas qui ne peuvent pas se produire actuellement. (Sauf si vous écrivez des tests unitaires qui exercent des chemins de code que le programme principal ne fait pas.)
Peter Cordes
23

Un autre principe auquel vous voudrez peut-être réfléchir est l’idée d’ échouer rapidement . L'idée est que lorsque quelque chose ne va pas dans votre programme, vous voulez l'arrêter complètement immédiatement, au moins pendant que vous le développez avant de le publier. En vertu de ce principe, vous souhaitez rédiger de nombreuses vérifications pour vous assurer que vos hypothèses sont vérifiées, mais envisagez sérieusement de faire en sorte que votre programme soit totalement bloqué chaque fois que ces hypothèses sont violées.

Pour être courageux, s'il y a même une petite erreur dans votre programme, vous voulez qu'il se bloque complètement pendant que vous regardez!

Cela peut sembler paradoxal, mais cela vous permet de détecter les bogues le plus rapidement possible lors du développement de routine. Si vous écrivez un morceau de code et que vous pensez que c'est fini, mais que ça se bloque lorsque vous le testez, il ne fait aucun doute que vous n'êtes pas encore fini. En outre, la plupart des langages de programmation vous offrent d’excellents outils de débogage qui sont plus faciles à utiliser lorsque votre programme se bloque complètement au lieu d’essayer de faire de son mieux après une erreur. L’exemple le plus important et le plus courant est que si vous bloquez votre programme en lançant une exception non gérée, le message d’exception vous indiquera une quantité incroyable d’informations sur le bogue, y compris la ligne de code ayant échoué et le chemin emprunté par votre programme. son chemin vers cette ligne de code (la trace de la pile).

Pour plus de réflexion, lisez ce court essai: Ne clouez pas votre programme en position verticale


Ceci est pertinent pour vous car il est possible que les vérifications que vous écrivez existent parfois, car vous souhaitez que votre programme continue à fonctionner même après un dysfonctionnement. Par exemple, considérons cette brève implémentation de la séquence de Fibonacci:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Cela fonctionne, mais que se passe-t-il si quelqu'un passe un nombre négatif à votre fonction? Cela ne fonctionnera pas alors! Vous voudrez donc ajouter une vérification pour vous assurer que la fonction est appelée avec une entrée non négative.

Il pourrait être tentant d'écrire la fonction comme ceci:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Cependant, si vous faites cela, puis plus tard, appelez accidentellement votre fonction Fibonacci avec une entrée négative, vous ne le réaliserez jamais! Pire encore, votre programme continuera probablement à fonctionner, mais commencera à produire des résultats absurdes sans vous donner la moindre idée de ce qui pourrait mal tourner. Ce sont les types de bogues les plus difficiles à résoudre.

Au lieu de cela, il est préférable d'écrire un chèque comme celui-ci:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Maintenant, si vous appelez accidentellement la fonction de Fibonacci avec une entrée négative, votre programme s’arrêtera immédiatement et vous fera savoir que quelque chose ne va pas. De plus, en vous donnant une trace de pile, le programme vous indiquera quelle partie de votre programme a essayé d'exécuter la fonction Fibonacci de manière incorrecte, ce qui vous donnera un excellent point de départ pour déboguer ce qui ne va pas.

Kevin
la source
1
C # n'a-t-il pas un type particulier d'exception pour indiquer des arguments non valides ou des arguments hors limites?
JDługosz
@ JDługosz Yep! C # a une exception ArgumentException et Java a une exception IllegalArgumentException .
Kevin
La question utilisait c #. Voici C ++ (lien vers le résumé) pour l'exhaustivité.
JDługosz
3
"Echec rapide à l'état de sécurité le plus proche" est plus raisonnable. Si vous configurez votre application de sorte qu'elle se bloque en cas d'imprévu, vous risquez de perdre les données de l'utilisateur. Les endroits dans lesquels les données de l'utilisateur sont compromises constituent d'excellents atouts pour la gestion des "avant-dernières" exceptions (il existe toujours des cas où vous ne pouvez rien faire d'autre que de lever la main, le crash ultime). Le seul fait de provoquer un crash dans le débogage consiste simplement à ouvrir une autre boîte de Pandore - vous devez quand même tester ce que vous déployez sur l'utilisateur, et maintenant vous passez la moitié de votre temps de test sur une version que l'utilisateur ne verra jamais.
Luaan
2
@ Luan: ce n'est cependant pas la responsabilité de la fonction exemple.
Whatsisname
11

Devez-vous ajouter du code redondant? Non.

Mais ce que vous décrivez n'est pas un code redondant .

Ce que vous décrivez est une programmation défensive visant à empêcher le code d’appel de violer les conditions préalables de votre fonction. Que vous le fassiez ou que vous laissiez simplement aux utilisateurs le soin de lire la documentation et d’éviter ces violations, est tout à fait subjectif.

Personnellement, je suis un grand partisan de cette méthodologie, mais comme pour tout, il faut être prudent. Prenons, par exemple, C ++ std::vector::operator[]. Mettant de côté l'implémentation en mode débogage de VS, cette fonction n'effectue pas de vérification des limites. Si vous demandez un élément qui n'existe pas, le résultat est indéfini. Il appartient à l'utilisateur de fournir un index de vecteur valide. C'est assez délibéré: vous pouvez "accepter" la vérification des limites en l'ajoutant au site d'appels, mais si la operator[]mise en œuvre devait l'exécuter, vous ne pourriez pas "désactiver". En tant que fonction de bas niveau, cela a du sens.

Mais si vous écriviez une AddEmployee(string name)fonction pour une interface de niveau supérieur, je m'attendrais parfaitement à ce que cette fonction lève au moins une exception si vous fournissez une valeur vide name, ainsi que cette condition préalable étant documentée immédiatement au-dessus de la déclaration de fonction. Vous ne fournissez peut-être pas aujourd’hui à cette fonction une entrée non normalisée de l'utilisateur, mais le rendre "sûr" de cette manière signifie que toute violation de condition préalable pouvant survenir dans le futur peut être facilement diagnostiquée, plutôt que de déclencher potentiellement une chaîne de dominos difficiles à gérer. détecter les bugs. Ce n'est pas redondant: c'est de la diligence.

Si je devais établir une règle générale (bien que, en règle générale, j'essaie de les éviter), je dirais une fonction qui satisfait l'un des éléments suivants:

  • vit dans un langage de très haut niveau (disons JavaScript plutôt que C)
  • est assis à une limite d'interface
  • n'est pas critique pour la performance
  • accepte directement la saisie de l'utilisateur

… Peut bénéficier d'une programmation défensive. Dans d'autres cas, vous pouvez toujours écrire des assertions qui se déclenchent pendant les tests mais sont désactivés dans les versions validées, pour augmenter encore votre capacité à détecter les bogues.

Ce sujet est approfondi sur Wikipedia ( https://en.wikipedia.org/wiki/Defensive_programming ).

Courses de légèreté en orbite
la source
9

Deux des dix commandements de programmation sont pertinents ici:

  • Tu ne supposeras pas que la saisie est correcte

  • Tu ne feras pas de code pour une utilisation future

Ici, vérifier la valeur null ne signifie pas "créer du code pour une utilisation future". Créer du code pour une utilisation future s'apparente à l'ajout d'interfaces, car vous pensez qu'elles pourraient être utiles "un jour". En d'autres termes, le commandement est de ne pas ajouter de couches d'abstraction à moins qu'elles ne soient nécessaires maintenant.

La vérification de la valeur null n'a rien à voir avec une utilisation future. Cela concerne le commandement n ° 1: ne présumez pas que la saisie sera correcte. Ne supposez jamais que votre fonction recevra un sous-ensemble d'entrées. Une fonction doit répondre de manière logique, peu importe la façon dont les entrées sont fictives et gâchées.

Tyler Durden
la source
5
Où sont ces commandements de programmation. avez vous un lien? Je suis curieux parce que j'ai travaillé sur plusieurs programmes qui souscrivent au deuxième de ces commandements, et plusieurs qui ne l'ont pas fait. Invariablement, ceux qui ont souscrit au commandement ont Thou shall not make code for future userencontré des problèmes de maintenabilité plus tôt , malgré la logique intuitive du commandement. J'ai constaté, dans le codage réel, que le commandement n'est efficace que dans le code où vous contrôlez la liste des fonctionnalités et les délais, et assurez-vous que vous n'avez pas besoin d'un code futur pour les atteindre ... c'est-à-dire, jamais.
Cort Ammon
1
Trivialement prouvable: si l’on peut estimer la probabilité d’une utilisation future et la valeur projetée du code "utilisation future", et que le produit de ces deux valeurs est supérieur au coût d’ajout du code "utilisation future", il est statistiquement optimal pour: ajoutez le code. Je pense que le commandement apparaît dans les situations où les développeurs (ou les gestionnaires) sont contraints d'admettre que leurs compétences en estimation ne sont pas aussi fiables qu'ils le voudraient. C'est donc une mesure défensive qu'ils choisissent de ne pas estimer des tâches futures.
Cort Ammon
2
@CortAmmon Il n'y a pas de place pour les commandements religieux dans la programmation - les "meilleures pratiques" n'ont de sens que dans leur contexte, et apprendre une "meilleure pratique" sans raisonnement vous empêche de vous adapter. J'ai trouvé YAGNI très utile, mais cela ne signifie pas que je ne pense pas aux endroits où ajouter des points d'extension plus tard sera coûteux, cela signifie simplement que je n'ai pas à penser aux cas simples à l'avance. Bien sûr, cela change aussi au fil du temps, à mesure que de plus en plus d'hypothèses sont intégrées à votre code, ce qui accroît effectivement l'interface de votre code - c'est un exercice d'équilibre.
Luaan
2
@CortAmmon Votre cas "trivialement prouvable" ignore deux coûts très importants: le coût de l'estimation elle-même et les coûts de maintenance du point d'extension (éventuellement inutile). C'est là que les gens obtiennent des estimations extrêmement peu fiables - en sous-estimant les estimations. Pour une fonctionnalité très simple, il suffit peut-être de réfléchir quelques secondes, mais il est fort probable que vous trouviez toute une boîte de vers qui découle de la fonctionnalité initialement "simple". La communication est la clé - à mesure que les choses grossissent, vous devez parler à votre leader / client.
Luaan
1
@Luaan J'essayais de défendre votre point de vue, il n'y a pas de place pour les commandements religieux dans la programmation. Dans la mesure où il existe une analyse de rentabilisation pour laquelle les coûts d'estimation et de maintenance du point d'extension sont suffisamment limités, il existe un cas dans lequel ledit "commandement" est discutable. D'après mon expérience avec le code, la question de savoir s'il faut laisser de tels points d'extension ne correspond jamais parfaitement à un commandement d'une seule ligne, dans un sens ou dans l'autre.
Cort Ammon
7

La définition de «code redondant» et de «YAGNI» dépend souvent de la mesure dans laquelle vous regardez.

Si vous rencontrez un problème, vous avez tendance à écrire le code futur de manière à éviter ce problème. Un autre programmeur qui n'a pas rencontré ce problème particulier pourrait considérer que votre code est excessif.

Ma suggestion est de garder le temps que vous passez sur "des choses qui n'ont pas encore erré" si elles se chargent et que vos pairs exploitent des fonctionnalités plus rapidement que vous, puis réduisez-les.

Cependant, si vous êtes comme moi, je suppose que vous aurez simplement tout tapé «par défaut» et que cela ne vous a pas vraiment pris plus longtemps.

Ewan
la source
6

Il est judicieux de documenter toute hypothèse concernant les paramètres. Et c'est une bonne idée de vérifier que votre code client ne viole pas ces hypothèses. Je voudrais faire ceci:

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[En supposant qu'il s'agisse de C #, Assert n'est peut-être pas le meilleur outil pour le travail, car il n'est pas compilé dans le code publié. Mais c'est un débat pour un autre jour.]

Pourquoi est-ce mieux que ce que vous avez écrit? Votre code a du sens si, dans un futur où votre client a changé, lorsque le client passe dans une liste vide, la bonne chose à faire sera d'ajouter une première ligne et d'ajouter une application à ses rendez-vous. Mais comment savez-vous que ce sera le cas? Mieux vaut faire moins d’hypothèses maintenant sur l’avenir.

Theodore Norvell
la source
5

Estimez le coût de l'ajout de ce code maintenant . Ce sera relativement bon marché parce que tout est frais dans votre esprit, vous pourrez donc le faire rapidement. Il est nécessaire d’ajouter des tests unitaires - il n’ya rien de pire que d’utiliser une méthode un an plus tard, cela ne fonctionne pas, et vous vous rendez compte qu’elle était cassée depuis le début et n’avait jamais réellement fonctionné.

Estimez le coût de l'ajout de ce code lorsque cela est nécessaire. Cela coûtera plus cher, car il faut revenir au code, tout en mémoire, et c'est beaucoup plus difficile.

Estimez la probabilité que le code supplémentaire soit réellement nécessaire. Ensuite, faites le calcul.

D'autre part, un code complet avec les hypothèses "X n'arrivera jamais" est terrible pour le débogage. Si quelque chose ne fonctionne pas comme prévu, cela signifie soit une erreur stupide, soit une hypothèse erronée. Votre "X n'arrivera jamais" est une hypothèse, et en présence d'un bogue, il est suspect. Ce qui oblige le prochain développeur à perdre du temps. Il est généralement préférable de ne pas s’appuyer sur de telles hypothèses.

gnasher729
la source
4
Dans votre premier paragraphe, vous avez oublié de mentionner le coût de la maintenance de ce code au fil du temps, lorsqu'il s'est avéré que la fonctionnalité réellement nécessaire était mutuellement exclusive avec la fonctionnalité qui avait été ajoutée inutilement. . .
Ruakh
Vous devez également estimer le coût des bogues qui pourraient s’infiltrer dans le programme car vous n’échouez pas sur une entrée invalide. Mais vous ne pouvez pas estimer le coût des bugs car, par définition, ils sont inattendus. Alors tout le "faire le calcul" tombe en morceaux.
JacquesB
3

La principale question ici est "que se passera-t-il si vous faites / ne faites pas"?

Comme d'autres l'ont souligné, ce type de programmation défensive est bon, mais il est aussi parfois dangereux.

Par exemple, si vous fournissez une valeur par défaut, le programme reste actif. Mais le programme ne fait peut-être pas ce que vous voulez. Par exemple, s'il écrit ce tableau vide dans un fichier, vous pouvez maintenant avoir transformé votre bogue de "crash parce que j'ai fourni null par accident" à "efface les lignes du calendrier parce que j'ai fourni null par accident". (par exemple, si vous commencez à supprimer des éléments n'apparaissant pas dans la liste de la partie "// bla")

La clé pour moi est Jamais des données corrompues . Permettez-moi de répéter cela; JAMAIS. CORROMPU. LES DONNÉES. Si votre programme fait exception, vous obtenez un rapport de bogue que vous pouvez corriger. si elle écrit de mauvaises données dans un fichier et que ce sera plus tard, vous devez semer le sol avec du sel.

Toutes vos décisions "inutiles" doivent être prises avec cette prémisse.

deworde
la source
2

Ce dont vous avez affaire ici est essentiellement une interface. En ajoutant le comportement "quand l'entrée est null, initialisez l'entrée", vous avez effectivement étendu l'interface de la méthode. Désormais, au lieu de toujours utiliser une liste valide, vous avez réussi à "corriger" l'entrée. Qu'il s'agisse de la partie officielle ou non officielle de l'interface, vous pouvez parier que quelqu'un (y compris très probablement vous) va utiliser ce comportement.

Les interfaces doivent rester simples et relativement stables, notamment avec une public staticméthode. Vous avez un peu de marge de manœuvre dans les méthodes privées, en particulier les méthodes d'instances privées. En étendant implicitement l'interface, vous avez rendu votre code plus complexe en pratique. Maintenant, imaginez que vous ne souhaitiez pas réellement utiliser ce chemin de code - afin de l'éviter. Maintenant, vous avez un peu de code non testé qui prétend faire partie du comportement de la méthode. Et je peux vous dire tout de suite qu’il ya probablement un bogue: quand vous passez une liste, cette liste est mutée par la méthode. Toutefois, si vous ne le faites pas, vous créez une localeliste, et le jeter plus tard. C’est le genre de comportement incohérent qui vous fera pleurer dans une demi-année, alors que vous essayez de traquer un bogue obscur.

En général, la programmation défensive est une chose très utile. Mais les chemins de code pour les contrôles défensifs doivent être testés, comme tout autre code. Dans un cas comme celui-ci, ils compliquent votre code sans raison, et je choisirais plutôt une alternative comme celle-ci:

if (rows == null) throw new ArgumentNullException(nameof(rows));

Vous ne voulez pas d' une entrée où rowssont nuls, et vous voulez rendre l'erreur évidente à tous vos appelants dès que possible .

Vous devez jongler avec de nombreuses valeurs lorsque vous développez un logiciel. Même la robustesse elle-même est une qualité très complexe - par exemple, je ne considérerais pas votre contrôle défensif plus robuste que de lancer une exception. Les exceptions sont assez pratiques pour vous permettre de réessayer dans un endroit sûr: les problèmes de corruption de données sont généralement beaucoup plus difficiles à suivre que de reconnaître un problème à un stade précoce et de le gérer en toute sécurité. En fin de compte, ils ont tendance à vous donner une illusion de robustesse, puis un mois plus tard, vous remarquez qu'un dixième de vos rendez-vous ont disparu, car vous n'avez jamais remarqué qu'une liste différente avait été mise à jour. Aie.

Assurez-vous de faire la distinction entre les deux. La programmation défensive est une technique utile pour détecter les erreurs à un endroit où elles sont les plus pertinentes, ce qui contribue considérablement à vos efforts de débogage et permet une bonne gestion des exceptions, empêchant ainsi une "corruption sournoise". Échouer tôt, échouer vite. D'autre part, ce que vous faites ressemble plus à "cacher une erreur" - vous jonglez avec les entrées et faites des suppositions sur ce que l'appelant voulait dire. Ceci est très important pour le code destiné à l'utilisateur (par exemple, la vérification orthographique), mais vous devez être prudent lorsque vous le voyez avec du code destiné aux développeurs.

Le problème principal est que quelle que soit votre abstraction, elle va couler ("Je voulais taper ofrerere, pas avant! Stupid correcteur orthographique!"), Et le code pour gérer tous les cas spéciaux et les corrections est toujours le code que vous besoin de maintenir et de comprendre, et le code que vous devez tester. Comparez les efforts pour vous assurer qu'une liste non NULL est passée et pour corriger le bogue que vous avez en production un an plus tard - ce n'est pas un bon compromis. Dans un monde idéal, vous voudriez que chaque méthode fonctionne exclusivement avec sa propre entrée, renvoyant un résultat et ne modifiant aucun état global. Bien sûr, dans le monde réel, vous trouverez de nombreux cas où ce n'est pasla solution la plus simple et la plus claire (par exemple lors de la sauvegarde d’un fichier), mais j’estime que le fait de garder les méthodes "pures" quand elles n’ont aucune raison de lire ou de manipuler un état global rend le code beaucoup plus facile à raisonner. Cela tend également à vous donner des points plus naturels pour séparer vos méthodes :)

Cela ne signifie pas que tout ce qui est inattendu doit faire planter votre application, bien au contraire. Si vous utilisez bien les exceptions, elles constituent naturellement des points de traitement des erreurs sécurisés, dans lesquels vous pouvez restaurer un état d'application stable et permettre à l'utilisateur de continuer ce qu'il est en train de faire (idéalement tout en évitant toute perte de données pour l'utilisateur). À ces points de traitement, vous verrez des possibilités de résoudre les problèmes ("Aucun numéro de commande 2212 trouvé. Voulez-vous dire 2212b?") Ou de donner le contrôle à l'utilisateur ("Erreur de connexion à la base de données. Essayez à nouveau?"). Même si aucune option de ce type n'est disponible, au moins, cela vous donnera une chance que rien ne soit corrompu - j'ai commencé à apprécier le code qui utilise usinget try... finallybeaucoup plus que try...catch, cela vous donne beaucoup d’occasions de maintenir des invariants même dans des conditions exceptionnelles.

Les utilisateurs ne doivent pas perdre leurs données et travailler. Cela doit encore être équilibré avec les coûts de développement, etc., mais c'est une très bonne directive générale (si les utilisateurs décident d'acheter ou non votre logiciel, les logiciels internes n'ont généralement pas ce luxe). Même toute l'application qui tombe en panne devient beaucoup moins problématique si l'utilisateur peut simplement redémarrer et revenir à ce qu'il était en train de faire. C’est une véritable robustesse - Word enregistre votre travail tout le temps sans corrompre votre document sur le disque et vous offre une optionrestaurer ces modifications après le redémarrage de Word après un blocage. Est-ce mieux qu'un no bug en premier lieu? Probablement pas - mais n'oubliez pas que le travail passé à attraper un bug rare pourrait être mieux dépensé partout. Mais c'est beaucoup mieux que les alternatives - par exemple, un document corrompu sur le disque, tout le travail depuis la dernière sauvegarde perdue, le document remplacé automatiquement par des modifications avant le crash qui se trouvaient juste être Ctrl + A et Supprimer.

Luaan
la source
1

Je vais répondre à cette question en partant de votre hypothèse selon laquelle un code robuste vous sera bénéfique à partir de maintenant. Si votre objectif est d'obtenir des avantages à long terme, je privilégierais la conception et la maintenabilité à la robustesse.

Le compromis entre conception et robustesse, c'est du temps et de la concentration. La plupart des développeurs préfèrent disposer d'un ensemble de code bien conçu, même si cela implique de passer par certains points problématiques et d'effectuer certaines conditions supplémentaires ou de gérer les erreurs. Après quelques années d'utilisation, les utilisateurs ont probablement identifié les lieux qui vous intéressent vraiment.

En supposant que la conception soit à peu près de la même qualité, moins de code est facile à gérer. Cela ne signifie pas que nous ferions mieux de laisser les problèmes connus disparaître pendant quelques années, mais l'ajout d'éléments dont vous savez que vous n'avez pas besoin complique les choses. Nous avons tous examiné le code existant et trouvé des pièces inutiles. Vous devez avoir un code de changement de niveau de confiance élevé qui fonctionne depuis des années.

Donc, si vous pensez que votre application est conçue aussi bien que possible, qu’elle est facile à gérer et qu’elle n’a pas de bugs, trouvez quelque chose de mieux à faire que d’ajouter du code dont vous n’avez pas besoin. C'est le moins que vous puissiez faire par respect pour tous les autres développeurs qui travaillent de longues heures sur des fonctionnalités inutiles.

JeffO
la source
1

Non, vous ne devriez pas. Et vous répondez à votre propre question lorsque vous déclarez que cette méthode de codage peut masquer des bogues . Cela ne rendra pas le code plus robuste, mais le rendra plus vulnérable aux bogues et rendra le débogage plus difficile.

Vous indiquez vos attentes actuelles concernant l' rowsargument: Soit il est nul, soit il contient au moins un élément. La question est donc la suivante: est-ce une bonne idée d'écrire le code pour gérer en plus le troisième cas inattendu dans lequel il n'y rowsa aucun élément?

La réponse est non. Vous devriez toujours lever une exception en cas d'entrée inattendue. Considérez ceci: Si certaines autres parties du code annulent l’attente (c’est-à-dire le contrat) de votre méthode, cela signifie qu’il ya un bogue . S'il y a un bogue, vous voulez le savoir le plus tôt possible pour pouvoir le corriger, et une exception vous aidera à le faire.

Ce que le code fait actuellement, c'est de deviner comment récupérer un bogue qui peut ou non exister dans le code. Mais même s'il y a un bogue, vous ne pouvez pas savoir comment le récupérer complètement. Les bogues par définition ont des conséquences inconnues. Certains codes d’initialisation n’ont peut-être pas fonctionné comme prévu. Cela pourrait avoir d’autres conséquences que la seule ligne manquante.

Donc, votre code devrait ressembler à ceci:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

Remarque: Il existe des cas spécifiques dans lesquels il est logique de "deviner" comment gérer une entrée non valide plutôt que de simplement lancer une exception. Par exemple, si vous gérez des entrées externes, vous n’avez aucun contrôle. Les navigateurs Web sont un exemple infâme, car ils essaient de gérer avec élégance tout type d’entrée mal formée et non valide. Mais cela n'a de sens qu'avec une entrée externe, pas avec des appels provenant d'autres parties du programme.


Edit: Certaines autres réponses indiquent que vous effectuez une programmation défensive . Je ne suis pas d'accord. La programmation défensive signifie que vous ne faites pas automatiquement confiance à une entrée valide. Ainsi, la validation des paramètres (comme ci-dessus) est une technique de programmation défensive, mais cela ne signifie pas que vous devez modifier les entrées inattendues ou non valides en les devinant. L'approche défensive robuste est de valider l' entrée et ensuite lancer une exception en cas d'entrée incorrecte ou inattendue.

JacquesB
la source
1

Dois-je ajouter du code redondant maintenant juste au cas où il serait nécessaire dans le futur?

Vous ne devez pas ajouter de code redondant à tout moment.

Vous ne devez pas ajouter de code nécessaire uniquement dans le futur.

Vous devez vous assurer que votre code se comporte bien, peu importe ce qui se passe.

La définition de "bien se comporter" est à la hauteur de vos besoins. Une technique que j'aime bien utiliser est la "paranoïa". Si je suis sûr à 100% qu’un certain cas ne peut jamais se produire, je programme toujours une exception, mais je le fais d’une manière qui: a) indique clairement à tout le monde que je ne prévois jamais que cela se produise, et b) est clairement affiché et consigné et ne conduit donc pas à une corruption rampante plus tard.

Exemple de pseudo-code:

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

Cela indique clairement que je suis à 100% sûr de pouvoir toujours ouvrir, écrire ou fermer le fichier, c'est-à-dire que je ne vais pas jusqu'à créer un traitement complexe des erreurs. Mais si (non: quand) je ne peux pas ouvrir le fichier, mon programme échouera quand même de manière contrôlée.

Bien entendu, l’interface utilisateur n’affiche pas de tels messages à l’utilisateur, ils sont consignés en interne avec la trace de la pile. Encore une fois, ce sont des exceptions internes "Paranoia" qui s'assurent juste que le code "s'arrête" quand quelque chose d'inattendu se produit. Cet exemple est un peu caché. En pratique, j’impliquerais bien sûr le traitement des erreurs lors de l’ouverture des fichiers, car cela se produit régulièrement (nom de fichier incorrect, clé USB montée en lecture seule, peu importe).

Un terme de recherche connexe très important serait "échec rapide", comme indiqué dans d'autres réponses, et est très utile pour créer un logiciel robuste.

AnoE
la source
-1

Il y a beaucoup de réponses trop compliquées ici. Vous avez probablement posé cette question parce que vous ne vous sentiez pas bien à propos de ce code, mais vous ne saviez pas pourquoi ni comment le corriger. Donc, ma réponse est que le problème est très probable dans la structure du code (comme toujours).

Tout d'abord, l'en-tête de la méthode:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)

Attribuer rendez-vous à quelle rangée? Cela devrait être clair immédiatement de la liste des paramètres. Sans aucune connaissance plus loin, je vous attendre à regarder la méthode params comme ceci: (Appointment app, CalendarRow row).

Ensuite, les "contrôles d'entrée":

//1. Is rows equal to null? - This will be the case if this is the first appointment.
if (rows == null) {
    rows = new List<CalendarRow> ();
}

//2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
if(rows.Count == 0)
{
    rows.Add (new CalendarRow (0));
    rows [0].Appointments.Add (app);
}

C'est des conneries.

  1. check) L'appelant de la méthode doit simplement s'assurer qu'il ne transmet pas de valeurs non initialisées à l'intérieur de la méthode. C'est la responsabilité du programmeur (d'essayer) de ne pas être stupide.
  2. check) Si je ne tiens pas compte du fait que passer rowsà la méthode est probablement une erreur (voir le commentaire ci-dessus), il ne devrait pas être de la responsabilité de la méthode appelée AssignAppointmentToRowde manipuler des lignes autrement que par l'attribution du rendez-vous quelque part.

Mais l'idée d'assigner des rendez-vous quelque part est étrange (à moins qu'il s'agisse d'une partie graphique du code). Votre code semble contenir (ou du moins tente de le faire) une structure de données explicite représentant un calendrier ( List<CalendarRows><- qui doit être défini comme Calendarquelque part si vous voulez utiliser cette méthode, vous passez alors Calendar calendarà votre méthode). Si vous vous en tenez ainsi, je m'attendrais à ce que le calendarsoit pré-rempli avec des créneaux où vous placez (affectez) les rendez-vous après (par exemple calendar[month][day] = appointmentserait le code approprié). Mais vous pouvez également choisir d’abandonner la structure du calendrier de la logique principale et d’avoir juste List<Appointment>où les Appointmentobjets contiennent des attributs.date. Et ensuite, si vous devez rendre un calendrier quelque part dans l'interface graphique, vous pouvez créer cette structure de "calendrier explicite" juste avant le rendu.

Comme je ne connais pas les détails de votre application, certaines de ces informations ne vous concernent peut-être pas, mais ces deux vérifications (principalement la deuxième) me disent qu'il y a quelque chose qui cloche avec la séparation des problèmes dans votre code.

climat
la source
-2

Pour des raisons de simplicité, supposons que vous aurez éventuellement besoin de ce morceau de code dans N jours (pas plus tard ou plus tôt), soit que vous n’avez pas du tout besoin de le faire.

Pseudocode:

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.

Facteurs pour C_maint:

  • Améliore-t-il le code en général, le rendant plus auto-documenté, plus facile à tester? Si oui, négatif C_maintattendu
  • Rend-il le code plus gros (donc plus difficile à lire, plus long à compiler, à implémenter des tests, etc.)?
  • Y a-t-il des refactorings / remaniements en attente? Si oui, bosse C_maint. Ce cas nécessite une formule plus complexe, avec plus de fois des variables telles que N.

Il faut donc laisser de côté une chose tellement importante qui alourdit le code et pourrait ne devenir nécessaire que dans deux ans avec une probabilité faible, mais elle devrait également être mise en œuvre .

Vi0
la source
Vous devez également prendre en compte le coût des bogues pouvant s'infiltrer dans le programme car vous ne rejetez pas les entrées non valides. Alors, comment estimez-vous le coût des bogues difficiles à trouver?
JacquesB