Perché dovremmo usare brevi funzioni per segmentare il nostro codice? [duplicare]

18

Ho notato una tendenza crescente nel mondo della programmazione che afferma che è buona norma separare i blocchi di codice nelle proprie funzioni. Ovviamente, se quel blocco di codice è riutilizzabile, dovresti farlo. Quello che non capisco è questa tendenza nell'usare una chiamata di funzione come essenzialmente un commento che si nasconde dietro il codice se il codice non è riutilizzabile. Ecco a cosa serve la ripiegatura del codice.

Personalmente, odio anche leggere il codice in questo modo perché sembra che abbia lo stesso problema dell'istruzione GOTO - diventa codice spaghetti in cui, se sto cercando di seguire il flusso del programma, salgo continuamente in giro e posso t seguire logicamente il codice. È molto più facile per me seguire un codice che è lineare, ma ha un singolo commento sulle sezioni del codice che etichettano ciò che fa. Con la piegatura del codice, questa è essenzialmente la stessa identica cosa, tranne che il codice rimane in un modo lineare. Quando cerco di spiegare questo ai miei colleghi, dicono che i commenti sono malvagi e disordinati - come è un commento su un blocco di codice piegato diverso da una chiamata di funzione che non verrà mai chiamata più di una volta? In che modo l'uso eccessivo delle funzioni è diverso dall'eccesso di commenti? In che modo l'uso frequente delle funzioni è diverso dai problemi con le istruzioni GOTO? Qualcuno può spiegarmi il valore del paradigma di programmazione?

    
posta dallin 04.09.2013 - 03:01
fonte

11 risposte

29

L'organizzazione del codice si basa sulla visualizzazione di informazioni sufficienti per trasmettere una singola idea. Il punto debole è far sì che il tuo codice si abbassi abbastanza che una singola idea possa adattarsi a una singola unità di codice. La tua unità di codice può essere una funzione, una classe, ecc. Questi sono solo strumenti di organizzazione. Come con qualsiasi strumento, può essere usato o utilizzato in modo errato.

Avere una funzione a una riga non ha senso a meno che la funzione non trasmetta un'idea significativa. Avere una grande funzione imperativa che trasmette molte idee è difficile da digerire e riutilizzare. Si tratta di trovare il giusto equilibrio, e anche questo è soggettivo.

    
risposta data 04.09.2013 - 03:24
fonte
21

I've seen an increasing trend in the programming world saying that it is good practice to separate code blocks into their own functions.

Non l'avrei definito un "trend crescente". Mi è stato insegnato che suddividere metodi eccessivamente grandi in metodi più piccoli ha migliorato la leggibilità ... ummm ... quasi 40 anni fa. E mi è stato insegnato l'equivalente in fase di progettazione ... la decomposizione funzionale.

What I do not understand is this trend of using a function call as essentially a comment that you hide your code behind if the code is not reusable. That's what code folding is for.

No. La scomposizione funzionale non riguarda principalmente la creazione di componenti riutilizzabili / funzioni / metodi / qualsiasi cosa. In realtà ciò che è in realtà è rendere il codice base più facile da capire riducendolo in "pezzi di dimensioni mordenti" che lo rendono più facile da capire.

IMO, non puoi ottenere la stessa cosa con la piegatura del codice IDE. Il codice pieghevole in genere non tiene conto degli effetti del codice piegato su un altro codice. Ad esempio:

    int a = 1;
    if (something()) {
        a = a + 1;
    }
    print(a);

Se l'IDE ha deciso di piegare il corpo dell'istruzione if , il programmatore non è in grado di notare che a può cambiare rispetto al suo valore iniziale. Se spetta al programmatore decidere cosa piegare, allora lui / lei deve capire il codice per decidere ... il che rende circolare il processo.

Al contrario, se il codice è stato scritto in questo modo:

   int a = 1;
   a = someMethod(a);
   print(a);

dove

   function someMethod(a):
       return something() ? a + 1 : a;

non hai la stessa "sorpresa".

(Ovviamente questo è un esempio altamente irrealistico ... ma illustra il problema con la piegatura del codice.)

    
risposta data 04.09.2013 - 06:37
fonte
10

Non vedo la necessità di leggere ogni singola riga di codice per capire cosa fa un programma. Se le funzioni sono nominate in modo appropriato, perché anche guardare i contenuti a meno che non ti diano il risultato che ti aspetti?

Un altro vantaggio della scrittura di funzioni più piccole è per il test dell'unità. Scrivi una piccola funzione che supera i test e dimentica i dettagli. La chiave per il debug e la risoluzione dei problemi è revisione e modifica il minor numero possibile di righe di codice.

L'analogia GOTO può spiegare la parte "da saltare" del tuo problema, ma sai esattamente dove una funzione sta per tornare. Questa è un'enorme differenza.

È molto facile abituarsi a molte cose nella programmazione. Progettare con una GUI non ti aiuta quando alla fine devi guardare il codice sottostante e sono completamente persi.

    
risposta data 04.09.2013 - 04:45
fonte
7

La scrittura di brevi funzioni ha alcuni vantaggi diretti e molti benefici indiretti, come spiegato a lungo nel libro di Robert C. Martin Pulisci codice . Memoria spenta:

  • Migliora la leggibilità, perché le funzioni brevi sono più facili da leggere a colpo d'occhio
  • Rende più facile riutilizzare quelle funzioni più piccole in seguito (so che hai eliminato questa possibilità come presupposto, ma ...)

I benefici indiretti sono legioni e possono essere riformulati come "ciò che non si ottiene se non si è in grado di ridefinire una funzione di grandi dimensioni in funzioni più piccole":

  • Riduce al minimo gli effetti collaterali (se hai una funzione lunga, è possibile che una variabile sia modificata in seguito a centinaia di linee - molto difficile da rintracciare nella tua testa)
  • Rende ovvio quali variabili sono utilizzate e per cosa - input, output - il che rende più semplice tenere traccia delle dipendenze
  • Usando nomi di funzioni significativi, incoraggia codice chiaro (se non riesci a riassumere ciò che una funzione fa in poche parole ...)
  • Speriamo che, quando hai ridisegnato la tua grande funzione in piccole funzioni che fanno ben poco, noterai dei modelli comuni, tali che le funzioni che pensavi non potessero essere riutilizzate, possono farlo. O almeno sostituito con algoritmi generici.

Queste ragioni sono ben comprese e puoi leggere tutto su di loro attraverso i libri o le relative risposte. Si sta facendo qualcosa con il secondo paragrafo, che evidenzierò qui:

Le funzioni brevi sono grandi, ma i nostri IDE sono progettati per renderli più difficili!

Hai accennato a questo con la tua menzione del codice pieghevole. Perché è possibile farlo:

SomeLongFunction(doohickey) {
    [+] /// Frobnicate the doohickey
    foreach (d : doohickeys) {
        [+] /// Reticulate the doohickey
    }
}

... ed espandi per vedere il codice attuale, in linea, aprendo il [+] s, ma non possiamo avere questo:

SomeLongFunction(doohickey) {
    splines = FrobnicateTheDoohickey(doohickey);
    foreach (d : doohickeys) {
        ReticulateTheDoohickey(d);
    }
}

... e "espandi" le chiamate di funzione? Potremmo chiedere all'IDE di portarci a quelle definizioni di funzione, ma a seconda di dove si trova, potremmo essere portati a una nuova classe in un nuovo file o in una nuova posizione nello stesso file e perdere il contesto di chiamata! Certo, ci sono strumenti che ti permettono di passare facilmente da un contesto all'altro, ma perché non possiamo visualizzare tutto il codice in linea, come avremmo se usassimo il code-folding? Questo è completamente indietro; Gli IDE dovrebbero aiutarci a scrivere codice pulito rendendo le cose più facili quando lo facciamo, e non incoraggiarci a scrivere codice cattivo introducendo funzionalità che lo fanno, come la piegatura del codice.

Ecco i vantaggi di poter visualizzare il codice in linea (sia che si tratti di piegare o chiamare altre funzioni):

  • Mi dà la possibilità di visualizzare il codice in qualsiasi granularità: posso attraversarlo tra BFS, DFS o qualsiasi combinazione.
  • Con il codice in un contesto, posso facilmente trovare il codice che viene eseguito in precedenza, o più tardi, da ( gasp ) scorrendo verso l'alto o verso il basso
  • Espandendo alcune sezioni piegate (di nuovo, a mia scelta) ho un migliore senso della complessità del codice, quindi è più facile da buttare. Essere limitati a visualizzare una funzione alla volta impedisce questo nascondendo la complessità.

Un altro modo di guardarlo è che è analogo avere monitor più o più grandi - puoi vedere di più alla volta, così puoi capire meglio, anche se spesso non è "necessario", perché un buon codice dovrebbe avere linee corte e funzioni brevi. A proposito, è stato dimostrato che più monitor migliora la produttività . Di molto.

Quindi perché i nostri editor o IDE non consentono questo tipo di espansione delle chiamate di funzioni in linea? ( se ce n'è uno per favore, PER FAVORE fammi sapere! ) Non ne ho idea. È chiaro per me che le funzioni brevi hanno enormi benefici. È anche chiaro per me che essere in grado di visualizzare un sacco di codice insieme è anche vantaggioso. Perché non possiamo avere entrambi?

    
risposta data 04.09.2013 - 10:53
fonte
5

Innanzitutto, come ha detto @Stephen C, questa non è una nuova idea. È vecchio, e il motivo per cui è stato intorno così a lungo è che funziona.

In secondo luogo, la ragione per cui funziona è la stessa ragione per cui la programmazione funzionale sta diventando un argomento scottante: rende il programma più facile da ragionare. Potrebbe rendere più difficile il tracciamento fisico (sembra un'opportunità per uno strumento), ma rende più facili i singoli pezzi.

Prendi una funzione che accetta una dozzina di parametri e restituisce un valore booleano, a destra, saprai che restituirà uno dei due valori. Non è necessario rintracciare, vedere che chiama sedici metodi su 7 oggetti più 4 funzioni di utilità, tutto ciò è irrilevante in quanto restituisce vero o falso.

Ora, potresti chiedere come i valori di ritorno siano associati alla rottura delle funzioni, ma in realtà sono la stessa cosa - una funzione che accetta una dozzina di parametri e restituisce un valore booleano, è solo un altro modo per dire che hai una serie di calcoli che culminano in un unico risultato che viene poi utilizzato altrove. Una funzione vuota è semplicemente una serie di operazioni che si raggruppano logicamente e possono essere trattate come una sola.

Rompere una grande funzione, anche se nessuno dei pezzi è riutilizzabile, significa che puoi tenere a mente i pezzi meglio al livello appropriato di astrazione. Una funzione chiamata F che consta di nient'altro che 10 chiamate a F1, F2 e così via, è difficile ragionare solo mentre stai cercando di capire cosa fa F - che un buon nome si ridurrebbe ad un tempo meglio misurato in frazioni di un secondo. Ma anche una cattiva convenzione di denominazione renderà più semplice ignorare il resto del programma mentre ci si concentra sul passaggio successivo 4 e precedente 6. E, scomposto in passaggi, lo stato di un oggetto viene modificato, i globals sono in corso abusato, o qualsiasi informazione che F1 vuole fornire a F10 viene restituita e quindi mantenuta in una variabile locale ... In tutti i casi sarà più facile tenere traccia di dove le cose sono cambiate in parti più piccole.

    
risposta data 04.09.2013 - 09:08
fonte
2

L'atto di dividere le tue funzioni e i tuoi metodi in quelli più piccoli non è solo per la riutilizzabilità. È una buona pratica di progettazione generale perché incoraggia il codice disaccoppiato. Il codice disaccoppiato è più facile da leggere e gestire.

Che cosa intendo con questo?

Se ho una funzione di 100 linee di codice. Gestisce ciò che accade quando l'utente apporta una modifica a un oggetto dominio. Questa funzione è lunga e fa 100 cose discrete.

Se l'ho diviso in due funzioni, una dentro l'altra, la prima funzione ora sta facendo 51 cose discrete e la seconda funzione sta facendo 50 cose discrete. Tuttavia, la seconda funzione probabilmente sta facendo solo una cosa che può causare effetti collaterali per le altre 50 cose. Se c'è un bug nella funzione principale, posso probabilmente isolarlo sia nella mia nuova chiamata di metodo o in una delle altre 50 righe di codice. In un semplice refactoring, ho ridotto la quantità di codice che ho a disposizione per trovare il mio bug a metà.

Come industria, abbiamo lavorato sempre più duramente per rimuovere cose come variabili globali e dipendenze a livello di sistema dal nostro linguaggio e codice. In una certa misura, possiamo considerare ogni frammento di codice procedurale come il suo ambito globale. Quando viene inquadrato in questo modo, diventa molto facile vedere che desideriamo ridurre le dimensioni di questo ambito il più possibile. Usare piccoli metodi che fanno una cosa aiuta molto questo. Non è solo un problema di leggibilità.

    
risposta data 04.09.2013 - 06:52
fonte
1

Credo che questo approccio sia fatto principalmente per rendere il codice più facile da leggere. E sì, sono d'accordo, è possibile andare troppo lontano lungo questa strada, dove ogni funzione contiene solo 2-3 linee e il risultato è che la funzione principale non è più facile da leggere. Fare questo bene è più un'arte che altro.

La mia guida personale è che ho diviso il codice solo in una funzione separata e (più probabile) monouso se ridurrebbe la lunghezza della funzione principale di 10 o più righe, se le linee che vengono separate sono tutte logicamente parte della stessa attività, e se a quell'attività può essere dato un nome di facile comprensione (perché altrimenti la funzione principale non sarà più leggibile).

    
risposta data 04.09.2013 - 03:19
fonte
1

È più facile distruggere le cose e riutilizzarle al bisogno, piuttosto che leggere e comprendere una cacofonia di micro-idee completamente inutilmente sballate che non verranno mai riutilizzate e rappresentano ben poco per il loro merito .

sbaglia sempre dalla parte della chiarezza, ma quando non aiuta nulla, lascia le cose non riutilizzate all'interno di una funzione. Non hai intenzione di averne bisogno in quello stato.

    
risposta data 04.09.2013 - 05:10
fonte
1

Lavoro con giovani programmatori a cui è stato insegnato a scuola che le funzioni dovrebbero essere brevi ma mancano di esperienza per ragionare correttamente il loro codice.

Spesso faccio esattamente l'opposto: incorporando nei siti di chiamata quelle funzioni o metodi per rendere leggibile il codice.

Sebbene sia vero che il codice ben strutturato dovrebbe solitamente essere conciso, probabilmente non è più grande di uno schermo per una data funzione o metodo che funzioni è importante.

Quello che faccio è seguire le regole degli odori del codice per organizzare il mio codice.

La funzione lunga è un odore di codice ben noto, ma non è l'unico, e certamente non il più importante.

Ad esempio, se prendi a caso blocchi di codice casuali e cambi i metodi come fanno molti programmatori inesperti, scambia solo funzioni lunghe con classe estesa . Ancora peggio, questo può portare a metodi che ricevono parti dei loro dati attraverso parametri di metodo e parti dall'istanza dell'oggetto. Evitare quell'odore ( Campi temporanei ... un modo orientato agli oggetti di programmare con i globali) è molto più importante per me dei metodi brevi.

Anche una funzione o un metodo dovrebbe svolgere alcune attività chiaramente identificabili e solo una di queste attività. Ho visto molto spesso una funzione eseguire una mezza attività e restituire alcuni parametri passati a un'altra funzione poche righe dopo l'altra metà dell'attività (questo potrebbe essere un caso di Clumps di dati odore). O funzione il cui unico compito è chiamarne un altro, senza fare praticamente nulla ( metodo Lazy ). Anche questo è orribile. Di solito, puoi facilmente rilevare che sta esaminando l'uso di variabili locali (se queste variabili locali non sono state inserite nell'istanza dell'oggetto come nel caso precedente, Campo temporaneo odore)

Un altro odore comune è il passaggio di un booleano a una funzione: in molti casi nasconde una funzione che esegue un'attività o un'altra a seconda del booleano fornito. Probabilmente sarebbe meglio dividerlo in due funzioni (io chiamo metodo schizofrenico )

.

Ciò che è interessante in quanto la scelta delle funzioni non è sempre così male all'inizio, ma l'evoluzione del codice nel tempo spesso porta a problemi di cui sopra.

La mia spiegazione del perché accade risiede nella psicologia dei programmatori ed è probabilmente il principale svantaggio di dividere il codice tra molti metodi o funzioni.

Puoi facilmente accecarti con funzioni / metodi e in pratica dimenticare che c'è del codice all'interno di questi metodi. Alcuni programmatori (inesperti) dopo aver creato un metodo lo danno per scontato come se fosse una nuova chiamata di libreria. Non permetteranno a themselve di cambiarlo più di quanto farebbero per alcune chiamate in librerie di terze parti. (lo stesso problema si verifica anche con classi definite dall'utente).

Incredibilmente abbastanza funziona anche il contrario: alcuni programmatori inesperti non creeranno un metodo o una classe logicamente necessari se il codice iniziale è breve (tipico nel caso di curryfying una funzione impostando alcuni parametri sui valori iniziali) !!!

Questo può diventare davvero brutto se non ne sei consapevole e prudente.

Ricapitolando, quello che sto dicendo è che la suddivisione del codice tra molti metodi / funzioni / classi può essere davvero una buona cosa se sai cosa stai facendo, o un vero problema se non lo fai.

Per questo motivo ho messo questo odore piuttosto basso nella mia lista personale.

Peccato che sia spesso vicino alla testa della lista nelle liste degli odori del codice in rete. Immagino sia perché è uno degli odori più facili da rilevare, sia da parte dell'uomo che degli strumenti automatici.

    
risposta data 04.09.2013 - 14:28
fonte
0

Un identificatore ben definito per la funzione deve darti un'ampia conoscenza della funzione senza entrarci. Riutilizzabile o no, una funzione intende seguire il principio divide et conquer e nella maggior parte dei casi un programmatore inizia a progettare / implementare / codificare una funzione non principale e solo allora è necessaria una chiamata.

    
risposta data 04.09.2013 - 06:11
fonte
0

Se il codice è "buon codice" altrimenti, c'è una vera differenza nella cognizione tra l'azione di espandere un blocco di codice con un commento intenzionale da un lato e il seguire il debugger in una funzione ben denominata dall'altro ?

Potresti perdere il contesto quando segui il debugger in una chiamata di funzione - ma se questo fa una differenza significativa per la comprensione della funzione che stai guardando allora indica che il contesto è importante - che implica che lo scopo della funzione chiamata non è chiaro, quindi il codice non è "codice buono". In altre parole, non ti fidi veramente di ciò che la funzione fa dal suo nome, che è un problema separato.

Un blocco di codice potrebbe utilizzare qualsiasi variabile o parametro dalla funzione corrente e non si saprà per certo senza espandere il blocco di codice. Il commento spiegherà cosa fa il blocco, ma i parametri di una chiamata di funzione mostrano anche le dipendenze dal contesto corrente che vengono passati alla funzione. Questo è in realtà più contesto mentre guardi meno codice, che è una buona cosa (supponendo che il codice sia effettivamente buono, ovviamente).

Il contesto in cui stai lavorando può forzare l'uso di un editor non pieghevole, nel qual caso blocchi di codice più grandi possono oscurare la struttura complessiva di una funzione, ad esempio se la funzione si adatterebbe su una singola schermata con tutti i blocchi compressi , ma copre molti schermi con loro espansi.

Una buona via di mezzo sarebbe quella di disporre di IDE / editor che possano "allineare" il codice delle piccole funzioni che vengono chiamate, in modo che si guardi al contesto, anche se tale contesto è distribuito su più file.

    
risposta data 04.09.2013 - 12:07
fonte