Divisione di una funzione / programma molto grande in funzioni più piccole. Efficace?

3

So che i nomi delle funzioni possono essere molto espressivi. E quindi può essere allettante suddividere un programma in funzioni particolari e chiamarle da un grande file di funzioni "supervisionabili".

Tuttavia questo è effettivamente efficace per un programmatore? Poiché ciascuna funzione di solito si basa su un input fornito da una funzione precedente, un valore restituito o una variabile globale o qualsiasi altra cosa usi la lingua, spesso funziona e ha senso nel contesto della funzione precedente. Una modifica a una funzione precedente spesso distruggerebbe ovviamente la funzionalità nella seconda funzione.

Quando un programma è suddiviso in funzioni che sono mescolate in un file di funzioni. L'effetto che i cambiamenti hanno non sono necessariamente molto chiari. Quindi è una buona idea dividere le cose in questo modo?

    
posta Max Travis 31.03.2017 - 19:14
fonte

6 risposte

5

Since each function usually relies on some input provided from a previous function, either a returned value or a global variable or whatever else your language uses - it often only works, and makes sense in context with the previous function.

Anche se preferisco sempre le funzioni compatte e autonome su enormi, seppur estensivi alberi, è vero che i programmi del mondo reale sono raramente belli e ordinati (districati) come vorremmo. Lo stato dell'applicazione (globale o meno) è generalmente inevitabile.

Credo che sia anche vero che ci sono rendimenti decrescenti per dividere le cose in funzioni sempre più piccole. Per un estremo logico, dai un'occhiata al libro Pulisci codice di Robert C. Martin . Non tutti saranno d'accordo con me, ma trovo che molti dei suoi esempi siano in realtà meno comprensibili dopo il suo refactoring. (Forse non scrivo abbastanza Java per apprezzarli?)

Detto ciò, personalmente credo che la funzione sia l'astrazione più potente a nostra disposizione . Certo, le funzioni possono essere usate in modo errato. Non c'è assolutamente nulla che ti impedisca di scrivere codice terribile e aggrovigliato con un mucchio di piccole funzioni, anche senza quelle armi malvagie, le variabili globali. Ma nel complesso, le funzioni sono una forza di bontà e luce.

Quando è praticamente possibile, presti attenzione alla saggezza standard della folla nella creazione di funzioni "pulite":

  • Non modificare nessuno stato esterno all'interno delle tue funzioni.
  • Una funzione dovrebbe essere deterministica: lo stesso input deve sempre produrre lo stesso output.

Solo queste due regole eviteranno il 90% dei mal di testa più comuni del design. Permette anche di scrivere facilmente test per le tue funzioni!

Oltre a ciò, prova a fare del tuo meglio solo le funzioni "pulite" quando puoi. A seconda dell'applicazione, questo può essere in realtà un esercizio molto difficile all'inizio. Ma uno migliora. Come per molte altre cose, è un vero mestiere e richiede esperienza (esperienza di programmazione in generale e esperienza con quel progetto, in particolare) per farlo nel modo giusto.

Il codice districato non si verifica solo. È un lavoro duro e un'arte ... e ne vale la pena.

    
risposta data 31.03.2017 - 20:05
fonte
5

Come qualcuno che di recente ha fatto proprio questo, ha rifattorizzato alcuni lunghi fogli di codice in un insieme di piccole funzioni, posso attestare che è efficace.

Immagino tu abbia sentito parlare del principio Unix: "fai una cosa e fallo bene". Aiuta immensamente.

Una funzione breve è facile da osservare, facile da ragionare e facile da testare. Ciò che è importante, di solito è facile da testare in assenza della maggior parte delle altre funzioni, e mentre si prende in giro solo pochi oggetti, se ce ne sono, dipende da.

Quando scrivi una funzione breve, sei costretta a pensare a cosa esattamente fa, a trovare un nome appropriato (doIt () non va bene), nomi di parametri validi, ecc. Rende il tuo codice più auto-descrittivo, e ti fa capire meglio il codice. Fornisce anche concetti di livello superiore per descrivere il tuo programma con.

La suddivisione di transizioni di stato complesse in brevi funzioni ti fa pensare al flusso di dati del programma e lo districa il più possibile. Ciò porta a un minor numero di dipendenze tra i dati, spesso a vite più brevi di alcuni dati (importante quando i dati sono enormi) e meno errori come razze di dati, aggiornamenti di stato che si scontrano l'un l'altro, ecc.

Anche la realizzazione di un frammento di codice più grande di una schermata in una funzione ti fa notare e calcola il codice incollato sulla copia, anche leggermente modificato. Trovare il modello comune aiuta a capire.

A volte ti ritrovi con funzioni che riconosci immediatamente, perché fanno (quasi) la stessa cosa che fanno già alcune funzioni di libreria. Riduci il conteggio delle righe e riutilizza il lavoro esistente di qualcun altro (e eventuali miglioramenti e correzioni di bug imminenti).

Al contrario, le funzioni estratte possono essere riutilizzate negli altri programmi. Non puoi riutilizzare un frammento di codice monolitico, tranne incollandolo per copia.

Molte persone hanno difficoltà a scrivere brevi funzioni, però. Tendono a scrivere una lunga sceneggiatura monolitica che fa il tutto, poiché scriverebbero un intero capitolo di prosa. Uno dei rimedi non è solo scrivere codice, ma eseguirlo immediatamente.

Se hai un linguaggio dinamico come Python o JS, o anche cose come Scala, hai sempre un REPL aperto. Quando trovi un frammento di codice, provalo nella REPL. Avrai bisogno di oggetti dipendenti per questo? Scrivi una funzione che fornisce ciascun oggetto dipendente. Una volta che hai trovato un frammento di codice che produce qualcosa di significativo, avvolgilo anche in una funzione. Ne avrai bisogno al prossimo passaggio. In questo modo si finisce con una serie di funzioni che compongono l'intero programma di cui si aveva bisogno, e ne avete già eseguito la maggior parte!

Con le lingue che non forniscono prontamente REPL, le cose sono più difficili. Qui la 'progettazione basata sui test' può aiutare: non puoi giocare liberamente con i tuoi frammenti di codice in un REPL, ma puoi eseguirlo a basso costo come test. Mentre vai, finisci con l'intero programma e con una serie di test che lo verificano e lo spiegano. Con un monolite, questo è impossibile.

Se il tuo programma è uno script usa e getta, non abbastanza importante da essere sottoposto a peer-review prima di essere in produzione, un muro di codice può andare bene.

Altrimenti, il factoring del tuo codice in parti più piccole paga ogni volta che devi estendere, modificare, rivedere le modifiche di qualcun altro o semplicemente consultarlo.

    
risposta data 31.03.2017 - 20:48
fonte
5

Sì, questa è generalmente una buona idea. Le funzioni più piccole sono solitamente più facili da leggere e comprendere e sono potenzialmente riutilizzabili in diversi contesti. È vero che il processo di refactoring a volte può rompere alcune cose, ma se lo fai correttamente (e aggiusta le cose mentre si rompono e usano i test unitari per aiutare a rilevare le cose che si rompono), il risultato finale di solito vale la pena.

    
risposta data 31.03.2017 - 19:19
fonte
4

Since each function usually relies on some input provided from a previous function, either a returned value or a global variable or whatever else your language uses - it often only works, and makes sense in context with the previous function.

^ Questa affermazione è vera anche se lasci il tuo codice come un'unica grande funzione! Invece di piccole funzioni che dipendono l'una dall'altra, hai linee di codice all'interno di una grande funzione a seconda l'una dell'altra. Questo è molto peggio perché non c'è modo di indicare quali variabili si applicano a quale codice in quale fase dell'esecuzione; tutto in ambito locale è disponibile per tutto ciò che riguarda la funzione.

Se dovessi scomporlo in una serie di funzioni più piccole, puoi definire vincoli su quali funzioni funzionano con quali dipendenze, dichiarandole come parametri e sfruttando l'ambito locale di ciascuna funzione. Così puoi rompere un problema più grande in una serie di problemi più piccoli che (1) hanno chiare interruzioni tra di loro, rendendo ognuno più facile da capire, e (2) hanno una chiara relazione, definita nel codice, che ti dice come il problema è suddiviso in sottoproblemi.

Questo è di gran lunga preferibile a funzioni lunghe che coprono molte pagine che richiedono al programmatore di scorrere su e giù per capire come funziona. Dopo anni di programmazione, posso dirti per esperienza che quelle sono le funzioni che tendono ad essere piene dei bug più cattivi.

    
risposta data 01.04.2017 - 00:11
fonte
2

it often only works, and makes sense in context with the previous function. A change to a previous function would often obviously destroy functionality in the second function.

Se questo è il caso, le tue funzioni sono mal progettate. Non hanno responsabilità o contratti chiari e non rappresentano buone astrazioni.

Le funzioni ben progettate fanno una cosa che è concettualmente facile da capire dal nome della funzione senza conoscere i dettagli di implementazione, ed è per lo più isolata dal resto dell'applicazione.

Scoprirai che se hai delle funzioni che sono vere astrazioni della funzionalità che implementano, saranno molto più spesso riutilizzabili in contesti diversi, e allo stesso tempo meno probabilità di avere effetti collaterali indesiderati quando vengono cambiate .

Con una funzione mal progettata, ogni cambiamento che fai ti preoccupa di rompere le cose in altri 5 posti da cui è chiamato. Con una funzione ben progettata, qualsiasi modifica apportata ad essa sarà qualcosa (come un bugfix) che vuoi applicare agli altri 5 posti da cui è chiamato.

    
risposta data 31.03.2017 - 20:43
fonte
1

However is this actually effective for a programmer?

I linguaggi di programmazione funzionale fanno tutto con le funzioni. Anche un linguaggio con scarso supporto per la programmazione funzionale dovrebbe essere in grado di avvicinarsi. Quindi sì.

A change to a previous function would often obviously destroy functionality in the second function.

Quindi non farlo. Lascia la vecchia funzione lì, e creane una nuova, possibilmente usando la vecchia funzione per fare la maggior parte del lavoro e semplicemente modificando l'output.

La filosofia generale per i linguaggi di programmazione funzionale è come costruire un vocabolario di parole gergali e poi scrivere qualcosa con loro. E come prendi le parole gergali? Bene, tu scrivi le definizioni in termini di altre parole che hai già. Invece di considerarlo come un gigantesco pezzo di codice esistente che deve essere suddiviso in luoghi casuali e inserito in funzioni, pensatelo come creando un vocabolario di funzioni che rende più semplice la scrittura della funzione di livello superiore.

    
risposta data 01.04.2017 - 06:23
fonte