Est-ce un mauvais style de vérifier une condition de manière redondante?

10

J'arrive souvent à des positions dans mon code où je me retrouve à vérifier une condition spécifique encore et encore.

Je veux vous donner un petit exemple: supposons qu'il existe un fichier texte qui contient des lignes commençant par "a", des lignes commençant par "b" et d'autres lignes et je ne souhaite en fait travailler qu'avec les deux premières sortes de lignes. Mon code ressemblerait à quelque chose comme ça (en utilisant python, mais le lire comme pseudocode):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a")):
        # do stuff
    elif (line.startsWith("b")):
        # magic
    else:
        # this else is redundant, I already made sure there is no else-case
        # by using clear_lines()
# ...

Vous pouvez imaginer que je ne vérifierai pas seulement cette condition ici, mais peut-être aussi dans d'autres fonctions et ainsi de suite.

Le considérez-vous comme du bruit ou ajoute-t-il de la valeur à mon code?

marktani
la source
5
Il s'agit essentiellement de savoir si vous codez défensivement ou non. Voyez-vous que ce code est beaucoup édité? Est-il probable que cela fasse partie d'un système qui doit être extrêmement fiable? Je ne vois pas grand-chose de mal à faire un assert()essai pour aider, mais au-delà, c'est probablement excessif. Cela dit, cela variera selon la situation.
Latty
votre cas "else" est essentiellement du code mort / inaccessible. Vérifiez qu'il n'y a pas d'exigences à l'échelle du système qui l'interdisent.
NWS
@NWS: dites-vous que je devrais garder le cas else? Désolé, je ne vous comprends pas complètement.
Marktani
2
pas spécialement lié à la question - mais je ferais de cette "assertion" un invariant - qui nécessiterait une nouvelle classe "Line" (peut-être avec des classes dérivées pour A & B), plutôt que de traiter les lignes comme des chaînes et de leur dire quoi ils représentent de l'extérieur. Je serais heureux de développer cela à CodeReview
MattDavey
tu voulais dire elif (line.startsWith("b"))? par ailleurs, vous pouvez supprimer en toute sécurité ces parenthèses environnantes sur les conditions, elles ne sont pas idiomatiques en Python.
Tokland

Réponses:

14

Il s'agit d'une pratique extrêmement courante et la façon de la gérer est d'utiliser des filtres d' ordre supérieur .

Essentiellement, vous transmettez une fonction à la méthode de filtrage, ainsi que la liste / séquence que vous souhaitez filtrer et la liste / séquence résultante contient uniquement les éléments que vous souhaitez.

Je ne connais pas la syntaxe python (cependant, elle contient une telle fonction comme vu dans le lien ci-dessus), mais en c # / f # cela ressemble à ceci:

c #:

var linesWithAB = lines.Where(l => l.StartsWith("a") || l.StartsWith("b"));
foreach (var line in linesWithAB)
{
    /* line is guaranteed to ONLY start with a or b */
}

f # (suppose ienumerable, sinon List.filter serait utilisé):

let linesWithAB = lines
    |> Seq.filter (fun l -> l.StartsWith("a") || l.StartsWith("b"))

for line in linesWithAB do
    /* line is guaranteed to ONLY start with a or b */

Donc, pour être clair: si vous utilisez du code / des modèles éprouvés, c'est un mauvais style. Cela, et la mutation de la liste en mémoire de la façon dont vous semblez via clear_lines () vous fait perdre la sécurité des threads et tout espoir de parallélisme que vous auriez pu avoir.

Steven Evers
la source
3
Comme une note, la syntaxe de Python pour ce serait une expression de générateur: (line for line in lines if line.startswith("a") or line.startswith("b")).
Latty
1
+1 pour avoir souligné que l'implémentation impérative (inutile) de clear_linesest vraiment une mauvaise idée. En Python, vous utiliseriez probablement des générateurs pour éviter de charger le fichier complet en mémoire.
Tokland
Que se passe-t-il lorsque le fichier d'entrée est plus volumineux que la mémoire disponible?
Blrfl
@Blrfl: eh bien, si le terme générateur est cohérent entre c # / f # / python, alors ce que @tokland et @Lattyware traduit en c # / f # yield et / ou yield! déclarations. C'est un peu plus évident dans mon exemple f # car Seq.filter ne peut être appliqué qu'aux collections de IEnumerable <T> mais les deux exemples de code fonctionneront s'il liness'agit d'une collection générée.
Steven Evers
@mcwise: Lorsque vous commencez à regarder toutes les autres fonctions disponibles qui fonctionnent de cette façon, cela commence à devenir vraiment sexy et incroyablement expressif car elles peuvent toutes être enchaînées et composées ensemble. Regardez skip, take, reduce( aggregate.NET), map( select.NET), et il y a plus , mais c'est un début vraiment solide.
Steven Evers
14

J'ai récemment dû implémenter un programmeur de micrologiciel utilisant le format Motorola S-record , très similaire à ce que vous décrivez. Étant donné que nous avions un peu de temps, mon premier projet a ignoré les redondances et a apporté des simplifications en fonction du sous-ensemble que j'avais réellement besoin d'utiliser dans ma demande. Il a réussi mes tests facilement, mais a échoué durement dès que quelqu'un d'autre l'a essayé. Il n'y avait aucune idée du problème. Il a réussi à traverser mais a échoué à la fin.

Je n'ai donc pas eu d'autre choix que de mettre en œuvre tous les contrôles redondants, afin de préciser où était le problème. Après cela, il m'a fallu environ deux secondes pour trouver le problème.

Il m'a fallu peut-être deux heures de plus pour le faire correctement, mais j'ai également perdu une journée de temps pour le dépannage. Il est très rare que quelques cycles de processeur valent une journée de dépannage inutile.

Cela étant dit, en ce qui concerne la lecture des fichiers, il est souvent avantageux de concevoir votre logiciel pour travailler avec sa lecture et son traitement une ligne à la fois, plutôt que de lire l'intégralité du fichier en mémoire et de le traiter en mémoire. De cette façon, il fonctionnera toujours sur de très gros fichiers.

Karl Bielefeldt
la source
"Il est très rare que quelques cycles de processeur valent une journée de dépannage inutile." Merci pour la réponse, vous avez un bon point.
marktani
5

Vous pouvez lever une exception au elsecas où. De cette façon, ce n'est pas redondant. Les exceptions ne sont pas des choses qui ne devraient pas se produire mais qui sont quand même vérifiées.

clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a)):
        # do stuff
    if (line.startsWith("b")):
        # magic
    else:
        throw BadLineException
# ...
Tulains Córdova
la source
Je dirais que ce dernier est une mauvaise idée, car il est moins explicite - si vous décidez plus tard d'ajouter un "c", cela pourrait être moins clair.
Latty
La première suggestion a du mérite ... la seconde (supposez "b") est une mauvaise idée
Andrew
@Lattyware J'ai amélioré la réponse. Merci pour vos commentaires.
Tulains Córdova
1
@Andrew, j'ai amélioré la réponse. Merci pour vos commentaires.
Tulains Córdova
3

En conception par contrat , on devine que chaque fonction doit faire son travail comme décrit dans sa documentation. Ainsi, chaque fonction a une liste de conditions préalables, c'est-à-dire les conditions sur les entrées de la fonction ainsi que les conditions postérieures, c'est-à-dire les conditions de sortie de la fonction.

La fonction doit garantir à ses clients que si les entrées respectent les conditions préalables, la sortie sera telle que décrite par les conditions préalables. Si au moins une des conditions préalables n'est pas respectée, la fonction peut faire ce qu'elle veut (planter, retourner n'importe quel résultat, ...). Les pré et post-conditions sont donc une description sémantique de la fonction.

Grâce au contrat, une fonction est sûre que ses clients l'utilisent correctement et un client est sûr que la fonction fait son travail correctement.

Certaines langues gèrent les contrats de manière native ou via un cadre dédié. Pour les autres, le mieux est de vérifier les pré et post-conditions grâce aux assertions, comme l'a dit @Lattyware. Mais je n'appellerais pas cela une programmation défensive, car dans mon esprit, ce concept est davantage axé sur la protection contre les intrants de l'utilisateur (humain).

Si vous exploitez des contrats, vous pouvez éviter la condition vérifiée de façon redondante car soit la fonction appelée fonctionne parfaitement et vous n'avez pas besoin de la double vérification, soit la fonction appelée est dysfonctionnelle et la fonction appelante peut se comporter comme elle le souhaite.

Le plus difficile est alors de définir quelle fonction est responsable de quoi et de documenter strictement ces rôles.

mgoeminne
la source
1

En fait, vous n'avez pas besoin de clear_lines () au début. Si la ligne n'est ni "a" ni "b", les conditions ne se déclencheront tout simplement pas. Si vous voulez vous débarrasser de ces lignes, transformez le else en clear_line (). En l'état, vous effectuez deux passages dans votre document. Si vous sautez clear_lines () au début et le faites dans le cadre de la boucle foreach, vous réduisez de moitié votre temps de traitement.

Ce n'est pas seulement un mauvais style, c'est un mauvais calcul.

Ingénieur du monde
la source
2
Il se pourrait que ces lignes soient utilisées pour autre chose, et elles doivent être traitées avant de traiter les lignes "a"/ "b". Ne pas dire que c'est probable (le nom clair implique qu'ils sont jetés), juste qu'il y a une possibilité que cela soit nécessaire. Si cet ensemble de lignes est itéré à plusieurs reprises à l'avenir, il pourrait également être utile de les supprimer au préalable pour éviter beaucoup d'itérations inutiles.
Latty
0

Si vous voulez vraiment faire quelque chose si vous trouvez une chaîne non valide (texte de débogage de sortie par exemple), je dirais que c'est tout à fait correct. Quelques lignes supplémentaires et quelques mois plus tard quand il cesse de fonctionner pour une raison inconnue, vous pouvez regarder la sortie pour savoir pourquoi.

Si, cependant, il est prudent de simplement l'ignorer, ou si vous savez avec certitude que vous n'obtiendrez jamais une chaîne non valide, alors il n'est pas nécessaire d'utiliser la branche supplémentaire.

Personnellement, je suis toujours pour mettre au moins une sortie de trace pour toute condition inattendue - cela rend la vie beaucoup plus facile lorsque vous avez un bug avec une sortie attachée vous indiquant exactement ce qui ne va pas.

Bok McDonagh
la source
0

... supposons qu'il existe un fichier texte qui contient des lignes commençant par "a", des lignes commençant par "b" et d'autres lignes et je ne souhaite en fait travailler qu'avec les deux premières sortes de lignes. Mon code ressemblerait à quelque chose comme ça (en utilisant python, mais le lire comme pseudocode):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if ...

Je déteste les if...then...elseconstructions. J'éviterais tout le problème:

process_lines_by_first_character (lines,  
                                  'a' => { |line| ... a code ... },
                                  'b' => { |line| ... b code ... } )
Kevin Cline
la source