it-swarm.it

In che modo refactificare le istruzioni IF annidate?

Stavo girando intorno alla blogosfera di programmazione quando mi sono imbattuto in questo post su GOTO:

http://giuliozambon.blogspot.com/2010/12/programmers-tabu.html

Qui lo scrittore parla di come "si deve arrivare alla conclusione che ci sono situazioni in cui i GOTO rendono il codice più leggibile e più gestibile" e poi continua a mostrare un esempio simile a questo:

if (Check#1)
{
    CodeBlock#1
    if (Check#2)
    {
        CodeBlock#2
        if (Check#3)
        {
            CodeBlock#3
            if (Check#4)
            {
                CodeBlock#4
                if (Check#5)
                {
                    CodeBlock#5
                    if (Check#6)
                    {
                        CodeBlock#6
                        if (Check#7)
                        {
                            CodeBlock#7
                        }
                        else
                        {
                            rest - of - the - program
                        }
                    }
                }
            }
        }
    }
}

Lo scrittore propone quindi che l'uso di GOTO faciliterebbe la lettura e la manutenzione di questo codice.

Personalmente posso pensare ad almeno 3 modi diversi per appiattirlo e rendere questo codice più leggibile senza ricorrere a GOTO che interrompono il flusso. Ecco i miei due preferiti.

1 - Funzioni piccole nidificate. Prendi ogni if ​​e il suo blocco di codice e trasformalo in una funzione. Se il controllo booleano fallisce, basta tornare. Se passa, chiama la funzione successiva nella catena. (Ragazzo, sembra molto simile alla ricorsione, potresti farlo in un singolo ciclo con i puntatori a funzione?)

2 - Variabile sentinale. Per me questo è il più semplice. Usa semplicemente una variabile blnContinueProcessing e controlla se è ancora vero nel tuo if check. Quindi se il controllo fallisce, imposta la variabile su false.

In quanti modi diversi questo tipo di problema di codifica può essere modificato per ridurre la nidificazione e aumentare la manutenibilità?

34
saunderl

È davvero difficile dirlo senza sapere come interagiscono i diversi controlli. Il refactoring rigoroso potrebbe essere in ordine. La creazione di una topologia di oggetti che eseguono il blocco corretto in base al loro tipo, potrebbe essere utile anche un modello di strategia o di stato.

Senza sapere cosa fare meglio prenderei in considerazione due possibili semplici refactoring che potrebbero essere ulteriormente rifattorizzati estraendo più metodi.

Il primo che non mi piace davvero dal momento che mi piace sempre come piccoli punti di uscita in un metodo (preferibilmente uno)

if (!Check#1)
{ 
    return;
}
CodeBlock#1

if (!Check#2)
{
    return;
}
CodeBlock#2
...

Il secondo rimuove i ritorni multipli, ma aggiunge anche molto rumore. (rimuove sostanzialmente solo l'annidamento)

bool stillValid = Check#1
if (stillValid)
{
  CodeBlock#1
}

stillValid = stillValid && Check#2
if (stillValid)
{
  CodeBlock#2
}

stillValid = stillValid && Check#3
if (stillValid)
{
  CodeBlock#3
}
...

Quest'ultimo può essere ricondizionato piacevolmente in funzioni e quando dai loro dei bei nomi il risultato potrebbe essere ragionevole ';

bool stillValid = DoCheck1AndCodeBlock1()
stillValid = stillValid && DoCheck2AndCodeBlock2()
stillValid = stillValid && DoCheck3AndCodeBlock3()

public bool DoCheck1AndCodeBlock1()
{
   bool returnValid = Check#1
   if (returnValid)
   {
      CodeBlock#1
   }
   return returnValid
}

Tutto sommato ci sono molto probabilmente opzioni migliori

33
KeesDijk

Questo si chiama "Codice freccia" a causa della forma del codice con rientro corretto.

Jeff Atwood ha pubblicato un buon post sul blog su Coding Horror su come appiattire le frecce:

Appiattimento del codice della freccia

Leggi l'articolo per il trattamento completo, ma qui ci sono i punti principali ..

  • Sostituire le condizioni con clausole di protezione
  • Decomporre i blocchi condizionali in funzioni separate
  • Converti i controlli negativi in ​​controlli positivi
  • Torna sempre opportunisticamente dalla funzione al più presto
40
JohnFx

So che alcune persone sosterranno che è un goto, ma return; è l'evidente refactoring, ad es.

if (!Check#1)
{ 
        return;
}
CodeBlock#1
if (!Check#2)
{
    return;
}
CodeBlock#2
.
.
.
if (Check#7)
{
    CodeBlock#7
}
else
{
    rest - of - the - program
}

Se è davvero solo un mucchio di controlli di guardia prima di eseguire il codice, allora funziona bene. Se è più complicato di così, questo renderà solo un po 'più semplice e avrai bisogno di una delle altre soluzioni.

26
Richard Gadsden

Quel codice spaghetti sembra il candidato perfetto per il refactoring in una macchina statale.

10
Pemdas

Questo può essere già stato menzionato, ma la mia risposta "go-to" (gioco di parole intenzionale) al "antipattern punta di freccia" mostrato qui è di invertire gli if. Prova l'opposto delle condizioni attuali (abbastanza facile con un non operatore) e ritorna dal metodo se questo è vero. Nessun goto (anche se parlando pedanticamente, un ritorno vuoto è poco più che un salto alla linea del codice chiamante, con il banale passaggio extra di far scattare un frame dallo stack).

Caso in questione, ecco l'originale:

if (Check#1)
{
    CodeBlock#1
    if (Check#2)
    {
        CodeBlock#2
        if (Check#3)
        {
            CodeBlock#3
            if (Check#4)
            {
                CodeBlock#4
                if (Check#5)
                {
                    CodeBlock#5
                    if (Check#6)
                    {
                        CodeBlock#6
                        if (Check#7)
                        {
                            CodeBlock#7
                        }
                        else
                        {
                            rest - of - the - program
                        }
                    }
                }
            }
        }
    }
}

Riscritta:

if (!Check#1) return;

CodeBlock#1

if (!Check#2) return;

CodeBlock#2

if (!Check#3) return;

CodeBlock#3

if (!Check#4) return;

CodeBlock#4

if (!Check#5) return;

CodeBlock#5

if (!Check#6) return;

CodeBlock#6

if (Check#7)
    CodeBlock#7
else
{
    //rest of the program
}

Ad ogni se, controlliamo fondamentalmente per vedere se dovremmo continuare. Funziona esattamente allo stesso modo dell'originale con un solo livello di annidamento.

Se c'è qualcosa oltre la fine di questo snippet che dovrebbe anche essere eseguito, estrai questo codice nel suo metodo e chiamalo da dove si trova attualmente questo codice, prima di procedere al codice che verrebbe dopo questo snippet. Lo snippet stesso è abbastanza lungo, dato sufficiente LOC effettivo in ogni blocco di codice, da giustificare la suddivisione di molti altri metodi, ma sto divagando.

6
KeithS

Se hai una logica di routine che in realtà richiede questa piramide di if controlli, probabilmente stai (metaforicamente) usando una chiave per martellare i chiodi. Saresti meglio servito a fare quel tipo di logica contorta e complicata in un linguaggio che supporta quel tipo di logica contorta e complicata con strutture migliori rispetto a lineare if/else if/else stile costrutti.

Le lingue che potrebbero essere più adatte a questo tipo di struttura potrebbero includere SNOBOL4 (con il suo bizzarro branching in stile dual-GOTO) o linguaggi logici come Prolog e Mercury (con le loro capacità di unificazione e backtracking, per non parlare dei DCG per l'espressione piuttosto concisa di decisioni complicate) .

Naturalmente se questa non è un'opzione (perché la maggior parte dei programmatori sono, tragicamente, non poliglotti) le idee che gli altri hanno escogitato sono buone come usare varie OOP - strutture o suddividere le clausole in funzioni o anche, se sei disperato, e non preoccuparti del fatto che la maggior parte delle persone le trova illeggibili, usando un macchina a stati .

La mia soluzione, tuttavia, rimane alla ricerca di un linguaggio che ti permetta di esprimere quale logica stai cercando di esprimere (supponendo che questo sia un luogo comune nel tuo codice) in un modo più facile e più leggibile.

Da qui :

Abbastanza spesso quando guardo un insieme di pac-man if trovo che se ho appena disegnato qualcosa come una tabella di verità di tutte le condizioni coinvolte, posso trovare una strada molto migliore per risolvere il problema.

In questo modo è anche possibile valutare se esiste un metodo migliore, come è possibile scomporlo ulteriormente e (e questo è un grosso problema con questo tipo di codice) se ci sono buchi nella logica.

Fatto ciò, probabilmente puoi suddividerlo in un paio di istruzioni switch e un paio di metodi e salvare il prossimo povero mook che deve passare attraverso il codice un sacco di problemi.

1
Jim G.

Personalmente, mi piace racchiudere queste istruzioni if ​​in funzioni separate che restituiscono un bool se la funzione ha successo.

La struttura si presenta come segue:


if (DoCheck1AndCodeBlock1() && 
    DoCheck2AndCodeBlock2() && 
    DoCheck3AndCodeBlock3()) 
{
   // ... you may perform the final operation here ....
}

L'unico inconveniente è che queste funzioni dovranno generare dati utilizzando attributi passati da variabili di riferimento o membro.

1
gogisan

Utilizzando un OO si avvicina a un modello composito in cui foglia è una condizione semplice e un componente un'unione di elementi semplici rende questo tipo di codice estensibile e adattabile

1
guiman

Se si desidera una possibile soluzione orientata agli oggetti, il codice appare come un esempio canonico per il refactoring utilizzando modello di progettazione della catena di responsabilità .

0
FinnNk

Dividerli in funzioni potrebbe aiutare?

Chiamate la prima funzione e quando completa chiamerà la successiva, la prima cosa che farebbe la funzione è testare il controllo per quella funzione.

0
Toby

Dipende molto dalla dimensione di ciascun blocco di codice e dalla dimensione totale, ma tendo a dividere o con un "metodo di estrazione" diretto su un blocco o spostando le parti incondizionate in metodi separati. Ma si applicano le avvertenze citate da @KeesDijk. Il primo approccio offre una serie di funzioni ognuna simile

if (Check#1)
{
    CodeBlock#1
    NextFunction
}

Che può funzionare bene ma porta a un gonfiamento del codice e al antipattern "metodo usato solo una volta". Quindi generalmente preferisco questo approccio:

if (CheckFunctionOne)
{
    MethodOneWithDescriptiveName
    if (CheckFunctionTwo)
    {
        MethodTwoWithDescriptiveName
        ....

Con un uso appropriato delle variabili private e del passaggio dei parametri può essere fatto. Avere dato un'occhiata alla programmazione alfabetica può aiutare qui.

0
Мסž