Questa è una situazione comune e ci sono molti modi comuni per affrontarla. Ecco il mio tentativo di risposta canonica. Si prega di commentare se ho perso qualcosa e terrò aggiornato questo post.
Questa è una freccia
Quello di cui stai discutendo è noto come anti-schema delle frecce . Si chiama freccia perché la catena di if nidificati forma blocchi di codice che si espandono sempre più a destra e poi di nuovo a sinistra, formando una freccia visiva che "punta" sul lato destro del riquadro dell'editor del codice.
Appiattire la freccia con la guardia
Alcuni modi comuni per evitare la freccia sono discussi qui . Il metodo più comune è utilizzare un modello di protezione , in cui il codice gestisce prima i flussi di eccezioni e quindi gestisce il flusso di base, ad esempio invece di
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... useresti ...
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Quando c'è una lunga serie di guardie, questo appiattisce considerevolmente il codice poiché tutte le guardie appaiono completamente a sinistra e i tuoi if non sono nidificati. Inoltre, stai accoppiando visivamente la condizione logica con il suo errore associato, il che rende molto più facile dire cosa sta succedendo:
Freccia:
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;
}
Guardia:
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;
}
Questo è oggettivamente e quantificabilmente più facile da leggere perché
- I caratteri {e} per un dato blocco logico sono più vicini
- La quantità di contesto mentale necessaria per comprendere una determinata linea è minore
- L'intera logica associata a una condizione if ha più probabilità di trovarsi su una pagina
- La necessità per il programmatore di scorrere la pagina / traccia dell'occhio è notevolmente ridotta
Come aggiungere un codice comune alla fine
Il problema con il modello di guardia è che si basa su ciò che viene chiamato "ritorno opportunistico" o "uscita opportunistica". In altre parole, interrompe il modello secondo cui ogni funzione dovrebbe avere esattamente un punto di uscita. Questo è un problema per due motivi:
- Sfrega alcune persone nel modo sbagliato, ad esempio le persone che hanno imparato a programmare su Pascal hanno imparato che una funzione = un punto di uscita.
- Non fornisce una sezione di codice che viene eseguita all'uscita a prescindere da quale sia l'argomento in questione.
Di seguito ho fornito alcune opzioni per aggirare questa limitazione utilizzando le funzionalità della lingua o evitando del tutto il problema.
Opzione 1. Non puoi farlo: usa finally
Sfortunatamente, come sviluppatore di c ++, non puoi farlo. Ma questa è la risposta numero uno per le lingue che contengono finalmente una parola chiave, poiché è esattamente quello che serve.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Opzione 2. Evitare il problema: ristrutturare le funzioni
È possibile evitare il problema suddividendo il codice in due funzioni. Questa soluzione ha il vantaggio di lavorare per qualsiasi lingua e inoltre può ridurre la complessità ciclomatica , che è un modo comprovato per ridurre la percentuale di difetti e migliora la specificità di eventuali test di unità automatizzati.
Ecco un esempio:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Opzione 3. Trucco linguistico: utilizzare un loop falso
Un altro trucco comune che vedo è usare while (true) e break, come mostrato nelle altre risposte.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Anche se questo è meno "onesto" rispetto all'uso goto
, è meno incline ad essere incasinato durante il refactoring, in quanto segna chiaramente i confini dell'ambito logico. Un programmatore ingenuo che taglia e incolla le etichette o le goto
dichiarazioni può causare gravi problemi! (E francamente il modello è così comune ora penso che comunichi chiaramente l'intento, e quindi non sia affatto "disonesto").
Esistono altre varianti di queste opzioni. Ad esempio, si potrebbe usare al switch
posto di while
. Qualsiasi costrutto di linguaggio con una break
parola chiave probabilmente funzionerebbe.
Opzione 4. Sfrutta il ciclo di vita dell'oggetto
Un altro approccio sfrutta il ciclo di vita dell'oggetto. Usa un oggetto di contesto per portare in giro i tuoi parametri (qualcosa che manca in modo sospetto al nostro esempio ingenuo) e smaltirlo quando hai finito.
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
}
Nota: assicurati di comprendere il ciclo di vita dell'oggetto della tua lingua preferita. Hai bisogno di una sorta di garbage collection deterministica perché questo funzioni, cioè devi sapere quando verrà chiamato il distruttore. In alcune lingue dovrai usare Dispose
invece di un distruttore.
Opzione 4.1. Sfrutta il ciclo di vita dell'oggetto (modello di wrapper)
Se hai intenzione di utilizzare un approccio orientato agli oggetti, puoi farlo nel modo giusto. Questa opzione utilizza una classe per "avvolgere" le risorse che richiedono la pulizia, così come le sue altre operazioni.
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
Ancora una volta, assicurati di comprendere il ciclo di vita degli oggetti.
Opzione 5. Trucco linguistico: utilizzare la valutazione del corto circuito
Un'altra tecnica è quella di sfruttare la valutazione del corto circuito .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Questa soluzione sfrutta il modo in cui opera l'operatore &&. Quando il lato sinistro di && viene considerato falso, il lato destro non viene mai valutato.
Questo trucco è molto utile quando è richiesto un codice compatto e quando è probabile che il codice non richieda molta manutenzione, ad esempio si sta implementando un noto algoritmo. Per una codifica più generale la struttura di questo codice è troppo fragile; anche una piccola modifica alla logica potrebbe innescare una riscrittura totale.