Dies ist eine häufige Situation, und es gibt viele gängige Möglichkeiten, damit umzugehen. Hier ist mein Versuch einer kanonischen Antwort. Bitte kommentieren Sie, wenn ich etwas verpasst habe und ich werde diesen Beitrag auf dem neuesten Stand halten.
Dies ist ein Pfeil
Was Sie besprechen, wird als Pfeil-Anti-Muster bezeichnet . Es wird als Pfeil bezeichnet, da die Kette verschachtelter ifs Codeblöcke bildet, die sich immer weiter nach rechts und dann wieder nach links ausdehnen und einen visuellen Pfeil bilden, der auf die rechte Seite des Code-Editor-Bereichs "zeigt".
Flache den Pfeil mit der Wache ab
Einige gebräuchliche Methoden zur Vermeidung des Pfeils werden hier erläutert . Die gängigste Methode ist die Verwendung eines Wachmusters, in dem der Code behandelt die Ausnahme erste fließt und dann übernimmt die Grundströmung, beispielsweise anstelle von
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... du würdest benutzen ....
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Wenn es eine lange Reihe von Wachen gibt, wird der Code dadurch erheblich abgeflacht, da alle Wachen ganz links angezeigt werden und Ihre Wenns nicht verschachtelt sind. Darüber hinaus koppeln Sie die logische Bedingung visuell mit dem zugehörigen Fehler, wodurch Sie viel einfacher erkennen können, was gerade passiert:
Pfeil:
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;
}
Bewachen:
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;
}
Dies ist objektiv und quantifizierbar leichter zu lesen, weil
- Die Zeichen {und} für einen bestimmten Logikblock liegen näher beieinander
- Der mentale Kontext, der zum Verstehen einer bestimmten Linie benötigt wird, ist geringer
- Die gesamte Logik, die einer if-Bedingung zugeordnet ist, befindet sich eher auf einer Seite
- Die Notwendigkeit, dass der Codierer die Seiten- / Augenspur scrollt, wird erheblich verringert
So fügen Sie am Ende allgemeinen Code hinzu
Das Problem mit dem Schutzmuster besteht darin, dass es auf dem beruht, was als "opportunistische Rückkehr" oder "opportunistischer Ausstieg" bezeichnet wird. Mit anderen Worten, es bricht das Muster, dass jede Funktion genau einen Austrittspunkt haben sollte. Dies ist aus zwei Gründen ein Problem:
- Es reibt einige Leute in die falsche Richtung, z. B. Leute, die gelernt haben, auf Pascal zu programmieren, haben gelernt, dass eine Funktion = ein Austrittspunkt ist.
- Es enthält keinen Codeabschnitt, der beim Beenden ausgeführt wird, unabhängig davon , um welches Thema es sich handelt.
Im Folgenden habe ich einige Optionen bereitgestellt, um diese Einschränkung zu umgehen, indem ich entweder Sprachfunktionen verwende oder das Problem insgesamt vermeide.
Option 1. Sie können dies nicht tun: verwenden finally
Als C ++ - Entwickler können Sie dies leider nicht tun. Dies ist jedoch die Antwort Nummer eins für Sprachen, die ein Schlüsselwort finally enthalten, da dies genau das ist, wofür es ist.
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. Vermeiden Sie das Problem: Restrukturieren Sie Ihre Funktionen
Sie können das Problem vermeiden, indem Sie den Code in zwei Funktionen aufteilen. Diese Lösung hat den Vorteil, dass sie für jede Sprache funktioniert. Darüber hinaus kann sie die zyklomatische Komplexität reduzieren. Dies ist eine bewährte Methode zur Reduzierung Ihrer Fehlerrate und verbessert die Spezifität automatisierter Komponententests.
Hier ist ein Beispiel:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Option 3. Sprachtrick: Verwenden Sie eine gefälschte Schleife
Ein weiterer häufiger Trick, den ich sehe, ist die Verwendung von while (true) und break, wie in den anderen Antworten gezeigt.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Dies ist zwar weniger "ehrlich" als die Verwendung goto
, aber es ist weniger anfällig, beim Refactoring durcheinander zu kommen, da es die Grenzen des logischen Umfangs klar markiert. Ein naiver Codierer, der Ihre Etiketten oder goto
Aussagen ausschneidet und einfügt, kann große Probleme verursachen! (Und ehrlich gesagt ist das Muster jetzt so verbreitet, dass ich denke, es kommuniziert klar die Absicht und ist daher überhaupt nicht "unehrlich").
Es gibt andere Varianten dieser Optionen. Zum Beispiel könnte man switch
anstelle von verwenden while
. Jedes Sprachkonstrukt mit einem break
Schlüsselwort würde wahrscheinlich funktionieren.
Option 4. Nutzen Sie den Objektlebenszyklus
Ein anderer Ansatz nutzt den Objektlebenszyklus. Verwenden Sie ein Kontextobjekt, um Ihre Parameter zu übertragen (etwas, das unserem naiven Beispiel verdächtig fehlt), und entsorgen Sie es, wenn Sie fertig sind.
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
}
Hinweis: Stellen Sie sicher, dass Sie den Objektlebenszyklus der Sprache Ihrer Wahl verstehen. Damit dies funktioniert, benötigen Sie eine Art deterministische Garbage Collection, dh Sie müssen wissen, wann der Destruktor aufgerufen wird. In einigen Sprachen müssen Sie Dispose
anstelle eines Destruktors verwenden.
Option 4.1. Nutzen Sie den Objektlebenszyklus (Wrapper-Muster)
Wenn Sie einen objektorientierten Ansatz verwenden möchten, können Sie dies auch richtig machen. Diese Option verwendet eine Klasse, um die zu bereinigenden Ressourcen sowie die anderen Vorgänge zu "verpacken".
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
Stellen Sie erneut sicher, dass Sie Ihren Objektlebenszyklus verstehen.
Option 5. Sprachtrick: Verwenden Sie die Kurzschlussauswertung
Eine andere Technik besteht darin, die Kurzschlussbewertung zu nutzen .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Diese Lösung nutzt die Arbeitsweise des && Operators. Wenn die linke Seite von && als falsch ausgewertet wird, wird die rechte Seite niemals ausgewertet.
Dieser Trick ist am nützlichsten, wenn kompakter Code erforderlich ist und wenn für den Code wahrscheinlich keine große Wartung erforderlich ist, z. B. wenn Sie einen bekannten Algorithmus implementieren. Für eine allgemeinere Codierung ist die Struktur dieses Codes zu spröde. Selbst eine geringfügige Änderung der Logik kann ein vollständiges Umschreiben auslösen.