Devo estrarre funzionalità specifiche in una funzione e perché?

28

Ho un grande metodo che svolge 3 compiti, ognuno dei quali può essere estratto in una funzione separata. Se creerò funzioni aggiuntive per ciascuna di queste attività, renderà il mio codice migliore o peggiore e perché?

Ovviamente, renderà meno linee di codice nella funzione principale, ma ci saranno ulteriori dichiarazioni di funzioni, quindi la mia classe avrà metodi aggiuntivi, che credo non siano buoni, perché renderà la classe più complesso.

Dovrei farlo prima di aver scritto tutto il codice o dovrei lasciarlo fino a quando non è stato fatto tutto e quindi estrarre le funzioni?

    
posta dhblah 01.10.2012 - 09:46
fonte

6 risposte

34

Questo è un libro a cui mi collego spesso, ma eccomi di nuovo: Codice pulito di Robert C. Martin , capitolo 3, "Funzioni".

Obviously, it'll make less lines of code in the main function, but there'll be additional function declarations, so my class will have additional methods, which I believe isn't good, because it'll make the class more complex.

Preferisci leggere una funzione con +150 linee o una funzione che chiama 3 +50 funzioni di linea? Penso di preferire la seconda opzione.

, renderà il tuo codice migliore nel senso che sarà più "leggibile". Crea funzioni che eseguano una sola ed unica cosa, saranno più facili da mantenere e produrre un caso di test per.

Inoltre, una cosa molto importante che ho imparato con il libro di cui sopra: scegliere nomi validi e precisi per le tue funzioni. Più importante è la funzione, più preciso dovrebbe essere il nome. Non preoccuparti della lunghezza del nome, se deve essere chiamato FunctionThatDoesThisOneParticularThingOnly , quindi chiamalo in questo modo.

Prima di eseguire il refactoring, scrivi uno o più test case. Assicurati che funzionino. Una volta che hai finito con il tuo refactoring, sarai in grado di lanciare questi casi di test per assicurarti che il nuovo codice funzioni correttamente. Puoi scrivere ulteriori test "più piccoli" per assicurarti che le tue nuove funzioni funzionino in modo uniforme.

Infine, e questo non è contrario a quello che ho appena scritto, chiediti se hai davvero bisogno di fare questo refactoring, controlla le risposte a " Quando refactoring ?" (inoltre, cerca le domande SO su "refactoring", ci sono altre e le risposte sono interessanti da leggere)

Should I do that before I write all the code or should I leave it until everything is done and then extract functions?

Se il codice è già lì e funziona e hai poco tempo per la prossima versione, non toccarlo. Altrimenti, penso che si dovrebbero fare piccole funzioni quando possibile e come tali, refactator ogni volta che è disponibile un certo tempo, assicurandosi che tutto funzioni come prima (casi di test).

    
risposta data 01.10.2012 - 10:10
fonte
12

Sì, ovviamente. Se è facile vedere e separare i diversi "compiti" della singola funzione.

  1. Leggibilità - Le funzioni con un buon nome rendono esplicito quale codice non richiede la lettura di quel codice.
  2. Riusabilità - È più facile usare la funzione che fa una cosa in più punti, che avere una funzione che fa cose che non ti servono.
  3. Testabilità - È più facile testare la funzione, che ha una "funzione" definita, quella che ne ha molti

Ma potrebbero esserci problemi con questo:

  • Non è facile vedere come separare la funzione. Ciò potrebbe richiedere prima il refactoring dell'interno della funzione, prima di passare alla separazione.
  • La funzione ha un enorme stato interno, che viene passato in giro. Questo di solito richiede qualche tipo di soluzione OOP.
  • È difficile dire quale funzione dovrebbe svolgere. Unitalo e testalo fino a quando non lo sai.
risposta data 01.10.2012 - 10:03
fonte
5

Il problema che stai ponendo non è un problema di codifica, convenzioni o pratica di codifica, piuttosto, un problema di leggibilità e modi in cui gli editor di testo mostrano il codice che scrivi. Questo stesso problema è presente anche nel post:

È OK dividere le funzioni e i metodi lunghi in quelli più piccoli anche se non verranno chiamati da altro?

La divisione di una funzione in sotto-funzioni ha senso quando si implementa un grande sistema con l'intento di incapsulare le diverse funzionalità di cui sarà composto. Nondimeno, prima o poi, ti troverai con una serie di grandi funzioni. Alcuni di essi sono illeggibili e difficili da mantenere se si mantengono come singole funzioni lunghe o se le si suddividono in funzioni più piccole. Ciò è particolarmente vero per le funzioni in cui le operazioni eseguite non sono necessarie in nessun altro punto del sistema. Consente di eseguire il ritiro di una funzione così lunga e considerarla in una visualizzazione più ampia.

Pro:

  • Una volta letto, hai un'idea completa su tutte le oprazioni della funzione (puoi leggerla come un libro);
  • Se vuoi eseguire il debug, puoi eseguirlo passo dopo passo senza saltare a nessun altro file / parte del file;
  • Hai la libertà di accedere / utilizzare qualsiasi variabile dichiarata in qualsiasi fase della funzione;
  • L'algoritmo che la funzione implementa è completamente contenuto nella funzione (incapsulato);

Contra:

  • Occorrono molte pagine del tuo schermo;
  • Ci vuole molto tempo per leggerlo;
  • Non è facile memorizzare tutti i diversi passaggi;

Ora immaginiamo di suddividere la funzione lunga in diverse sottofunzioni e guardarle con una prospettiva più ampia.

Pro:

  • Ad eccezione delle funzioni di congedo, ciascuna funzione descrive con parole (nomi di sotto-funzioni) i diversi passaggi eseguiti;
  • Ci vuole molto poco tempo per leggere ogni singola funzione / sotto-funzione;
  • È chiaro quali sono i parametri e le variabili influenzati da ciascuna sotto-funzione (separazione delle preoccupazioni);

Contra:

  • È facile immaginare che funzioni abbia una funzione come "sin ()", ma non è così facile immaginare cosa fanno le nostre funzioni secondarie;
  • L'algoritmo ora è scomparso, ora è distribuito in sotto-funzioni di maggio (nessuna panoramica);
  • Quando esegui il debug di un passo dopo l'altro, è facile dimenticare la chiamata della funzione di livello di profondità da cui vieni (saltando qua e là nei file di progetto);
  • È possibile perdere facilmente il contesto durante la lettura delle diverse sottofunzioni;

Entrambe le soluzioni hanno pro e contro. La soluzione migliore sarebbe disporre di editor che consentano di espandere, in linea e per la profondità, ogni chiamata di funzione nel suo contenuto. Il che farebbe, dividere le funzioni nelle sotto funzioni l'unica soluzione migliore.

    
risposta data 12.08.2013 - 19:58
fonte
2

Per me ci sono quattro motivi per estrarre i blocchi di codice in funzioni:

  • Lo stai riutilizzando : hai appena copiato un blocco di codice negli appunti. Invece di incollarlo, inserirlo in una funzione e sostituire il blocco con una chiamata di funzione su entrambi i lati. Quindi, ogni volta che è necessario modificare quel blocco di codice, è sufficiente modificare quella singola funzione invece di modificare il codice in più posizioni. Pertanto, ogni volta che copi un blocco di codice, devi creare una funzione.

  • È una richiamata : è un gestore di eventi o un tipo di codice utente una libreria o una chiamata di framework. (Non riesco a immaginarmelo senza fare funzioni.)

  • Pensi che verrà riutilizzato , nel progetto corrente o forse da qualche altra parte: hai appena scritto un blocco che calcola una sottosezione comune più lunga di due array. Anche se il tuo programma chiama questa funzione solo una volta, credo che avrò bisogno di questa funzione anche in altri progetti.

  • Vuoi codice di autocertificazione : così invece di scrivere una riga di commento su un blocco di codice che riepiloga ciò che fa, estrai l'intera cosa in una funzione e assegnagli il nome cosa scriveresti in un commento. Anche se non ne sono un fan, perché mi piace scrivere il nome dell'algoritmo usato, il motivo per cui ho scelto quell'algoritmo, ecc. I nomi delle funzioni sarebbero troppo lunghi allora ...

risposta data 21.06.2014 - 13:21
fonte
1

Sono sicuro che hai sentito il consiglio che le variabili dovrebbero essere esaminate il più strettamente possibile, e spero che tu sia d'accordo. Bene, le funzioni sono contenitori di scope e in funzioni più piccole l'ambito delle variabili locali è più piccolo. È molto più chiaro come e quando dovrebbero essere usati ed è più difficile usarli nell'ordine sbagliato o prima che siano inizializzati.

Inoltre, le funzioni sono contenitori di flusso logico. C'è solo un modo per entrare, le vie d'uscita sono chiaramente indicate e se la funzione è abbastanza breve, i flussi interni dovrebbero essere ovvi. Questo ha l'effetto di ridurre la complessità ciclomatica che è un modo affidabile per ridurre il tasso di difetti.

    
risposta data 14.02.2014 - 03:17
fonte
0

A parte: ho scritto questo in risposta a dallin domanda (ora chiusa) ma sento ancora che potrebbe essere utile a qualcuno quindi ecco qui

Penso che la ragione delle funzioni di atomizzazione sia di 2 volte, e come menzioni @jozefg dipende dal linguaggio usato.

Separazione delle preoccupazioni

La ragione principale per fare ciò è tenere separati pezzi di codice diversi, quindi qualsiasi blocco di codice che non direttamente contribuisce al risultato / intento desiderato della funzione è un problema separato e potrebbe essere estratto.

Supponiamo che tu abbia un'attività in background che aggiorni anche una barra di avanzamento, che l'aggiornamento della barra di avanzamento non sia direttamente correlato all'attività a lungo termine, quindi dovrebbe essere estratto, anche se è l'unica parte di codice che utilizza la barra di avanzamento. / p>

Dire in JavaScript hai una funzione getMyData (), che 1) crea un messaggio soap dai parametri, 2) inizializza un riferimento al servizio, 3) chiama il servizio con il messaggio soap, 4) analizza il risultato, 5) restituisce il risultato. Sembra ragionevole, ho scritto molte volte questa funzione esatta, ma in realtà potrebbe essere diviso in 3 funzioni private, incluso solo il codice per 3 & 5 (se questo) poiché nessun altro codice è direttamente responsabile per ottenere i dati dal servizio.

Esperienza di debug migliorata

Se hai funzioni completamente atomiche, la tua traccia dello stack diventa un elenco di attività, elencando tutto il codice eseguito con successo, cioè:

  • Ottieni i miei dati
    • Crea messaggio di sapone
    • Inizializza il riferimento al servizio
    • Risposta del servizio analizzata - ERRORE

potrebbe essere più interessante quindi scoprire che si è verificato un errore durante il recupero dei dati. Ma alcuni strumenti sono ancora più utili per eseguire il debug di alberi di chiamata dettagliati, ad esempio Microsofts Debugger Canvas .

Capisco anche le tue preoccupazioni sul fatto che può essere difficile seguire il codice scritto in questo modo perché alla fine della giornata, devi scegliere un ordine di funzioni in un singolo file in cui il tuo albero di chiamata sarebbe più disponibile complesso allora. Ma se le funzioni si chiamano bene (intellisense mi permette di usare 3-4 parole di caso camale in qualsiasi funzione per favore senza rallentarmi) e strutturato con interfaccia pubblica nella parte superiore del file, il tuo codice leggerà come pseudo-codice che è di gran lunga il modo più semplice per ottenere una comprensione di alto livello di un codebase.

FYI - questa è una di quelle cose "fai come dico non come faccio io", mantenere il codice atomico è inutile a meno che tu non sia spietatamente coerente con esso. IMHO, che non sono.

    
risposta data 04.09.2013 - 15:27
fonte

Leggi altre domande sui tag