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


29

Ho un grande metodo che fa 3 compiti, ognuno di essi può essere estratto in una funzione separata. Se creerò funzioni aggiuntive per ognuna di queste attività, migliorerà o peggiorerà il mio codice e perché?

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

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


19
"Lascio fino a quando tutto è fatto" di solito è sinonimo di "Non sarà mai fatto".
Euforico

2
Questo è generalmente vero, ma ricorda anche il principio opposto di YAGNI (che non si applica in questo caso, dal momento che ne hai già bisogno).
jhocking


Volevo solo enfatizzare non concentrarti molto sulla riduzione delle righe di codice. Cerca invece di pensare in termini di astrazioni. Ogni funzione dovrebbe avere un solo lavoro. Se ritieni che le tue funzioni stiano svolgendo più di un lavoro, in genere dovresti riformattare il metodo. Se segui queste linee guida dovrebbe essere quasi impossibile avere funzioni troppo lunghe.
Adrian,

Risposte:


35

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

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

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

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

Inoltre, una cosa molto importante che ho imparato con il libro di cui sopra: scegli 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 quel modo.

Prima di eseguire il refactor, scrivere uno o più casi di test. Assicurati che funzionino. Una volta terminato il refactoring, sarai in grado di avviare questi casi di test per assicurarti che il nuovo codice funzioni correttamente. È possibile scrivere ulteriori test "più piccoli" per assicurarsi che le nuove funzioni eseguano bene separatamente.

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 ?" (cerca anche le domande SO sul "refactoring", ce ne sono altre e le risposte sono interessanti da leggere)

Devo farlo prima di scrivere tutto il codice o dovrei lasciarlo fino a quando tutto è fatto e quindi estrarre le funzioni?

Se il codice è già presente e funziona e hai poco tempo per la prossima versione, non toccarlo. Altrimenti, penso che uno dovrebbe fare piccole funzioni ogni volta che è possibile e come tale, refactoring ogni volta che è disponibile un po 'di tempo assicurando che tutto funzioni come prima (casi di test).


10
In realtà, Bob Martin ha dimostrato più volte di preferire 7 funzioni con 2 o 3 linee rispetto a una funzione con 15 linee (vedi siti.google.com/site/unclebobconsultingllc/… ). Ed è qui che molti sviluppatori persino esperti resisteranno. Personalmente, penso che molti di quegli "sviluppatori esperti" abbiano solo difficoltà ad accettare che potrebbero ancora migliorare su una cosa così basilare come costruire astrazioni con funzioni dopo> 10 anni di programmazione.
Doc Brown,

+1 solo per fare riferimento a un libro che, per la mia modesta opinione, dovrebbe essere negli scaffali di qualsiasi azienda di software.
Fabio Marcolini,

3
Potrei parafrasare qui, ma una frase di quel libro che riecheggia nella mia testa quasi ogni giorno è "ogni funzione dovrebbe fare solo una cosa e farlo bene". Sembra particolarmente rilevante qui dal momento che l'OP ha detto "la mia funzione principale fa tre cose"
wakjah,

Hai assolutamente ragione!
Jalayn,

Dipende da quanto si intrecciano le tre funzioni separate. Potrebbe essere più semplice seguire un blocco di codice che si trova tutto in un posto rispetto a tre blocchi di codice che si basano ripetutamente l'uno sull'altro.
user253751

13

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

  1. Leggibilità - Le funzioni con nomi validi rendono esplicito ciò che il codice fa senza che sia necessario leggerlo.
  2. Riutilizzabilità: è più facile usare la funzione che fa una cosa in più posti, piuttosto 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 molte

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 una sorta di soluzione OOP.
  • È difficile dire quale funzione debba essere svolta. L'unità lo prova e lo refactoring fino a quando non lo sai.

5

Il problema che stai ponendo non è un problema di codifica, convenzioni o pratiche di codifica, piuttosto un problema di leggibilità e in che modo gli editor di testo mostrano il codice che scrivi. Lo stesso problema appare anche nel post:

È corretto dividere funzioni e metodi lunghi in più piccoli anche se non saranno chiamati da nessun altro?

Dividere una funzione in sotto-funzioni ha senso quando si implementa un grande sistema con l'intento di incapsulare le diverse funzionalità di cui sarà composta. Tuttavia, prima o poi, ti ritroverai con una serie di grandi funzioni. Alcuni di essi sono illeggibili e difficili da mantenere se vengono mantenuti come singole funzioni lunghe o divise in funzioni più piccole. Ciò è particolarmente vero per le funzioni in cui le operazioni eseguite non sono necessarie in nessun altro luogo del sistema. Consente di selezionare una delle funzioni così lunghe e di considerarla in una visione più ampia.

Pro:

  • Una volta letto, hai un'idea completa di tutte le oprazioni che la funzione fa (puoi leggerlo come un libro);
  • Se si desidera eseguire il debug, è possibile 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:

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

Ora immaginiamo di dividere la lunga funzione in diverse sotto-funzioni e guardarle con una prospettiva più ampia.

Pro:

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

Contra:

  • È facile immaginare cosa faccia una funzione come "sin ()", ma non è altrettanto facile immaginare cosa facciano le nostre sotto-funzioni;
  • L'algoritmo è ora scomparso, ora è distribuito in sotto funzioni (nessuna panoramica);
  • Quando si esegue il debug passo dopo passo, è facile dimenticare la chiamata di funzione del livello di profondità da cui si proviene (saltando qua e là nei file di progetto);
  • Puoi facilmente perdere contesto quando leggi le diverse sotto-funzioni;

Entrambe le soluzioni hanno pro e contro. La migliore soluzione effettiva sarebbe avere editor che permettano di espandere, in linea e per tutta la profondità, ogni funzione chiama nel suo contenuto. Ciò renderebbe la suddivisione delle funzioni in funzioni secondarie l'unica soluzione migliore.


2

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

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

  • È un callback : è un gestore di eventi o una sorta di codice utente che una libreria o un framework chiama. (Non riesco quasi a immaginarlo senza creare funzioni.)

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

  • Volete un codice autocompattante : quindi invece di scrivere una riga di commento su un blocco di codice che riassume ciò che fa, estraete tutto in una funzione e lo nominate come scrivere in un commento. Anche se non sono un fan di questo, perché mi piace scrivere il nome dell'algoritmo utilizzato, il motivo per cui ho scelto quell'algoritmo, ecc. I nomi delle funzioni sarebbero troppo lunghi quindi ...


1

Sono sicuro che hai sentito il consiglio che le variabili dovrebbero essere mirate il più strettamente possibile e spero che tu sia d'accordo. Bene, le funzioni sono contenitori di ambito, e nelle 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 vengano inizializzati.

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


0

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


Penso che il motivo delle funzioni di atomizzazione sia duplice, e come menziona @jozefg dipende dal linguaggio usato.

Separazione degli interessi

Il motivo principale per farlo è mantenere separati diversi pezzi di codice, quindi qualsiasi blocco di codice che non contribuisce direttamente al risultato / intento desiderato della funzione è una preoccupazione separata e potrebbe essere estratto.

Supponiamo che tu abbia un'attività in background che aggiorna anche una barra di avanzamento, l'aggiornamento della barra di avanzamento non è direttamente correlato all'attività di lunga durata, quindi dovrebbe essere estratto, anche se è l'unico pezzo di codice che utilizza la barra di avanzamento.

Diciamo che 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 esatta funzione, ma in realtà potrebbe essere suddivisa in 3 funzioni private, incluso solo il codice per 3 e 5 (se quello), poiché nessuno degli altri codici è direttamente responsabile dell'ottenimento dei dati dal servizio .

Esperienza di debug migliorata

Se si dispone di funzioni completamente atomiche, la traccia dello stack diventa un elenco di attività, che elenca tutto il codice eseguito correttamente, ovvero:

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

sarebbe molto più interessante quindi scoprire che si è verificato un errore durante il recupero dei dati. Ma alcuni strumenti sono ancora più utili per il debug di alberi dettagliati delle chiamate, quindi prendiamo ad esempio il Debugger Canvas di Microsofts .

Comprendo 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 delle chiamate sarebbe molto più complesso di quello . Ma se le funzioni sono ben definite (intellisense mi permette di usare 3-4 parole di caso camale in qualsiasi funzione che mi piace senza rallentarmi) e strutturata con l'interfaccia pubblica nella parte superiore del file, il tuo codice verrà letto come pseudo-codice che è di gran lunga il modo più semplice per ottenere una comprensione di alto livello di una base di codice.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.