C'est un problème mineur, mais chaque fois que je dois coder quelque chose comme cela, la répétition me dérange, mais je ne suis pas sûr qu'aucune des solutions ne soit pire.
if(FileExists(file))
{
contents = OpenFile(file); // <-- prevents inclusion in if
if(SomeTest(contents))
{
DoSomething(contents);
}
else
{
DefaultAction();
}
}
else
{
DefaultAction();
}
- Y a-t-il un nom pour ce genre de logique?
- Suis-je un peu trop TOC?
Je suis ouvert aux suggestions de codes diaboliques, ne serait-ce que par curiosité ...
coding-style
conditions
Benjol
la source
la source
DefaultAction
appels violent le principe DRYmake sure I have valid data for DoSomething(), and then DoSomething() with it. Otherwise, take DefaultAction()
. Les détails indispensables de l’assurance que vous avez les données pour DoSomething () sont à un niveau d’abstraction inférieur et doivent donc occuper une fonction différente. Cette fonction aura un nom dans le niveau d'abstraction le plus élevé et sa mise en œuvre sera de bas niveau. Les bonnes réponses ci-dessous répondent à ce problème.Réponses:
Extrayez-le pour séparer la fonction (méthode) et utilisez la
return
déclaration suivante:Ou peut-être mieux, séparez le contenu et son traitement:
Upd:
Pourquoi pas les exceptions, pourquoi
OpenFile
ne pas lancer l'exception IO:je pense que c'est vraiment une question générique, plutôt qu'une question sur le fichier IO. Des noms tels que
FileExists
,OpenFile
peut être source de confusion, mais si pour les remplacer parFoo
,Bar
, etc., - il serait plus clair que l'onDefaultAction
peut appeler aussi souvent queDoSomething
, de sorte qu'il peut être le cas non exceptionnel. Péter Török a écrit à ce sujet à la fin de sa réponse.Pourquoi il y a un opérateur conditionnel ternaire dans la deuxième variante:
S'il y aurait une balise [C ++], j'aurais écrit une
if
déclaration avec une déclaration decontents
dans sa partie condition:Mais pour les autres langages (de type C),
if(contents) ...; else ...;
est exactement identique à une expression avec un opérateur conditionnel ternaire, mais plus long. Parce que la partie principale du code était uneget_contents
fonction, j'ai juste utilisé la version la plus courte (et lecontents
type omis ). Quoi qu'il en soit, c'est au-delà de cette question.la source
Si le langage de programmation que vous utilisez (0) court-circuite des comparaisons binaires (c'est-à-dire si ne pas appeler
SomeTest
siFileExists
retourne faux) et (1) une affectation retourne une valeur (le résultat deOpenFile
est assigné àcontents
puis cette valeur est passée en argument toSomeTest
), vous pouvez utiliser quelque chose comme ce qui suit, mais il vous serait quand même conseillé de commenter le code en notant que le single=
est intentionnel.En fonction de la complexité du if, il peut être préférable d’avoir une variable flag (qui sépare le test des conditions de réussite / échec avec le code qui traite l’erreur
DefaultAction
dans ce cas).la source
if
déclaration, à mon avis.Plus sérieux que la répétition de l'appel à DefaultAction est le style lui-même car le code est écrit non orthogonal (voir cette réponse pour de bonnes raisons d'écrire orthogonalement).
Pour montrer pourquoi un code non orthogonal est incorrect, considérons l'exemple d'origine, lorsqu'une nouvelle exigence nous oblige à ne pas ouvrir le fichier s'il est stocké sur un disque réseau est introduite. Eh bien, nous pourrions simplement mettre à jour le code comme suit:
Mais il faut aussi ne pas ouvrir de gros fichiers de plus de 2 Go. Eh bien, nous venons de mettre à jour à nouveau:
Il devrait être très clair que ce style de code sera un gros problème de maintenance.
Parmi les réponses écrites correctement orthogonalement figurent le deuxième exemple d'Abyx et la réponse de Jan Hudec . Je ne vais donc pas répéter cela. Soulignons simplement que l'ajout des deux exigences dans ces réponses serait simplement
(ou
goto notexists;
au lieu dereturn null;
), n'affectant aucun autre code que ces lignes ajoutées . Par exemple orthogonal.Lors du test, la règle générale devrait être de tester les exceptions, et non le cas normal .
la source
Évidemment:
Vous avez dit que vous êtes ouvert même aux solutions perverses, alors utiliser le mal compte, non?
En fait, selon le contexte, cette solution pourrait bien être moins diabolique que le mal agissant deux fois ou le mal extra variable. Je l'ai enveloppé dans une fonction, parce que cela ne serait certainement pas correct au milieu d'une longue fonction (notamment en raison du retour au milieu). Mais la fonction longue n'est pas OK, point final.
Lorsque vous aurez des exceptions, elles seront plus faciles à lire, en particulier si vous pouvez avoir une exception OpenFile et DoSomething simplement une exception si les conditions ne sont pas remplies. Vous n'avez donc pas besoin de vérifications explicites. En revanche, en C ++, Java et C #, une exception est une opération lente. Par conséquent, du point de vue de la performance, la lecture est toujours préférable.
Note sur le "mal": La FAQ 6.15 de C ++ définit le "mal" comme suit:
Et cela s'applique à
goto
dans ce contexte. La plupart du temps, les structures de contrôle de flux structurées sont préférables, mais si vous accumulez trop de leurs propres maux, comme une affectation conditionnelle, une imbrication de plus de 3 niveaux de profondeur, une duplication de code ou de longues conditions,goto
peut tout simplement se terminer up être moins diabolique.la source
for(;;) { if(!FileExists(file)) break; contents = OpenFile(file); if(!SomeTest(contents)) break; DoSomething(contents); return; } /* broken out */ DefaultAction();
goto
, car vous abusezbreak
d'une manière que personne ne s'attend à ce qu'elle soit maltraitée. Par conséquent, les lecteurs du code auront plus de difficultés à voir où la rupture les mènera qu'avec goto qui le dit explicitement. De plus, vous utilisez une boucle infinie qui ne fonctionnera qu'une seule fois, ce qui sera assez déroutant. Malheureusement, cedo { ... } while(0)
n’est pas tout à fait lisible non plus, parce que vous voyez que c’est juste un bloc amusant quand vous arrivez à la fin et C ne supporte pas la rupture d’autres blocs (contrairement à perl)....
la source
SomeTest
peut avoir la même sémantique que l'existence de fichier siSomeTest
le type de fichier est vérifié, par exemple, vérifie que .gif est vraiment un fichier GIF.contents && f(contents)
. Deux fonctions pour sauver une autre?!Une possibilité:
Bien entendu, cela rend le code légèrement plus complexe d’une manière différente. C'est donc en grande partie une question de style.
Une approche différente consisterait à utiliser des exceptions, par exemple:
Cela semble plus simple, mais il n’est applicable que si
DefaultAction()
convient à chacunSomeTest()
est clairement une condition erronée; il est donc approprié de lancer une exception.la source
(function () { ... })()
en Javascript,{ flag = false; ... }
en C-like, etc.C'est à un niveau d'abstraction supérieur:
Et cela remplit les détails.
la source
Certaines personnes trouvent cette approche un peu extrême, mais elle est également très propre. Permettez-moi d'illustrer en Python:
Quand il dit que les fonctions doivent faire une chose, il veut dire une chose.
processFile()
choisit une action en fonction du résultat d'un test, et c'est tout ce qu'il fait.fileMeetsTest()
combine toutes les conditions du test, et c'est tout ce qu'il fait.contentsTest()
transfère la première ligne versfirstLineTest()
, et c'est tout ce que cela fait.Cela ressemble à beaucoup de fonctions, mais il se lit pratiquement comme un anglais simplifié:
Certes, c'est un peu verbeux, mais notez que si un responsable ne se soucie pas des détails, il peut arrêter de lire après seulement les 4 lignes de code
processFile()
, et il aura toujours une bonne connaissance de haut niveau du fonctionnement de la fonction.la source
En ce qui concerne son nom, il peut facilement devenir un modèle anti-tête de flèche à mesure que votre code prend en charge de plus en plus d'exigences (comme le montre la réponse fournie à l' adresse https://softwareengineering.stackexchange.com/a/122625/33922 ) et tombe ensuite dans le piège d'avoir d'énormes sections de codes avec des instructions conditionnelles imbriquées qui ressemblent à une flèche.
Voir les liens tels que;
http://codinghorror.com/blog/2006/01/flattening-arrow-code.html
http://lostechies.com/chrismissal/2009/05/27/anti-patterns-and-worst-practices-the-arrowhead-anti-pattern/
Il y a beaucoup plus sur ce sujet et sur d'autres anti-modèles que l'on trouve sur Google.
Quelques bons conseils que Jeff fournit sur son blog à ce sujet sont:
Voir certains commentaires sur le blog de Jeff concernant les suggestions de Steve McConnells sur les premiers retours également;
J'ai toujours souscrit à la théorie 1 entrée / sortie par fonction en raison de ce que l'on m'avait appris il y a une quinzaine d'années. Je pense que cela rend le code tellement plus facile à lire et plus facile à gérer
la source
Ceci est conforme aux règles DRY, no-goto et no-multiple-return, est évolutif et lisible à mon avis:
la source
if
s dans d'autresif
s. En outre, le code permettant de gérer la casse infructueuse (DefaultAction()
) ne se trouve qu’à un seul endroit et, à des fins de débogage, il ne saute pas autour des fonctions d’aide et l’ajout de points d’arrêt aux lignes dans lesquelles lasuccess
variable est modifiée permet de visualiser rapidement les tests passés (au-dessus du test déclenché). point d'arrêt) et ceux qui n'ont pas été testés (ci-dessous).success
àok_so_far
:)Je l'extrais à une méthode séparée et ensuite:
ce qui permet aussi
alors éventuellement, vous pourriez supprimer les
DefaultAction
appels et laisser l'exécutionDefaultAction
pour l'appelant:J'aime aussi l'approche de Jeanne Pindar .
la source
Pour ce cas particulier, la réponse est assez simple ...
Il y a une condition de concurrence entre
FileExists
etOpenFile
: que se passe-t-il si le fichier est supprimé?La seule façon sensée de traiter ce cas particulier est de sauter
FileExists
:Cela résout parfaitement ce problème et rend le code plus propre.
En général: Essayez de repenser le problème et imaginez une autre solution qui évite complètement le problème.
la source
Une autre possibilité, si vous n'aimez pas en voir trop, consiste à abandonner complètement l'utilisation de else et à ajouter une instruction return supplémentaire. Sinon, c'est superflu sauf si vous avez besoin d'une logique plus complexe pour déterminer s'il existe plus que deux possibilités d'action.
Ainsi, votre exemple pourrait devenir:
Personnellement, cela ne me dérange pas d'utiliser la clause else car elle énonce explicitement comment la logique est supposée fonctionner et améliore ainsi la lisibilité de votre code. Certains outils d'embellissement de code préfèrent toutefois simplifier en une seule instruction if pour décourager la logique d'imbrication.
la source
Le cas présenté dans l'exemple de code peut généralement être réduit à une seule
if
instruction. Sur de nombreux systèmes, la fonction d'ouverture de fichier renverra une valeur non valide si le fichier n'existe pas déjà. Parfois, c'est le comportement par défaut; d'autres fois, il doit être spécifié via un argument. Cela signifie que leFileExists
test peut être supprimé, ce qui peut également contribuer aux conditions de concurrence résultant de la suppression de fichier entre le test d'existence et l'ouverture du fichier.Cela ne résout pas directement le problème du mélange de niveaux d'abstraction, car il élimine complètement le problème des tests multiples et non chaînables, bien que le fait de supprimer le test d'existence de fichier ne soit pas incompatible avec la séparation des niveaux d'abstraction. En supposant que les descripteurs de fichier non valides équivalent à "false" et que les descripteurs de fichier soient fermés lorsqu'ils sortent de la portée:
la source
Je suis d'accord avec frozenkoi, cependant, pour C # de toute façon, j'ai pensé qu'il serait utile de suivre la syntaxe des méthodes TryParse.
la source
Votre code est moche parce que vous en faites trop dans une seule fonction. Vous voulez traiter le fichier ou prendre l’action par défaut, commencez donc par dire:
Les programmeurs Perl et Ruby écrivent
processFile(file) || defaultAction()
Maintenant, écrivez ProcessFile:
la source
Bien sûr, vous ne pouvez pas aller aussi loin dans des scénarios comme ceux-ci, mais voici une solution:
Vous voudrez peut-être des filtres supplémentaires. Alors fais ceci:
Bien que cela puisse avoir un sens aussi bien:
Quel est le meilleur? Cela dépend du problème du monde réel auquel vous êtes confronté.
Mais ce qu'il faut retenir, c'est que vous pouvez faire beaucoup avec la composition et le polymorphisme.
la source
Quel est le problème avec l'évidence
Cela me semble assez standard? Pour ce genre de grosse procédure où beaucoup de petites choses doivent se passer et dont l’échec empêcherait ces dernières. Les exceptions le rendent un peu plus propre si c'est une option.
la source
Je réalise que c’est une vieille question, mais j’ai remarqué une tendance qui n’a pas été mentionnée; principalement, définir une variable pour déterminer plus tard la / les méthode (s) que vous souhaitez appeler (en dehors de if ... else ...).
C’est un autre angle à considérer pour rendre le code plus facile à utiliser. Cela vous permet également d’appeler une autre méthode ou de modifier la méthode appropriée à appeler dans certaines situations.
Plutôt que de devoir remplacer toutes les mentions de la méthode (et peut-être manquer certains scénarios), elles sont toutes répertoriées à la fin du bloc if ... else ... et sont plus simples à lire et à modifier. J'ai tendance à utiliser cela lorsque, par exemple, plusieurs méthodes peuvent être appelées, mais dans l'imbrication si ... sinon ... une méthode peut être appelée dans plusieurs correspondances.
Si vous définissez une variable qui définit l'état, vous pouvez avoir plusieurs options profondément imbriquées et mettre à jour l'état lorsque quelque chose doit être (ou ne pas être) exécuté.
Cela pourrait être utilisé comme dans l'exemple de la question où nous vérifions si "DoSomething" s'est produit, et si ce n'est pas le cas, exécutez l'action par défaut. Ou vous pouvez avoir l'état pour chaque méthode que vous souhaitez appeler, définir le cas échéant, puis appeler la méthode applicable en dehors de if ... else ...
À la fin des instructions if if else ... imbriquées, vous vérifiez l'état et vous agissez en conséquence. Cela signifie que vous n'avez besoin que d'une seule mention d'une méthode à la place de tous les emplacements auxquels elle devrait être appliquée.
la source
Pour réduire les SI imbriqués:
1 / retour anticipé;
2 / expression composée (sensible au court-circuit)
Donc, votre exemple peut être refactorisé comme ceci:
la source
J'ai vu beaucoup d'exemples avec "return" que j'utilise aussi, mais parfois, je veux éviter de créer de nouvelles fonctions et utiliser une boucle à la place:
Si vous voulez écrire moins de lignes ou si vous détestez les boucles infinies comme moi, vous pouvez changer le type de boucle en "do ... while (0)" et éviter le dernier "break".
la source
Que diriez-vous de cette solution:
J'ai supposé qu'OpenFile renvoie un pointeur, mais cela pourrait également fonctionner avec le type de valeur return en spécifiant une valeur par défaut non retournable (codes d'erreur ou quelque chose du genre).
Bien sûr, je ne m'attends pas à une action possible via la méthode SomeTest sur le pointeur NULL (mais vous ne le savez jamais), cela pourrait donc également être vu comme une vérification supplémentaire du pointeur NULL pour l'appel SomeTest (contenu).
la source
Clairement, la solution la plus élégante et la plus concise consiste à utiliser une macro de préprocesseur.
Ce qui vous permet d'écrire un code magnifique comme celui-ci:
Il peut être difficile de s’appuyer sur le formatage automatique si vous utilisez souvent cette technique, et certains IDE peuvent vous crier un peu plus sur ce qu’elle suppose à tort comme étant malformé. Et comme dit le proverbe, tout est un compromis, mais je suppose que ce n'est pas un mauvais prix à payer pour éviter les méfaits d'un code répété.
la source
Étant donné que vous avez posé des questions par curiosité et que votre question n'est pas associée à une langue spécifique (même si vous aviez clairement à l'esprit les langues impératives), il peut être intéressant de noter que les langues prenant en charge l'évaluation paresseuse permettent une approche totalement différente. Dans ces langages, les expressions ne sont évaluées que lorsque cela est nécessaire. Vous pouvez donc définir des "variables" et les utiliser uniquement lorsque cela vous semble judicieux. Par exemple, dans un langage fictif avec des structures
let
/ paresseusesin
, vous oubliez le contrôle de flux et écrivez:la source