Jest to powszechna sytuacja i istnieje wiele typowych sposobów radzenia sobie z nią. Oto moja próba kanonicznej odpowiedzi. Jeśli coś przeoczyłem, proszę o komentarz, a ten post będę aktualizowany na bieżąco.
To jest strzała
To, co omawiasz, nazywa się anty-wzorem strzałki . Nazywa się to strzałką, ponieważ łańcuch zagnieżdżonych ifs tworzy bloki kodu, które rozszerzają się coraz bardziej w prawo, a następnie z powrotem w lewo, tworząc strzałkę wizualną, która „wskazuje” na prawą stronę panelu edytora kodu.
Spłaszcz strzałę strażnikiem
Omówiono tutaj niektóre typowe sposoby unikania Strzałki . Najpopularniejszą metodą jest użycie wzorca zabezpieczającego , w którym kod obsługuje najpierw przepływy wyjątków, a następnie obsługuje przepływ podstawowy, np. Zamiast
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... użyłbyś ...
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Gdy jest długa seria strażników, spłaszcza to znacznie kod, ponieważ wszyscy strażnicy pojawiają się aż do lewej, a twoje nie są zagnieżdżone. Ponadto wizualnie parujesz warunek logiczny z powiązanym błędem, co znacznie ułatwia stwierdzenie, co się dzieje:
Strzałka:
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;
}
Strzec:
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;
}
Jest to obiektywnie i ilościowo łatwiejsze do odczytania, ponieważ
- Znaki {i} dla danego bloku logicznego są bliżej siebie
- Ilość kontekstu mentalnego potrzebnego do zrozumienia określonej linii jest mniejsza
- Całość logiki powiązanej z warunkiem if jest bardziej prawdopodobne na jednej stronie
- Konieczność przewijania kodera przez stronę / ścieżkę oka jest znacznie zmniejszona
Jak dodać wspólny kod na końcu
Problem z wzorcem ochronnym polega na tym, że polega on na tak zwanym „oportunistycznym powrocie” lub „oportunistycznym wyjściu”. Innymi słowy, łamie to wzór, że każda funkcja powinna mieć dokładnie jeden punkt wyjścia. Jest to problem z dwóch powodów:
- Pociera niektóre osoby w niewłaściwy sposób, np. Osoby, które nauczyły się kodować na Pascalu, nauczyły się, że jedna funkcja = jeden punkt wyjścia.
- Nie zawiera części kodu, która jest wykonywana przy wyjściu bez względu na to , co jest przedmiotem.
Poniżej przedstawiłem kilka opcji obejścia tego ograniczenia albo za pomocą funkcji językowych, albo całkowicie unikając problemu.
Opcja 1. Nie możesz tego zrobić: użyj finally
Niestety, jako programista c ++, nie możesz tego zrobić. Jest to jednak odpowiedź numer jeden w przypadku języków, które zawierają słowo kluczowe, ponieważ właśnie po to jest.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Opcja 2. Uniknij problemu: zrestrukturyzuj swoje funkcje
Możesz uniknąć problemu, dzieląc kod na dwie funkcje. Rozwiązanie to ma tę zaletę, że działa w dowolnym języku, a dodatkowo może zmniejszyć złożoność cykliczną , co jest sprawdzonym sposobem na zmniejszenie liczby defektów i poprawia specyfikę zautomatyzowanych testów jednostkowych.
Oto przykład:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Opcja 3. Sztuczka językowa: użyj fałszywej pętli
Inną częstą sztuczką, którą widzę, jest używanie while (true) i break, jak pokazano w innych odpowiedziach.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Chociaż jest to mniej „uczciwe” niż używanie goto
, jest mniej podatne na pomieszanie podczas refaktoryzacji, ponieważ wyraźnie wyznacza granice zakresu logiki. Naiwny programista, który wycina i wkleja etykiety lub goto
oświadczenia, może powodować poważne problemy! (I szczerze mówiąc, ten wzór jest teraz tak powszechny, że myślę, że wyraźnie przekazuje intencję i dlatego nie jest wcale „nieuczciwy”).
Istnieją inne warianty tej opcji. Na przykład można użyć switch
zamiast while
. break
Prawdopodobnie działałby każdy konstrukt języka ze słowem kluczowym.
Opcja 4. Wykorzystaj cykl życia obiektu
Jedno inne podejście wykorzystuje cykl życia obiektu. Użyj obiektu kontekstowego, aby przenieść parametry (coś, czego podejrzanie brakuje w naszym naiwnym przykładzie) i usuń je, gdy skończysz.
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
}
Uwaga: Upewnij się, że rozumiesz cykl życia obiektu w wybranym języku. Aby to zadziałało, potrzebujesz pewnego rodzaju deterministycznego wyrzucania elementów bezużytecznych, tzn. Musisz wiedzieć, kiedy zostanie wywołany destruktor. W niektórych językach będziesz musiał użyć Dispose
zamiast destruktora.
Opcja 4.1. Wykorzystaj cykl życia obiektu (wzór opakowania)
Jeśli zamierzasz zastosować podejście obiektowe, równie dobrze możesz to zrobić poprawnie. Ta opcja używa klasy do „zawijania” zasobów, które wymagają czyszczenia, a także innych operacji.
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
Ponownie upewnij się, że rozumiesz swój cykl życia obiektu.
Opcja 5. Sztuczka językowa: Użyj oceny zwarcia
Inną techniką jest skorzystanie z oceny zwarcia .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
To rozwiązanie wykorzystuje sposób działania operatora &&. Kiedy lewa strona && ma wartość false, prawa strona nigdy nie jest oceniana.
Ta sztuczka jest najbardziej przydatna, gdy wymagany jest zwarty kod i gdy kod prawdopodobnie nie wymaga wiele konserwacji, np. Wdrażasz dobrze znany algorytm. Dla bardziej ogólnego kodowania struktura tego kodu jest zbyt krucha; nawet niewielka zmiana w logice może spowodować całkowite przepisanie.