web-dev-qa-db-fra.com

Dois-je ajouter du code redondant maintenant juste au cas où il pourrait être nécessaire à l'avenir?

À tort ou à raison, je suis actuellement d'avis que je devrais toujours essayer de rendre mon code aussi robuste que possible, même si cela signifie ajouter du code redondant/vérifier que je sais ne sera d'aucune utilité pour le moment, mais ils pourraient être x nombre d'années plus tard.

Par exemple, je travaille actuellement sur une application mobile qui a 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 regardant spécifiquement la section deux, je sais que si la section un est vraie, la section deux sera également vraie. Je ne peux penser à aucune raison expliquant pourquoi la section un serait fausse et la section deux évaluerait vrai, ce qui rend la deuxième instruction if redondante.

Cependant, il peut y avoir un cas dans le futur où cette deuxième instruction if est réellement nécessaire, et pour une raison connue.

Certaines personnes peuvent regarder cela au début et penser que je programme en pensant à l'avenir, ce qui est évidemment une bonne chose. Mais je connais quelques cas où ce type de code a des bogues "cachés". Ce qui signifie qu'il m'a fallu encore plus de temps pour comprendre pourquoi la fonction xyz fait abc alors qu'elle devrait réellement faire def.

D'un autre côté, il y a également eu de nombreux cas où ce type de code a rendu beaucoup plus facile d'améliorer le code avec de nouveaux comportements, car je n'ai pas besoin de revenir en arrière et de m'assurer que toutes les vérifications pertinentes sont en place.

Existe-t-il des règles générales règle générale pour ce type de code? (J'aimerais également savoir si cela serait considéré comme une bonne ou une mauvaise pratique?)

N.B: Cela pourrait être considéré comme similaire à cette question, mais contrairement à cette question, je voudrais une réponse en supposant qu'il n'y a pas de délais.

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

116
KidCode

Comme exercice, vérifions d'abord votre logique. Bien que, comme nous le verrons, vous rencontrez des problèmes plus importants que n'importe quel problème logique.

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

Vous dites d'abord:

En regardant spécifiquement la section deux, je sais que si la section un est vraie, la section deux sera également vraie.

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

Et alors:

Je ne peux penser à aucune raison pour laquelle la section un serait fausse et la section deux est vraie, ce qui rend la deuxième instruction if redondante.

C'est-à-dire: NOT((NOT A) AND B). Appliquez la loi de Demorgan pour obtenir (NOT B) OR A Qui est B implique A.

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

Donc oui, les contrôles 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 sujet redoutable. 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, donc il doit être résistant aux mauvaises données des appelants arbitraires.

Il renvoie 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 son effet secondaire.

Le contrat du paramètre de liste est:

  • Une liste nulle est OK
  • Une liste contenant un ou plusieurs éléments est OK
  • Une liste ne contenant aucun élément 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 machine à bonbons écrite partout. (L'expression provient d'une vieille histoire sur les machines à bonbons chez Microsoft, où les prix et les sélections sont des nombres à deux chiffres, et il est super facile de taper "85" qui est le prix de l'article 75, et vous obtenez le mauvais article. Fait amusant: oui, je l'ai fait par erreur lorsque j'essayais de retirer la gomme d'un distributeur automatique chez Microsoft!)

Voici comment concevoir un contrat judicieux:

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

N'acceptez pas les types mutables comme entrées, comme les listes; si vous avez besoin d'une séquence d'informations, prenez un IEnumerable. Ne lisez que la séquence; n'écrivez pas dans une collection transmise sauf s'il est très clair que c'est le contrat de la méthode. En prenant IEnumerable, vous envoyez un message à l'appelant que vous n'allez pas muter sa collection.

N'acceptez jamais de null; une séquence nulle est une abomination. Exiger que l'appelant passe une séquence vide si cela a du sens, jamais jamais nul.

Crashez immédiatement si l'appelant viole votre contrat, pour leur enseigner que vous pensez aux affaires et pour qu'ils détectent leurs bogues lors des tests, pas de la production.

Concevez d'abord le contrat pour qu'il soit le plus sensé possible, puis mettez-le clairement en œuvre. That est le moyen de pérenniser une conception.

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, en tant que développeur, pouvez déduire 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, viole 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 découvert, soit vous avez un test manquant, soit vous avez un code mort. Le code mort est étonnamment dangereux car il n'est généralement pas destiné à être mort! La faille de sécurité incroyablement Apple "goto fail" il y a quelques années vient immédiatement à l'esprit.

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

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. Faire ces choses rendra l’avenir beaucoup plus facile; la partie la plus difficile de l'avenir est de gérer tout le code de machine à bonbons buggy que les gens ont écrit dans le passé. Faites-le bien aujourd'hui et les coûts seront plus bas à l'avenir.

176
Eric Lippert

Ce que vous faites dans le code que vous montrez ci-dessus, ce n'est pas tant le futur que le codage défensif.

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

La section 1 teste et corrige un objet null. Note latérale: La création de la liste ne crée aucun élément enfant (par exemple, CalendarRow).

La section 2 teste et corrige les erreurs d'utilisateur et/ou d'implémentation. Tout simplement parce que vous avez un List<CalendarRow> ne signifie pas que vous avez des éléments dans la liste. 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 vous semble logique ou non.

89
Adam Zuckerman

Je suppose que cette question est essentiellement au goût. Oui, c'est une bonne idée d'écrire du code robuste, mais le code dans votre exemple est une légère violation du principe KISS (comme beaucoup de code "à l'épreuve du temps")).

Personnellement, je ne prendrais 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 "futur pare-balles" est voué à l'échec lamentable quand l'avenir arrivera de toute façon.

Au lieu de cela, je préférerais une approche différente: faites les hypothèses que vous expliquez en utilisant la macro assert() ou des installations similaires. De cette façon, lorsque l'avenir se présentera, il vous dira précisément où vos hypothèses ne tiennent plus.

Un autre principe auquel vous voudrez peut-être réfléchir est l'idée de échec rapide. L'idée est que lorsque quelque chose ne va pas dans votre programme, vous voulez arrêter le programme immédiatement, au moins pendant que vous le développez avant de le publier. Sous ce principe, vous voulez écrire beaucoup de vérifications pour vous assurer que vos hypothèses tiennent, mais envisagez sérieusement de faire en sorte que votre programme s'arrête net chaque fois que les hypothèses sont violées.

Pour le dire hardiment, s'il y a même une petite erreur dans votre programme, vous voulez qu'il plante complètement pendant que vous regardez!

Cela peut sembler contre-intuitif, mais cela vous permet de découvrir les bogues aussi rapidement que possible pendant le développement de routine. Si vous écrivez un morceau de code et que vous pensez qu'il est terminé, mais qu'il se bloque lorsque vous le testez, il ne fait aucun doute que vous n'avez pas encore terminé. De plus, 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 plantez 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 qui a échoué et le chemin à travers votre code que le programme a emprunté. son chemin vers cette ligne de code (la trace de la pile).

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


Ceci est pertinent pour vous car il est possible que parfois, les chèques que vous écrivez soient là parce que vous voulez que votre programme essaie de continuer à fonctionner même après un problème. Par exemple, considérons cette brève mise en œuvre 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 transmet 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 appelez accidentellement plus tard votre fonction Fibonacci avec une entrée négative, vous ne vous en rendrez jamais compte! Pire encore, votre programme continuera probablement de fonctionner mais commencera à produire des résultats absurdes sans vous donner la moindre indication quant à un problème. Ce sont les types de bogues les plus difficiles à corriger.

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 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 fera savoir quelle partie de votre programme a essayé d'exécuter la fonction Fibonacci de manière incorrecte, vous donnant un excellent point de départ pour déboguer ce qui ne va pas.

23
Kevin

Devriez-vous ajouter du code redondant? Non.

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

Ce que vous décrivez est une programmation défensive contre le code appelant violant 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 eux-mêmes ces violations, cela est entièrement subjectif.

Personnellement, je suis un grand partisan de cette méthodologie, mais comme pour tout, il faut être prudent. Prenons, par exemple, le std::vector::operator[] De C++. Mis à part l'implémentation du mode débogage de VS pendant un moment, cette fonction n'effectue pas de vérification des limites. Si vous demandez un élément qui n'existe pas, le résultat n'est pas défini. Il appartient à l'utilisateur de fournir un index vectoriel valide. C'est tout à fait délibéré: vous pouvez "vous inscrire" à la vérification des limites en l'ajoutant sur le site d'appel, mais si l'implémentation operator[] Devait l'exécuter, vous ne seriez pas en mesure de "vous retirer". En tant que fonction de niveau assez bas, cela a du sens.

Mais si vous écriviez une fonction AddEmployee(string name) pour une interface de niveau supérieur, je m'attends à ce que cette fonction lève au moins une exception si vous fournissez un name vide, ainsi que ceci précondition étant documentée juste au-dessus de la déclaration de fonction. Il se peut que vous ne fournissiez pas d'entrée utilisateur non asservie à cette fonction aujourd'hui, mais en la rendant "sûre" de cette manière, toute violation des conditions préalables qui apparaîtra à l'avenir peut être facilement diagnostiquée, plutôt que de déclencher potentiellement une chaîne de dominos de détecter les bugs. Ce n'est pas de la redondance: c'est de la diligence.

Si je devais trouver une règle générale (même si, en règle générale, j'essaye de les éviter), je dirais qu'une fonction qui satisfait à l'une des conditions suivantes:

  • vit dans un langage de très haut niveau (disons, JavaScript plutôt que C)
  • se trouve à une frontière d'interface
  • n'est pas critique en termes de performances
  • accepte directement les entrées 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ées dans les versions, pour augmenter encore votre capacité à trouver des bogues.

Ce sujet est exploré plus en détail sur Wikipedia ( https://en.wikipedia.org/wiki/Defensive_programming ).

Deux des dix commandements de programmation sont pertinents ici:

  • Tu ne dois pas supposer que la saisie est correcte

  • Tu ne dois pas faire de code pour une utilisation future

Ici, vérifier null n'est pas "créer du code pour une utilisation future". Faire du code pour une utilisation future, c'est comme ajouter des interfaces parce que vous pensez qu'elles pourraient être utiles "un jour". En d'autres termes, le commandement consiste à ne pas ajouter de couches d'abstraction à moins qu'elles ne soient nécessaires pour le moment.

La vérification de null n'a rien à voir avec une utilisation future. Cela a à voir avec le commandement n ° 1: ne présumez pas que l'entrée sera correcte. Ne présumez jamais que votre fonction recevra un sous-ensemble d'entrées. Une fonction doit répondre de manière logique, quelle que soit la fausseté et la confusion des entrées.

9
Tyler Durden

La définition de "code redondant" et de "YAGNI" dépend souvent de ce que vous voyez dans l’avenir.

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

Ma suggestion est de garder une trace du temps que vous passez sur des `` trucs qui ne se sont pas encore trompés '' si ses charges et vous pairs suppriment des fonctionnalités plus rapidement que vous, puis diminuez-les.

Cependant, si vous êtes comme moi, je suppose que vous venez de tout taper "par défaut" et cela ne vous a pas vraiment pris plus de temps.

7
Ewan

C'est une bonne idée de documenter toutes les hypothèses sur les paramètres. Et c'est une bonne idée de vérifier que votre code client ne viole pas ces hypothèses. Je ferais 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'agit 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 avenir 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? Il vaut mieux faire moins d'hypothèses maintenant sur l'avenir.

6
Theodore Norvell

Estimez le coût de l'ajout de ce code maintenant. Ce sera relativement bon marché car tout est frais dans votre esprit, vous pourrez donc le faire rapidement. L'ajout de tests unitaires est nécessaire - il n'y a rien de pire que d'utiliser une méthode un an plus tard, cela ne fonctionne pas, et vous vous rendez compte qu'il a été rompu dès le début et n'a jamais vraiment fonctionné.

Estimez le coût de l'ajout de ce code lorsque cela est nécessaire. Ce sera plus cher, car vous devez revenir au code, vous en souvenir, et c'est beaucoup plus difficile.

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

D'un autre côté, le code plein d'hypothèses "X ne se produira jamais" est horrible 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 bug, il est suspect. Ce qui oblige le prochain développeur à y perdre du temps. Il est généralement préférable de ne pas se fier à de telles hypothèses.

5
gnasher729

La question principale 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, vous maintenez le programme. Mais le programme ne fait peut-être plus ce que vous voulez qu'il fasse. Par exemple, s'il écrit ce tableau vide dans un fichier, vous avez peut-être désormais transformé votre bogue de "plante parce que j'ai fourni null par accident" en "efface les lignes du calendrier parce que j'ai fourni null par accident". (par exemple, si vous commencez à supprimer des éléments qui n'apparaissent pas dans la liste de la partie qui se lit "// blah")

La clé pour moi est Ne jamais corrompre les données. Permettez-moi de répéter cela; JAMAIS. DONNÉES CORROMPUES. Si votre programme fait exception, vous obtenez un rapport de bogue que vous pouvez corriger; s'il écrit de mauvaises données dans un fichier qu'il va plus tard, vous devez semer le sol avec du sel.

Toutes vos décisions "inutiles" doivent être prises en tenant compte de cette prémisse.

3
deworde

Il s'agit essentiellement d'une interface. En ajoutant le comportement "lorsque l'entrée est null, initialisez l'entrée", vous avez effectivement étendu l'interface de méthode - maintenant au lieu de toujours opérer sur une liste valide, vous avez fait en sorte de "corriger" l'entrée. Que ce soit la partie officielle ou non officielle de l'interface, vous pouvez parier que quelqu'un (y compris probablement vous) va utiliser ce comportement.

Les interfaces doivent rester simples et doivent être relativement stables, en particulier dans quelque chose comme un public static méthode. Vous obtenez un peu de latitude dans les méthodes privées, en particulier les méthodes d'instance privées. En étendant implicitement l'interface, vous avez rendu votre code plus complexe dans la pratique. Maintenant, imaginez que vous ne voulez pas réellement utiliser ce chemin de code - donc vous l'évitez. Vous avez maintenant un peu de code non testé qui prétend comme si cela faisait partie du comportement de la méthode. Et je peux vous dire tout de suite qu'il a probablement un bug: lorsque vous passez une liste, cette liste est mutée par la méthode. Cependant, si vous ne le faites pas, vous créez une liste locale et la jetez plus tard. C'est le genre de comportement incohérent qui vous fera pleurer dans six mois, alors que vous essayez de retrouver un bug obscur.

En général, la programmation défensive est une chose assez 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 j'opterais plutôt pour une alternative comme celle-ci:

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

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

Vous devez jongler avec de nombreuses valeurs lors du développement de logiciels. 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 comme plus robuste que de lever une exception. Les exceptions sont assez pratiques pour vous donner un endroit sûr pour réessayer à partir d'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 tôt et de le gérer en toute sécurité. En fin de compte, ils ont seulement 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 a été mise à jour. Aie.

Assurez-vous de faire la distinction entre les deux. Programmation défensive est une technique utile pour détecter les erreurs à un endroit où elles sont les plus pertinentes, aidant considérablement vos efforts de débogage et, avec une bonne gestion des exceptions, empêchant la "corruption sournoise". Échec tôt, échec rapide. D'un autre côté, ce que vous faites ressemble plus à une "dissimulation d'erreur" - vous jonglez avec des entrées et faites des hypothèses sur ce que l'appelant voulait dire. C'est très important pour le code accessible aux utilisateurs (par exemple, la vérification orthographique), mais vous devez vous méfier lorsque vous voyez cela avec du code destiné aux développeurs.

Le problème principal est que quelle que soit l'abstraction que vous faites, elle va fuir ("Je voulais taper ici, pas avant! Vérificateur orthographique stupide!"), 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 l'effort de s'assurer qu'une liste non nulle est passée avec la correction du bug que vous avez un an plus tard en production - 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 pas la solution la plus simple et la plus claire (par exemple lors de l'enregistrement d'un fichier), mais Je trouve que garder les méthodes "pures" quand elles n'ont aucune raison de lire ou de manipuler l'état global rend le code beaucoup plus facile à raisonner. Cela a également tendance à vous donner des points plus naturels pour diviser vos méthodes :)

Cela ne signifie pas que tout ce qui est inattendu devrait faire planter votre application, au contraire. Si vous utilisez bien les exceptions, elles forment naturellement des points de gestion des erreurs sûrs, où vous pouvez restaurer un état d'application stable et permettre à l'utilisateur de continuer ce qu'il fait (idéalement tout en évitant toute perte de données pour l'utilisateur). À ces points de traitement, vous verrez des opportunités pour résoudre les problèmes ("Aucun numéro de commande 2212 trouvé. Vouliez-vous dire 2212b?") Ou pour donner le contrôle à l'utilisateur ("Erreur de connexion à la base de données. Réessayez?"). Même lorsqu'aucune telle option n'est disponible, au moins cela vous donnera une chance que rien ne soit corrompu - j'ai commencé à apprécier le code qui utilise using et try...finally beaucoup plus que try...catch, cela vous donne beaucoup d'opportunités de maintenir des invariants même dans des conditions exceptionnelles.

Les utilisateurs ne devraient pas perdre leurs données et leur travail. Cela doit encore être équilibré avec les coûts de développement, etc., mais c'est une assez 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 se bloquant devient beaucoup moins problématique si l'utilisateur peut simplement redémarrer et revenir à ce qu'il faisait. C'est une vraie robustesse - Word enregistre votre travail tout le temps sans corrompre votre document sur le disque et vous donne un option pour restaurer ces modifications après le redémarrage de Word après un plantage. 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 a été perdu, le document a été remplacé automatiquement par des modifications avant le crash, qui se trouvaient être Ctrl + A et Supprimer.

2
Luaan

Dois-je ajouter du code redondant maintenant juste au cas où il pourrait être nécessaire à l'avenir?

Vous ne devez à aucun moment ajouter de code redondant.

Vous ne devez pas ajouter de code qui n'est nécessaire qu'à l'avenir.

Vous devez vous assurer que votre code se comporte bien, quoi qu'il arrive.

La définition de "bien se comporter" dépend de vos besoins. Une technique que j'aime utiliser, ce sont les exceptions de "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 m'attends jamais à ce que cela se produise, et b) est clairement affiché et enregistré et n'entraîne donc pas plus tard une corruption rampante.

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 sûr à 100% que je peux toujours ouvrir, écrire ou fermer le fichier, c'est-à-dire que je ne vais pas jusqu'à créer une gestion des erreurs élaborée. Mais si (non: quand) je ne peux pas ouvrir le fichier, mon programme échouera toujours de manière contrôlée.

L'interface utilisateur n'affichera bien sûr pas ces messages à l'utilisateur, ils seront enregistrés en interne avec la trace de la pile. Encore une fois, il s'agit d'exceptions internes de "paranoïa" qui garantissent simplement que le code "s'arrête" lorsqu'un événement inattendu se produit. Cet exemple est un peu récupéré, dans la pratique, j'implémenterais bien sûr une véritable gestion 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 "échouer rapidement", comme indiqué dans d'autres réponses et est très utile pour créer un logiciel robuste.

1
AnoE

Non vous ne devriez pas. Et vous répondez en fait à votre propre question lorsque vous déclarez que cette façon de coder peut cacher les bugs. Cela ne rendra pas le code plus robuste - il le rendra plus sujet aux bogues et rendra le débogage plus difficile.

Vous indiquez vos attentes actuelles concernant l'argument rows: soit il est nul, soit il contient au moins un élément. La question est donc: est-ce une bonne idée d'écrire le code pour gérer en outre le troisième cas inattendu où rows n'a aucun élément?

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

Ce que le code fait actuellement, c'est de deviner comment récupérer d'un bogue qui peut ou non exister dans le code. Mais même s'il y a un bug, vous ne savez pas comment vous en remettre complètement. Par définition, les bogues ont des conséquences inconnues. Peut-être qu'un code d'initialisation ne s'est pas exécuté comme prévu, cela peut avoir de nombreuses autres conséquences que la seule ligne manquante.

Votre code devrait donc 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 certains cas spécifiques où il est logique de "deviner" comment gérer les entrées invalides plutôt que de simplement lancer une exception. Par exemple, si vous gérez une entrée externe, vous n'avez aucun contrôle sur. Les navigateurs Web sont un exemple tristement célèbre, car ils essaient de gérer gracieusement tout type d'entrée malformée et invalide. 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 faites programmation défensive. Je ne suis pas d'accord. La programmation défensive signifie que vous ne faites pas automatiquement confiance à l'entrée pour être valide. La validation des paramètres (comme ci-dessus) est donc une technique de programmation défensive, mais cela ne signifie pas que vous devez modifier les entrées inattendues ou invalides en devinant. L'approche défensive robuste consiste à valider l'entrée et puis à lever une exception en cas d'entrée inattendue ou invalide.

1
JacquesB

Je vais y répondre en partant de votre hypothèse qu'un code robuste vous bénéficiera "des années" à partir de maintenant. Si les avantages à long terme sont votre objectif, je privilégierais la conception et la maintenabilité à la robustesse.

Le compromis entre design et robustesse, c'est le temps et la concentration. La plupart des développeurs préfèrent avoir un ensemble de code bien conçu, même si cela signifie passer par des points chauds et faire des conditions supplémentaires ou gérer les erreurs. Après quelques années d'utilisation, les endroits dont vous en avez vraiment besoin ont probablement été identifiés par les utilisateurs.

En supposant que la conception est à peu près de la même qualité, moins de code est plus facile à maintenir. Cela ne signifie pas que nous serions mieux si vous laissiez les problèmes connus disparaître pendant quelques années, mais l'ajout de choses dont vous savez que vous n'avez pas besoin rend la tâche difficile. Nous avons tous examiné le code hérité et trouvé des pièces inutiles. Vous devez avoir un code de changement de confiance de haut niveau qui fonctionne depuis des années.

Donc, si vous pensez que votre application est conçue aussi bien que possible, est facile à entretenir et ne contient pas de bogues, 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 travaillant de longues heures sur des fonctionnalités inutiles.

1
JeffO