C'est une situation commune et il y a plusieurs façons de traiter avec elle. Voici ma tentative de réponse canonique. S'il vous plaît commentaire si j'ai oublié quelque chose et je vais garder ce post à jour.
C'est une Flèche
Le sujet est connu comme la flèche anti-modèle. Il est appelé une flèche parce que la chaîne de imbriquée ifs forme de blocs de code qui s'élargissent de plus en plus loin vers la droite puis vers la gauche, formant un visuel flèche qui "points" sur le côté droit de l'éditeur de code volet.
Aplatir la Flèche avec la Garde
Quelques moyens communs d'éviter la Flèche sont discutés ici. La méthode la plus courante consiste à utiliser un garde - modèle, dans lequel le code gère l'exception des flux de première et gère le flux de base, par exemple, au lieu de
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... que vous souhaitez utiliser....
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Quand il y a une longue série de gardes cette aplatit le code considérablement comme tous les gardes apparaissent tout le chemin à gauche et à votre ifs ne sont pas imbriqués. En outre, vous êtes visuellement jumelage de la condition logique avec son associé de l'erreur, ce qui le rend beaucoup plus facile de dire ce qui se passe:
Flèche:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
Garde:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
Ce qui est objectivement et quantitativement la plus facile à lire parce que
- Les caractères { et } pour une logique de bloc sont proches
- Le montant de mental contexte nécessaire pour comprendre une ligne particulière est plus petit
- L'intégralité de la logique associée à une condition if est plus susceptible d'être sur une page
- La nécessité pour le codeur pour faire défiler la page/l'œil de la piste est grandement diminuée
Comment ajouter le code commun à la fin
Le problème avec la garde de modèle est qu'il s'appuie sur ce qui est appelé "opportunistes retour" ou "opportunistes de sortie." En d'autres termes, il brise le modèle que chaque fonction doit avoir exactement un point de sortie. C'est un problème pour deux raisons:
- Elle se frotte à certaines personnes dans le mauvais sens, par exemple, des gens qui ont appris à code sur Pascal ont appris qu'une fonction = un point de sortie.
-
Il ne fournit pas une section de code qui s'exécute à la sortie de n'importe quoi, qui est le sujet à portée de main.
Ci-dessous, que j'ai fourni quelques options pour contourner cette limitation en utilisant des fonctionnalités de langage ou en évitant complètement le problème.
L'Option 1. Vous ne pouvez pas le faire: utiliser l' finally
Malheureusement, en tant que développeur c++, vous ne pouvez pas faire cela. Mais c'est le nombre une seule réponse pour les langues qui contiennent un mot-clé finally, car c'est exactement ce qu'il est pour.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
L'Option 2. Éviter le problème: Restructurer vos fonctions
Vous pouvez éviter ce problème en brisant le code en deux fonctions. Cette solution a l'avantage de travailler pour n'importe quelle langue, et en outre, il peut réduire la complexité cyclomatique, qui est un moyen éprouvé pour réduire votre taux de défauts, et améliore la spécificité de tests unitaires automatisés.
Voici un exemple:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
L'Option 3. Langue astuce: Utiliser une fausse boucle
Un autre truc commun, je vois est d'utiliser while(true) et l'introduction, comme indiqué dans les autres réponses.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Alors que c'est moins "honnête" que l'utilisation d' goto
, il est moins enclin à être foiré lors d'un refactoring, car il marque clairement les limites de la logique portée. Un naïf codeur qui couper et coller vos étiquettes ou votre goto
instructions peut entraîner de graves problèmes! (Et franchement, le modèle est si commun aujourd'hui, je pense qu'il communique clairement l'intention, et n'est donc pas "malhonnêtes").
Il existe d'autres variantes de ces options. Par exemple, on pourrait utiliser switch
au lieu de while
. Une construction du langage avec un break
mot-clé serait probablement travailler.
L'Option 4. L'effet de levier de l'objet du cycle de vie
Une autre approche utilise l'objet du cycle de vie. Utiliser un objet de contexte de transporter vos paramètres (quelque chose qui notre naïf exemple étrangement manque) et de s'en débarrasser lorsque vous avez terminé.
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
Remarque: assurez-vous de comprendre l'objet du cycle de vie de la langue de votre choix. Vous avez besoin d'une sorte de déterministe de collecte des ordures pour que cela fonctionne, vous devez savoir quand le destructeur sera appelé. Dans certaines langues, vous devez utiliser Dispose
au lieu d'un destructeur.
Option 4.1. L'effet de levier de l'objet du cycle de vie (wrapper modèle)
Si vous allez utiliser une approche orientée-objet, peut aussi bien le faire à droite. Cette option utilise une classe pour "encapsuler" les ressources qui nécessite de nettoyage, ainsi que de ses autres opérations.
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
Encore une fois, assurez-vous de comprendre l'objet de votre cycle de vie.
Option 5. Langue astuce: Utilisez évaluation de court-circuit
Une autre technique est de prendre avantage de l' évaluation de court-circuit.
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Cette solution tire parti de la façon dont l' && opérateur fonctionne. Lorsque le côté gauche de && false, la droite n'est jamais évaluée.
Cette astuce est particulièrement utile lorsque compact code est nécessaire et lorsque le code n'est pas susceptible de voir beaucoup d'entretien, de l'e.g vous êtes à la mise en œuvre d'un algorithme bien connu. Pour plus générales de codage de la structure de ce code est trop fragile, même un changement mineur à la logique pourrait déclencher une réécriture totale.