Quale è più leggibile: ritorni anticipati o condizionali? [duplicare]

0

Sto scrivendo una funzione asincrona, Promise -returning. Durante l'elaborazione, cerco alcune condizioni e, se le condizioni passano, la promessa dovrebbe essere soddisfatta (risolta), ma se uno fallisce, la promessa dovrebbe essere respinta.

Voglio sapere qual è lo stile di codifica preferito, o che è più leggibile, quando scrivi promesse:

function earlyReturns(data) {
  return new Promise(function (resolve, reject) {
    if (/* something fails */) return reject(new Error('error type 1'))
    /* do some processing 1 */
    /* do some processing 2 */
    if (/* something else fails */) return reject(new Error('error type 2'))
    /* do some processing 3 */
    return resolve(processing_result)
  })
}
function conditionals(data) {
  return new Promise(function (resolve, reject) {
    if (/* something fails */) reject(new Error('error type 1'))
    else {
      /* do some processing 1 */
      /* do some processing 2 */
      if (/* something else fails */) reject(new Error('error type 2'))
      else {
        /* do some processing 3 */
        resolve(processing_result)
      }
    }
    return;
  })
}

Da una parte, la funzione earlyReturns è più succinta, ma d'altra parte, conditionals ha solo un punto di uscita . (Inoltre, return reject() e return resolve() sono fuorvianti perché il valore restituito è in realtà undefined .)

    
posta chharvey 11.02.2018 - 02:08
fonte

3 risposte

3

Dichiarazione di non responsabilità: questa è basata sull'opinione, basata sui miei sentimenti riguardo alla leggibilità.

Generalmente preferisco lo stile Early-return per cose come l'abortire un processo a causa di alcuni motivi di rifiuto, o per la gestione anticipata di casi speciali (e che si adatta alla descrizione del problema.) Per me il complicato if-then-else nidifica il codice più difficile da capire.

Ma ci sono alcune cose che cambierei per motivi di leggibilità:

  • Non utilizzerei return xy , se xy non è destinato a diventare un risultato di funzione utile. Si legge "Voglio restituire il valore di xy come risultato della mia funzione". Lo cambierei per diventare due dichiarazioni, prima eseguendo xy , e poi tornando.
  • renderei i primi ritorni più importanti, non nascondendoli da qualche parte all'interno di una lunga riga di codice sorgente, ma ponendoli su una linea a parte. Per me, i rendimenti iniziali migliorano solo la leggibilità su if annidati, se sono chiaramente visibili al lettore.
  • Includo sempre i blocchi "then" o "else" in parentesi graffe. Ho visto troppi bug da "solo aggiungendo un'altra istruzione in questo blocco" e dimenticando che questo funziona solo con parentesi graffe sul posto ...
risposta data 11.02.2018 - 12:02
fonte
1

L'importante è mantenere una struttura chiara.

Una semplice istruzione switch / case (o una serie di istruzioni if non annidate) che contiene un ritorno all'interno di ogni caso, sarà perfettamente chiara. Cioè, la mia prima preferenza sarebbe per i ritorni anticipati. Pensa a questi ritorni anticipati come se fossero come una dichiarazione GOTO .

Ma se si dispone di un nesso di logica leggermente complesso, in cui i ritorni potrebbero dover essere sparsi ovunque a diversi livelli, passerei all'utilizzo di una variabile intermedia e di un approccio a un solo ritorno.

Ai miei occhi, questo approccio sembra sempre un po 'disordinato a prima vista, ma serve a reimpiantare il flusso di controllo strutturato - poiché ora dobbiamo incorporare una logica strutturata che garantisce di arrivare alla fine della funzione senza eseguire nient'altro () le parti che sarebbero state saltate fuori più e più volte, se la funzione fosse tornata in anticipo).

Se hai deciso che i ritorni anticipati non sono buoni, ma diventa anche troppo difficile o irragionevolmente disordinato per implementare l'approccio a rendimento unico, a causa della difficoltà di arrivare alla fine della funzione (senza utilizzare GOTO s), questo è un segno sicuro che l'intera cosa è strutturata in maniera intrinsecamente scarsa e deve essere ulteriormente suddivisa in metodi separati.

Una volta che abbiamo suddiviso la logica eccessivamente complessa in metodi separati, potremmo essere in grado di tornare tranquillamente a utilizzare i ritorni anticipati in ciascuno di essi.

Se guardi ai ritorni anticipati come questo - che sono simili alle dichiarazioni di GOTO - vuoi chiederti quanti livelli di (e di quale complessità di) codice strutturato vengono tagliati attraverso il loro uso. Se stai tagliando attraverso un livello (ad esempio uscendo da un ciclo o da un interruttore che è l'istruzione esterna del metodo, con l'ambito interno contenente una o due righe), ciò è accettabile.

Forse è accettabile anche se ci sono un paio di cicli o interruttori annidati, se la logica all'interno della struttura esterna è banale - per esempio, se un ciclo è annidato all'interno dell'altro per attraversare un singolo array bidimensionale, o un interruttore annidato che valuta semplicemente due variabili, prima di raggiungere l'ambito interno contenente una o due istruzioni per ogni caso (o forse solo l'istruzione return).

Ma se stai tagliando la struttura del flusso di controllo che è di diversi livelli in profondità, contenente tutti i tipi di logica non banale a ogni livello, cioè quando il ritorno anticipato può essere visto come il superamento della struttura. Il comportamento del codice non strutturato è molto più difficile da ragionare e verificare, e anche se scritto correttamente, in primo luogo diventa molto difficile da riprendere o modificare in futuro (anche per la persona che lo ha scritto, ma soprattutto per gli altri) .

Come nota finale, se la funzione ha una "coda" di codice comune, come potrebbe essere richiesta per la registrazione, controlli di integrità, o codice di pulizia, o come posizione comoda per impostare un punto di interruzione del debugger - o se si potrebbe voglio che la funzione aggiunga questi in futuro senza codice di rilavorazione radicale che è già testato in uso - quindi i ritorni anticipati sono preclusi in ogni caso, e dovrai usare variabili intermedie e un singolo ritorno come inizio punto. Gli sviluppatori che lavorano in un ambiente in cui questi requisiti sono usuali, possono semplicemente utilizzare le variabili intermedie abitualmente.

    
risposta data 11.02.2018 - 15:45
fonte
0

Questo è abbastanza basato sull'opinione, ma avere un solo punto di uscita ha i suoi vantaggi.

La ragione principale per evitare i condizionali multipli è evitare un ziqqurat di if annidati, che può anche essere difficile da riprogettare, specialmente se la logica del programma è una serie di controlli che devono essere fatti e potrebbe aver bisogno di essere interrotto.

Inoltre, a volte durante l'elaborazione fai qualcosa che avresti bisogno di annulla in caso di exit (lavoro in C e questo mi succede molto: allocare memoria per qualche scopo, quindi deallocare Non posso sempre farlo nella funzione chiamata). La nidificazione dovrebbe quindi garantire una pulizia al massimo dell'efficienza, mentre la strategia di ritorno anticipato ti costringerebbe a fare qualcosa del tipo

undo_block_1;
return;

do_block_2;
if (error) {
   undo_block_2;
   undo_block_1;
   return;
}

do_block_3;
if (error) {
   undo_block_3;
   undo_block_2;
   undo_block_1;
   return;
}

Ma è possibile modificare quei if e avere ancora un ragionevole compromesso succinto, con la maggior parte dei vantaggi del nesting:

function earlyReturns(data) {
  return new Promise(function (resolve, reject) {
    var sofar = 'OK';
    if (('OK' === sofar) && /* something fails */) {
        sofar = reject(new Error('error type 1'));
    }
    if ('OK' === sofar) {
        /* do some processing 1 */
        /* do some processing 2 */
    }
    if ('OK' === sofar && /* something else fails */) {
        sofar = reject(new Error('error type 2'));
    }
    // Here you might undo something done in processing 2
    if ('OK' === sofar) {
         /* do some processing 3 */
    }
    if ('OK' === sofar) {
        sofar = resolve(processing_result);
    }
    return sofar;
  })
}

A volte uso (ma quello in C) una macro che si riduce a ('OK' === sofar) in produzione, ma si comporta in sviluppo come una debole forma di asserzione e registra il nome della funzione, il valore sofar e un messaggio opzionale prima di restituire il OK di sofar, dando qualcosa come

funcSomething sanity check OK
funcSomething matrix is Jordan canonical OK
funcSomething eigenvalues within bounds CHECK FAILED
funcSomething stage 1 CHECK FAILED
funcSomething stage 2 CHECK FAILED
funcSomething returning CHECK FAILED
    
risposta data 11.02.2018 - 02:56
fonte

Leggi altre domande sui tag