Il s'agit d'une situation courante et il existe de nombreuses façons courantes de la gérer. Voici ma tentative de réponse canonique. Veuillez commenter si j'ai raté quelque chose et je garderai ce post à jour.
Ceci est une flèche
Ce que vous discutez est connu sous le nom d' anti-modèle de flèche . Il s'agit d'une flèche car la chaîne d'if imbriqués forme des blocs de code qui se développent de plus en plus à droite puis de nouveau à gauche, formant une flèche visuelle qui "pointe" vers le côté droit du volet de l'éditeur de code.
Aplatissez la flèche avec la garde
Nous discutons ici de quelques moyens courants d'éviter la flèche . La méthode la plus courante consiste à utiliser un modèle de garde , dans lequel le code gère d'abord les flux d'exception, puis gère le flux de base, par exemple au lieu de
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... vous utiliseriez ....
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Lorsqu'il y a une longue série de gardes, cela aplatit considérablement le code car toutes les gardes apparaissent complètement à gauche et vos ifs ne sont pas imbriqués. De plus, vous associez visuellement la condition logique à son erreur associée, ce qui permet de savoir plus facilement ce qui se passe:
La 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;
}
Ceci est objectivement et quantifiablement plus facile à lire car
- Les caractères {et} pour un bloc logique donné sont plus proches les uns des autres
- La quantité de contexte mental nécessaire pour comprendre une ligne particulière est plus petite
- L'intégralité de la logique associée à une condition if est plus susceptible d'être sur une seule page
- La nécessité pour le codeur de faire défiler la page / l'œil est considérablement réduite
Comment ajouter du code commun à la fin
Le problème avec le modèle de garde est qu'il repose sur ce qu'on appelle le «retour opportuniste» ou la «sortie opportuniste». En d'autres termes, cela rompt le modèle selon lequel chaque fonction doit avoir exactement un point de sortie. Il s'agit d'un problème pour deux raisons:
- Cela frotte certaines personnes dans le mauvais sens, par exemple les personnes qui ont appris à coder sur Pascal ont appris qu'une fonction = un point de sortie.
- Il ne fournit pas de section de code qui s'exécute à la sortie, quel que soit le sujet traité .
Ci-dessous, j'ai fourni quelques options pour contourner cette limitation soit en utilisant les fonctionnalités du langage, soit en évitant complètement le problème.
Option 1. Vous ne pouvez pas faire cela: utilisez finally
Malheureusement, en tant que développeur c ++, vous ne pouvez pas faire cela. Mais c'est la réponse numéro un pour les langues qui contiennent un mot-clé finally, car c'est exactement à cela qu'il sert.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Option 2. Évitez le problème: restructurez vos fonctions
Vous pouvez éviter le problème en divisant le code en deux fonctions. Cette solution a l'avantage de fonctionner pour n'importe quelle langue, et en outre, elle peut réduire la complexité cyclomatique , qui est un moyen éprouvé de réduire votre taux de défauts, et améliore la spécificité de tous les tests unitaires automatisés.
Voici un exemple:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Option 3. Astuce linguistique: utilisez une fausse boucle
Une autre astuce que je vois consiste à utiliser while (true) et break, comme indiqué dans les autres réponses.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Bien que cela soit moins «honnête» que l'utilisation goto
, il est moins susceptible d'être gâché lors de la refactorisation, car il marque clairement les limites de la portée logique. Un codeur naïf qui coupe et colle vos étiquettes ou vos goto
relevés peut causer de gros problèmes! (Et franchement, le modèle est si commun maintenant je pense qu'il communique clairement l'intention, et n'est donc pas du tout "malhonnête").
Il existe d'autres variantes de cette option. Par exemple, on pourrait utiliser à la switch
place de while
. Toute construction de langage avec un break
mot clé fonctionnerait probablement.
Option 4. Tirez parti du cycle de vie des objets
Une autre approche exploite le cycle de vie des objets. Utilisez un objet contextuel pour transporter vos paramètres (quelque chose que notre exemple naïf manque étrangement) et éliminez-le 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 bien comprendre le cycle de vie de l'objet de la langue de votre choix. Vous avez besoin d'une sorte de ramasse-miettes déterministe pour que cela fonctionne, c'est-à-dire que vous devez savoir quand le destructeur sera appelé. Dans certaines langues, vous devrez utiliser Dispose
un destructeur.
Option 4.1. Tirer parti du cycle de vie de l'objet (modèle wrapper)
Si vous allez utiliser une approche orientée objet, autant le faire correctement. Cette option utilise une classe pour «encapsuler» les ressources qui nécessitent un nettoyage, ainsi que 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 bien comprendre le cycle de vie de votre objet.
Option 5. Astuce linguistique: utiliser l'évaluation de court-circuit
Une autre technique consiste à profiter de l' évaluation des courts-circuits .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Cette solution tire parti du fonctionnement de l'opérateur &&. Lorsque le côté gauche de && est évalué à faux, le côté droit n'est jamais évalué.
Cette astuce est plus utile lorsque du code compact est requis et lorsque le code ne risque pas de voir beaucoup de maintenance, par exemple, vous implémentez un algorithme bien connu. Pour un codage plus général, la structure de ce code est trop fragile; même une modification mineure de la logique pourrait déclencher une réécriture totale.