C'est un petit problème, mais chaque fois que je dois coder quelque chose comme ça, 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();
}
Je suis ouvert aux suggestions de codes diaboliques, ne serait-ce que par curiosité ...
Extrayez-le pour séparer la fonction (méthode) et utilisez l'instruction return
:
if(FileExists(file))
{
contents = OpenFile(file); // <-- prevents inclusion in if
if(SomeTest(contents))
{
DoSomething(contents);
return;
}
}
DefaultAction();
Ou, peut-être mieux, séparer le contenu et son traitement:
contents_t get_contents(name_t file)
{
if(!FileExists(file))
return null;
contents = OpenFile(file);
if(!SomeTest(contents)) // like IsContentsValid
return null;
return contents;
}
...
contents = get_contents(file)
contents ? DoSomething(contents) : DefaultAction();
Mise à jour:
Pourquoi pas des exceptions, pourquoi OpenFile
ne lance pas IO exception:
Je pense que c'est une question vraiment générique, plutôt qu'une question sur le fichier IO. Des noms comme FileExists
, OpenFile
peuvent prêter à confusion, mais si les remplacer par Foo
, Bar
, etc., - il serait plus clair que DefaultAction
peut être appelé aussi souvent que DoSomething
, il peut donc s'agir d'un cas non exceptionnel. Péter Török a écrit à ce sujet à la fin de sa réponse
Pourquoi il y a opérateur conditionnel ternaire dans la 2ème variante:
S'il y avait une balise [C++], j'écrirais une instruction if
avec une déclaration de contents
dans sa partie condition:
if(contents_t contents = get_contents(file))
DoSomething(contents);
else
DefaultAction();
Mais pour les autres langages (de type C), if(contents) ...; else ...;
est exactement la même que l'instruction d'expression avec opérateur conditionnel ternaire, mais plus longue. Parce que la partie principale du code était la fonction get_contents
, Je viens d'utiliser la version plus courte (et j'ai également omis le type contents
). Quoi qu'il en soit, c'est au-delà de cette question.
Si le langage de programmation que vous utilisez (0) des comparaisons binaires de courts-circuits (c'est-à-dire si n'appelle pas SomeTest
si FileExists
renvoie false) et (1) l'affectation renvoie une valeur (le résultat de OpenFile
est assigné à contents
puis cette valeur est passée en argument à SomeTest
), vous pouvez utiliser quelque chose comme ce qui suit, mais il vous serait quand même conseillé de commentez le code en notant que le single =
est intentionnel.
if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
DoSomething(contents);
}
else
{
DefaultAction();
}
Selon la complexité du if, il peut être préférable d'avoir une variable indicateur (qui sépare le test des conditions de réussite/d'échec avec le code qui gère l'erreur DefaultAction
dans ce cas)
Plus sérieusement 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 le code non orthogonal est mauvais, considérons l'exemple d'origine, lorsqu'une nouvelle exigence de 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:
if(FileExists(file))
{
if(! OnNetworkDisk(file))
{
contents = OpenFile(file); // <-- prevents inclusion in if
if(SomeTest(contents))
{
DoSomething(contents);
}
else
{
DefaultAction();
}
}
else
{
DefaultAction();
}
}
else
{
DefaultAction();
}
Mais il existe également une exigence selon laquelle nous ne devons pas non plus ouvrir de gros fichiers de plus de 2 Go. Eh bien, nous venons de mettre à jour à nouveau:
if(FileExists(file))
{
if(LessThan2Gb(file))
{
if(! OnNetworkDisk(file))
{
contents = OpenFile(file); // <-- prevents inclusion in if
if(SomeTest(contents))
{
DoSomething(contents);
}
else
{
DefaultAction();
}
}
else
{
DefaultAction();
}
else
{
DefaultAction();
}
}
else
{
DefaultAction();
}
Il devrait être très clair qu'un tel style de code sera un énorme problème de maintenance.
Parmi les réponses qui sont écrites correctement orthogonalement, il y a deuxième exemple d'Abyx et réponse de Jan Hudec , donc je ne répéterai pas cela, il suffit de souligner que l'ajout des deux exigences dans ces les réponses ne seraient que
if(! LessThan2Gb(file))
return null;
if(OnNetworkDisk(file))
return null;
(ou goto notexists;
au lieu de return null;
), n'affectant aucun autre code que les lignes ajoutées . Par exemple. orthogonal.
Lors des tests, la règle générale devrait être de tester les exceptions, pas le cas normal .
Évidemment:
Whatever(Arguments)
{
if(!FileExists(file))
goto notexists;
contents = OpenFile(file); // <-- prevents inclusion in if
if(!SomeTest(contents))
goto notexists;
DoSomething(contents);
return;
notexists:
DefaultAction();
}
Vous avez dit que vous êtes ouvert même aux solutions perverses, alors en utilisant le mal, allez-y, non?
En fait, selon le contexte, cette solution pourrait bien être moins mauvaise que le mal faisant l'action deux fois ou la variable supplémentaire mal. Je l'ai enveloppé dans une fonction, car ce ne serait certainement pas OK au milieu d'une fonction longue (notamment en raison du retour au milieu). Mais la fonction longue n'est pas OK, point final.
Lorsque vous avez des exceptions, elles seront plus faciles à lire, surtout si vous pouvez faire en sorte que OpenFile et DoSomething lèvent simplement une exception si les conditions ne sont pas remplies, vous n'avez donc pas besoin du tout de vérifications explicites. En revanche, en C++, Java et C # lançant une exception est une opération lente, donc du point de vue des performances, le goto est toujours préférable.
Remarque sur le "mal": C++ FAQ 6.15 définit le "mal" comme:
Cela signifie que tel ou tel est quelque chose que vous devriez éviter la plupart du temps , mais pas quelque chose que vous devriez éviter tout le temps . Par exemple, vous finirez par utiliser ces choses "diaboliques" chaque fois qu'elles sont "la moins mauvaise des alternatives perverses".
Et cela s'applique à goto
dans ce contexte. Les constructions de contrôle de flux structurées sont meilleures la plupart du temps, mais lorsque vous vous trouvez dans la situation où elles accumulent trop de leurs propres maux, comme l'affectation en condition, l'imbrication de plus de 3 niveaux environ, la duplication de code ou les conditions longues, goto
peut finir par être moins maléfique.
Une possibilité:
boolean handled = false;
if(FileExists(file))
{
contents = OpenFile(file); // <-- prevents inclusion in if
if(SomeTest(contents))
{
DoSomething(contents);
handled = true;
}
}
if (!handled)
{
DefaultAction();
}
Bien sûr, 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:
try
{
contents = OpenFile(file); // throws IO exception if file not found
DoSomething(contents); // calls SomeTest() and throws exception on failure
}
catch(Exception e)
{
DefaultAction();
// and the exception should be at least logged...
}
Cela semble plus simple, mais il n'est applicable que si
DefaultAction()
correspond à chaqueSomeTest()
défaillante est clairement une condition erronée, il est donc approprié de lever une exception dessus.function FileContentsExists(file) {
return FileExists(file) ? OpenFile(file) : null;
}
...
contents = FileContentExists(file);
if(contents && SomeTest(contents))
{
DoSomething(contents);
}
else
{
DefaultAction();
}
Les fonctions devraient faire une chose. Ils devraient bien le faire. Ils devraient le faire seulement.
- Robert Martin dans Clean Code
Certaines personnes trouvent cette approche un peu extrême, mais c'est aussi très propre. Permettez-moi d'illustrer en Python:
def processFile(self):
if self.fileMeetsTest():
self.doSomething()
else:
self.defaultAction()
def fileMeetsTest(self):
return os.path.exists(self.path) and self.contentsTest()
def contentsTest(self):
with open(self.path) as file:
line = file.readline()
return self.firstLineTest(line)
Quand il dit que les fonctions devraient faire une chose, il veut dire une chose. processFile()
choisit une action en fonction du résultat d'un test, et c'est tout. fileMeetsTest()
combine toutes les conditions du test, et c'est tout. contentsTest()
transfère la première ligne vers firstLineTest()
, et c'est tout.
Cela ressemble à beaucoup de fonctions, mais il se lit pratiquement comme un anglais simple:
Pour traiter le fichier, vérifiez s'il répond au test. Si c'est le cas, faites quelque chose. Sinon, effectuez l'action par défaut. Le fichier satisfait au test s'il existe et réussit le test de contenu. Pour tester le contenu, ouvrez le fichier et testez la première ligne. Le test de la première ligne ...
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 dans processFile()
, et il aura toujours de bonnes connaissances de haut niveau de ce que fait la fonction.
C'est à un niveau d'abstraction plus élevé:
if (WeCanDoSomething(file))
{
DoSomething(contents);
}
else
{
DefaultAction();
}
Et cela remplit les détails.
boolean WeCanDoSomething(file)
{
if FileExists(file)
{
contents = OpenFile(file);
return (SomeTest(contents));
}
else
{
return FALSE;
}
}
En ce qui concerne ce que cela s'appelle, il peut facilement se développer en anti-motif de pointe de flèche à mesure que votre code se développe pour gérer plus d'exigences (comme indiqué par la réponse fournie) à https://softwareengineering.stackexchange.com/a/122625/33922 ) puis tombe dans le piège d'avoir d'énormes sections de codes avec des instructions conditionnelles imbriquées qui ressemblent à une flèche.
Voir des liens tels que;
http://codinghorror.com/blog/2006/01/flattening-arrow-code.html
Il y a beaucoup plus à ce sujet et d'autres anti-modèles sur Google.
Voici quelques bons conseils que Jeff donne sur son blog à ce sujet;
1) Remplacer les conditions par des clauses de garde.
2) Décomposer les blocs conditionnels en fonctions distinctes.
3) Convertissez les chèques négatifs en chèques positifs
4) Revenez toujours opportuniste dès que possible de la fonction.
Voir aussi certains commentaires sur le blog de Jeff concernant les suggestions de Steve McConnells sur les retours anticipés;
"Utilisez un retour lorsqu'il améliore la lisibilité: dans certaines routines, une fois que vous connaissez la réponse, vous souhaitez la renvoyer immédiatement à la routine appelante. Si la routine est définie de telle manière qu'elle ne nécessite aucun nettoyage supplémentaire une fois qu'elle est détecte une erreur, ne pas retourner immédiatement signifie que vous devez écrire plus de code. "
...
"Minimisez le nombre de retours dans chaque routine: il est plus difficile de comprendre une routine lorsque, en la lisant en bas, vous n'êtes pas conscient de la possibilité qu'elle soit retournée quelque part. Pour cette raison, utilisez les retours judicieusement - uniquement lorsqu'ils s'améliorent lisibilité. "
J'ai toujours souscrit à la théorie de 1 entrée/sortie par fonction en raison de ce que j'ai appris il y a environ 15 ans. Je pense que cela rend le code beaucoup plus facile à lire et comme vous le mentionnez plus maintenable
Ceci est conforme aux règles DRY, no-goto et no-multiple-return, est évolutif et lisible à mon avis:
success = FileExists(file);
if (success)
{
contents = OpenFile(file);
success = SomeTest(contents);
}
if (success)
{
DoSomething(contents);
}
else
{
DefaultAction();
}
Pour ce cas particulier, la réponse est assez simple ...
Il existe une condition de concurrence entre FileExists
et OpenFile
: 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
:
contents = OpenFile(file);
if (!contents) // open failed
DefaultAction();
else (SomeTest(contents))
DoSomething(contents);
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.
Je l'extrait dans une méthode distincte, puis:
if(!FileExists(file))
{
DefaultAction();
return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
DefaultAction();
return;
}
DoSomething(contents);
ce qui permet également
if(!FileExists(file))
{
DefaultAction();
return Result.FileNotFound;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
DefaultAction();
return Result.TestFailed;
}
DoSomething(contents);
return Result.Success;
alors vous pourriez peut-être supprimer les appels DefaultAction
et laisser l'exécution de DefaultAction
à l'appelant:
Result OurMethod(file)
{
if(!FileExists(file))
{
return Result.FileNotFound;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
return Result.TestFailed;
}
DoSomething(contents);
return Result.Success;
}
void Caller()
{
// something, something...
var result = OurMethod(file);
// if (result == Result.FileNotFound || result == Result.TestFailed), or just
if (result != Result.Success)
{
DefaultAction();
}
}
J'aime l'approche de Jeanne Pindar aussi.
Une autre possibilité si vous n'aimez pas en voir trop est de supprimer complètement l'utilisation de else et d'ajouter une déclaration de retour supplémentaire. Else est un peu superflu à moins que vous ayez besoin d'une logique plus complexe pour déterminer s'il y a plus que deux possibilités d'action.
Ainsi, votre exemple pourrait devenir:
void DoABunchOfStuff()
{
if(FileExists(file))
{
DoSomethingWithFileContent(file);
return;
}
DefaultAction();
}
void DoSomethingWithFileContent(file)
{
var contents = GetFileContents(file)
if(SomeTest(contents))
{
DoSomething(contents);
return;
}
DefaultAction();
}
AReturnType GetFileContents(file)
{
return OpenFile(file);
}
Personnellement, cela ne me dérange pas d'utiliser la clause else car elle indique explicitement comment la logique est censé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.
Le cas illustré dans l'exemple de code peut généralement être réduit à une seule instruction if
. Sur de nombreux systèmes, la fonction d'ouverture de fichier renvoie 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 le test FileExists
peut être abandonné, ce qui peut également aider avec les conditions de concurrence résultant de la suppression de fichier entre le test d'existence et l'ouverture de fichier.
file = OpenFile(path);
if(isValidFileHandle(file) && SomeTest(file)) {
DoSomething(file);
} else {
DefaultAction();
}
Cela ne résout pas directement le problème de mélange au niveau de l'abstraction car il évite complètement le problème de plusieurs tests non chaînables, bien que la suppression du test d'existence de fichier ne soit pas incompatible avec la séparation des niveaux d'abstraction. En supposant que les descripteurs de fichiers non valides sont équivalents à "faux" et que les descripteurs de fichiers se ferment lorsqu'ils sortent du domaine:
OpenFileIfSomething(path:String) : FileHandle {
file = OpenFile(path);
if (file && SomeTest(file)) {
return file;
}
return null;
}
...
if ((file = OpenFileIfSomething(path))) {
DoSomething(file);
} else {
DefaultAction();
}
Je suis d'accord avec freezkoi, cependant, pour C # de toute façon, je pensais que cela aiderait à suivre la syntaxe des méthodes TryParse.
if(FileExists(file) && TryOpenFile(file, out contents))
DoSomething(contents);
else
DefaultAction();
bool TryOpenFile(object file, out object contents)
{
try{
contents = OpenFile(file);
}
catch{
//something bad happened, computer probably exploded
return false;
}
return true;
}
Qu'est-ce qui ne va pas avec l'évidence
if(!FileExists(file)) {
DefaultAction();
return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
DefaultAction();
return;
}
DoSomething(contents);
Cela me semble assez standard? Pour ce genre de grande procédure où beaucoup de petites choses doivent se produire, dont l'échec empêcherait ce dernier. Les exceptions le rendent un peu plus propre si c'est une option.
Bien sûr, vous ne pouvez aller aussi loin que dans des scénarios comme ceux-ci, mais voici un chemin à parcourir:
interface File<T> {
function isOK():Bool;
function getData():T;
}
var appleFile:File<Apple> = appleStorage.get(fileURI);
if (appleFile.isOK())
eat(file.getData());
else
cry();
Vous voudrez peut-être des filtres supplémentaires. Ensuite, faites ceci:
var appleFile = appleStorage.get(fileURI, isEdible);
//isEdible is of type Apple->Bool and will be used internally to answer to the isOK call
if (appleFile.isOK())
eat(file.getData());
else
cry();
Bien que cela puisse aussi avoir du sens:
function eat(Apple:Apple) {
if (isEdible(Apple))
digest(Apple);
else
die();
}
var appleFile = appleStorage.get(fileURI);
if (appleFile.isOK())
eat(appleFile.getData());
else
cry();
Quel est le meilleur? Cela dépend du problème du monde réel auquel vous êtes confronté.
Mais la chose à retenir est que vous pouvez faire beaucoup avec la composition et le polymorphisme.
Votre code est moche parce que vous en faites trop dans une seule fonction. Vous voulez soit traiter le fichier, soit effectuer l'action par défaut, alors commencez par dire que:
if (!ProcessFile(file)) {
DefaultAction();
}
Perl et Ruby écrivent processFile(file) || defaultAction()
Maintenant, écrivez ProcessFile:
if (FileExists(file)) {
contents = OpenFile(file);
if (SomeTest(contents)) {
processContents(contents);
return true;
}
}
return false;
De toute évidence, la solution la plus élégante et concise consiste à utiliser une macro de préprocesseur.
#define DOUBLE_ELSE(CODE) else { CODE } } else { CODE }
Ce qui vous permet d'écrire du beau code comme celui-ci:
if(FileExists(file))
{
contents = OpenFile(file);
if(SomeTest(contents))
{
DoSomething(contents);
}
DOUBLE_ELSE(DefaultAction();)
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 sur ce qu'il suppose à tort être mal formé. Et comme le dit le proverbe, tout est un compromis, mais je suppose que ce n'est pas un mauvais prix à payer pour éviter les maux du code répété.
Pour réduire les IF imbriqués:
1/retour anticipé;
2/expression composée (sensible aux courts-circuits)
Ainsi, votre exemple peut être refactorisé comme ceci:
if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
DoSomething(contents);
return;
}
DefaultAction();
Je reconnais que c'est une vieille question, mais j'ai remarqué un modèle qui n'a pas été mentionné; principalement, définir une variable pour déterminer ultérieurement la ou les méthodes que vous souhaitez appeler (en dehors de if ... else ...).
C'est juste un autre angle à regarder pour rendre le code plus facile à utiliser. Il vous permet également d'ajouter une autre méthode ou de changer la méthode appropriée qui doit être appelée 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 à l'utiliser lorsque, par exemple, plusieurs méthodes peuvent être appelées, mais dans le cas où ... sinon ... une méthode peut être appelée en plusieurs correspondances.
Si vous définissez une variable qui définit l'état, vous pouvez avoir de nombreuses options profondément imbriquées et mettre à jour l'état lorsque quelque chose doit être (ou ne pas être) effectué.
Cela pourrait être utilisé comme dans l'exemple demandé dans la question où nous vérifions si "DoSomething" s'est produit, et sinon, effectuez l'action par défaut. Ou vous pouvez avoir un é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 imbriquées if ... else ..., vous vérifiez l'état et agissez en conséquence. Cela signifie que vous n'avez besoin que d'une seule mention d'une méthode au lieu de tous les emplacements auxquels elle doit être appliquée.
bool ActionDone = false;
if (Method_1(object_A)) // Test 1
{
result_A = Method_2(object_A); // Result 1
if (Method_3(result_A)) // Test 2
{
Method_4(result_A); // Action 1
ActionDone = true;
}
}
if (!ActionDone)
{
Method_5(); // Default Action
}
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:
while (1) {
if (FileExists(file)) {
contents = OpenFile(file);
if (SomeTest(contents)) {
DoSomething(contents);
break;
}
}
DefaultAction();
break;
}
Si vous voulez écrire moins de lignes ou que vous détestez les boucles infinies comme moi, vous pouvez changer le type de boucle en "do ... while (0)" et éviter le dernier "break".
Que diriez-vous de cette solution:
content = NULL; //I presume OpenFile returns a pointer
if(FileExists(file))
contents = OpenFile(file);
if(content != NULL && SomeTest(contents))
DoSomething(contents);
else
DefaultAction();
J'ai fait l'hypothèse 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 renvoyable (codes d'erreur ou quelque chose comme ça).
Bien sûr, je ne m'attends pas à une action possible via la méthode SomeTest sur le pointeur NULL (mais vous ne savez jamais), donc cela pourrait également être considéré comme une vérification supplémentaire du pointeur NULL pour l'appel SomeTest (contents).