Le funzioni lunghe sono accettabili se hanno una struttura interna?


23

Quando ho a che fare con algoritmi complicati in linguaggi con supporto per funzioni nidificate (come Python e D) scrivo spesso enormi funzioni (perché l'algoritmo è complicato) ma mitigalo usando funzioni nidificate per strutturare il codice complicato. Le funzioni enormi (più di 100 linee) sono ancora considerate malvagie anche se sono ben strutturate internamente tramite l'uso di funzioni nidificate?

Modifica: per coloro che non conoscono Python o D, le funzioni nidificate in queste lingue consentono anche l'accesso all'ambito delle funzioni esterne. In D questo accesso consente la mutazione delle variabili nell'ambito esterno. In Python consente solo la lettura. In D è possibile disabilitare esplicitamente l'accesso all'ambito esterno in una funzione nidificata dichiarandolo static.


5
Scrivo regolarmente più di 400 funzioni / metodi di linea. Devo mettere da qualche parte quella dichiarazione del cambio di caso 500 :)
Callum Rogers,

7
@Callum Rogers: in Python, invece di avere 500 istruzioni switch case, useresti un dizionario / mappatura di ricerca. Credo che siano superiori all'affermazione di circa 500 casi. Hai ancora bisogno di 500 righe di definizioni di dizionario (in alternativa, in Python, puoi usare la riflessione per enumerarle dinamicamente), ma la definizione di dizionario è data, non codice; e ci sono molti meno scrupoli nell'avere una definizione di dati di grandi dimensioni rispetto alla definizione di funzioni di grandi dimensioni. Inoltre, i dati sono più affidabili del codice.
Lie Ryan,

Ogni volta che vedo le funzioni con un sacco di LOC mi faccio rabbrividire, mi chiedo quale sia lo scopo della modularità. È più facile seguire la logica e eseguire il debug con funzioni più piccole.
Aditya P,

L'antico e familiare linguaggio Pascal consente anche l'accesso all'ambito esterno, così come l'estensione delle funzioni nidificate in GNU C. tramandato e non restituito, il che richiederebbe il pieno supporto della chiusura lessicale).
Kaz,

Un buon esempio di funzioni che tendono ad essere lunghe sono i combinatori che scrivi durante la programmazione reattiva. Colpiscono facilmente trenta linee, ma raddoppiano se divise perché perdi chiusure.
Craig Gidney,

Risposte:


21

Ricorda sempre la regola, una funzione fa una cosa e la fa bene! In tal caso, evitare le funzioni nidificate.

Ostacola la leggibilità e i test.


9
Ho sempre odiato questa regola perché una funzione che fa "una cosa" se vista ad alto livello di astrazione può fare "molte cose" se vista ad un livello inferiore. Se applicata in modo errato, la regola "una cosa" può portare a API eccessivamente dettagliate.
dsimcha,

1
@dsimcha: quale regola non può essere applicata erroneamente e portare a problemi? Quando qualcuno sta applicando "regole" / banalità oltre il punto in cui sono utili, tirane fuori un altro: tutte le generalizzazioni sono false.

2
@Roger: un reclamo legittimo, tranne per il fatto che IMHO la regola spesso viene applicata in modo errato nella pratica per giustificare API eccessivamente raffinate e ingegnerizzate. Ciò ha costi concreti in quanto l'API diventa più difficile e più dettagliata da utilizzare per casi d'uso semplici e comuni.
dsimcha,

2
"Sì, la regola viene applicata in modo errato, ma la regola viene applicata in modo eccessivo!"? : P

1
La mia funzione fa una cosa e la fa molto bene: vale a dire, occupa 27 schermate. :)
Kaz,

12

Alcuni hanno sostenuto che le funzioni brevi possono essere più soggette ad errori rispetto alle funzioni lunghe .

Card and Glass (1990) sottolineano che la complessità del design coinvolge davvero due aspetti: la complessità all'interno di ciascun componente e la complessità delle relazioni tra i componenti.

Personalmente, ho scoperto che il codice a linea retta ben commentato è più facile da seguire (soprattutto quando non eri quello che lo ha scritto in origine) rispetto a quando è suddiviso in più funzioni che non sono mai state utilizzate altrove. Ma dipende davvero dalla situazione.

Penso che il take-away principale sia che quando dividi un blocco di codice, stai scambiando un tipo di complessità con un altro. C'è probabilmente un punto debole da qualche parte nel mezzo.


Hai letto "Codice pulito"? Ne discute a lungo. amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/…
Martin Wickman,

9

Idealmente, l'intera funzione dovrebbe essere visualizzabile senza dover scorrere. A volte, questo non è possibile. Ma se riesci a scomporlo in pezzi, sarà molto più facile leggere il codice.

So che non appena premo Pagina su / Giù o mi sposto in un'altra sezione del codice, riesco a ricordare solo 7 +/- 2 cose della pagina precedente. E sfortunatamente, alcune di queste posizioni verranno utilizzate durante la lettura del nuovo codice.

Mi piace sempre pensare alla mia memoria a breve termine come i registri di un computer (CISC, non RISC). Se hai l'intera funzione nella stessa pagina, puoi andare nella cache per ottenere le informazioni richieste da un'altra sezione del programma. Se l'intera funzione non può rientrare in una pagina, ciò equivarrebbe a spingere sempre qualsiasi memoria su disco dopo ogni operazione.


3
Devi solo stamparlo su una stampante a matrice - vedi, 10 metri di codice tutti in una volta :)

O ottieni un monitor con una risoluzione più elevata
frogstarr78,

E usalo con orientamento verticale.
Calmarius

4

Perché utilizzare le funzioni nidificate anziché le normali funzioni esterne?

Anche se le funzioni esterne sono sempre utilizzate nella tua unica funzione, in precedenza grande, rende ancora più facile leggere l'intero disordine:

DoThing(int x){
    x += ;1
    int y = FirstThing(x);
    x = SecondThing(x, y);
    while(spoo > fleem){
        x = ThirdThing(x+y);
    }
}

FirstThing(int x){...}
SecondThing(int x, int y){...}
ThirdThing(int z){...}

2
Due possibili ragioni: ingombrare lo spazio dei nomi e quella che chiamerò "prossimità lessicale". Questi potrebbero essere buoni o cattivi motivi, a seconda della tua lingua.
Frank Shearar,

Ma le funzioni nidificate possono essere meno efficienti perché devono supportare funarg verso il basso e possibilmente chiusure lessicali in piena regola. Se la lingua è scarsamente ottimizzata, potrebbe essere necessario un lavoro extra per creare un ambiente dinamico da passare alle funzioni figlio.
Kaz,

3

Non ho il libro davanti a me in questo momento (per citare), ma secondo Code Complete il "sweetspot" per la lunghezza della funzione era di circa 25-50 righe di codice secondo la sua ricerca.

Ci sono momenti in cui è ok avere funzioni lunghe:

  • Quando la complessità ciclomatica della funzione è bassa. I tuoi colleghi sviluppatori potrebbero sentirsi un po 'frustrati se devono guardare una funzione che contiene un'istruzione if gigante e l'affermazione else per quello se non è sullo schermo allo stesso tempo.

I tempi in cui non è giusto avere lunghe funzioni:

  • Hai una funzione con condizionali profondamente nidificati. Fai un favore ai tuoi colleghi lettori di codice, migliora la leggibilità rompendo la funzione. Una funzione fornisce un'indicazione al lettore che "Questo è un blocco di codice che fa una cosa". Chiediti anche se la lunghezza della funzione indica che sta facendo troppo e che deve essere presa in considerazione in un'altra classe.

La linea di fondo è che la manutenibilità dovrebbe essere una delle massime priorità nell'elenco. Se un altro sviluppatore non riesce a guardare il tuo codice e ottenere una "sintesi" di ciò che il codice sta facendo in meno di 5 secondi, il tuo codice ur non fornisce abbastanza "metadati" per dire cosa sta facendo. Gli altri sviluppatori dovrebbero essere in grado di dire cosa sta facendo la tua classe semplicemente guardando il browser degli oggetti nell'IDE che hai scelto invece di leggere oltre 100 righe di codice.

Le funzioni più piccole hanno i seguenti vantaggi:

  • Portabilità: è molto più facile spostare la funzionalità (sia all'interno della classe sul refactoring che su una diversa)
  • Debug: quando guardi lo stacktrace è molto più veloce individuare un errore se stai osservando una funzione con 25 righe di codice anziché 100.
  • Leggibilità: il nome della funzione indica cosa sta facendo un intero blocco di codice. Uno sviluppatore del tuo team potrebbe non voler leggere quel blocco se non ci lavora. Inoltre, nella maggior parte degli IDE moderni un altro sviluppatore può comprendere meglio cosa sta facendo la tua classe leggendo i nomi delle funzioni in un browser degli oggetti.
  • Navigazione: la maggior parte degli IDE consente di cercare il nome delle funzioni. Inoltre, la maggior parte degli IDE moderni ha la possibilità di visualizzare l'origine di una funzione in un'altra finestra, questo dà agli altri sviluppatori di guardare la tua lunga funzione su 2 schermi (se i multi-monitor) invece di farli scorrere.

L'elenco potrebbe continuare .....


2

La risposta è che dipende, tuttavia dovresti probabilmente trasformarlo in una classe.


A meno che tu non stia scrivendo in una OOPL, nel qual caso forse no :)
Frank Shearar,

dsimcha ha menzionato che stavano usando D e Python.
frogstarr78,

Le lezioni sono troppo spesso stampelle per le persone che non hanno mai sentito parlare di cose come le chiusure lessicali.
Kaz,

2

Non mi piacciono le funzioni più nidificate. Le lambda rientrano in quella categoria ma di solito non mi segnalano a meno che non abbiano più di 30-40 caratteri.

Il motivo di base è che diventa una funzione altamente localmente densa con ricorsione semantica interna, il che significa che è difficile per me avvolgere il cervello, ed è semplicemente più semplice inviare alcune cose a una funzione di supporto che non ingombra lo spazio del codice .

Ritengo che una funzione dovrebbe fare la sua cosa. Fare altre cose è ciò che fanno le altre funzioni. Quindi se hai una funzione a 200 linee che fa la sua cosa, e tutto scorre, è A-OK.


A volte devi passare così tanto contesto a una funzione esterna (alla quale potrebbe avere un comodo accesso come funzione locale) che ci sono più complicazioni nel modo in cui viene invocata la funzione rispetto a ciò che fa.
Kaz,

1

È accettabile? Questa è davvero una domanda a cui solo tu puoi rispondere. La funzione raggiunge ciò di cui ha bisogno? È mantenibile? È "accettabile" per gli altri membri della tua squadra? Se è così, allora è quello che conta davvero.

Modifica: non ho visto le funzioni nidificate. Personalmente, non li userei. Userei invece le funzioni regolari.


1

Una lunga funzione "retta" può essere un modo chiaro per specificare una lunga sequenza di passaggi che si verificano sempre in una particolare sequenza.

Tuttavia, come altri hanno già detto, questa forma è soggetta ad avere tratti di complessità locali in cui il flusso complessivo è meno evidente. Posizionare quella complessità locale in una funzione nidificata (cioè definita altrove all'interno della funzione lunga, forse in alto o in basso) può ripristinare la chiarezza nel flusso principale.

Una seconda considerazione importante è il controllo dell'ambito delle variabili che devono essere utilizzate solo in un tratto locale di una funzione lunga. È necessaria la vigilanza per evitare di avere una variabile che è stata introdotta in una sezione di codice involontariamente referenziata da qualche altra parte (ad esempio, dopo i cicli di modifica), poiché questo tipo di errore non verrà visualizzato come errore di compilazione o di runtime.

In alcune lingue, questo problema è facilmente evitato: un tratto di codice locale può essere racchiuso nel proprio blocco, ad esempio con "{...}", all'interno del quale qualsiasi variabile appena introdotta è visibile solo a quel blocco. Alcuni linguaggi, come Python, non dispongono di questa funzione, nel qual caso le funzioni locali possono essere utili per applicare aree di ambito più piccole.


1

No, le funzioni multipagina non sono desiderabili e non devono superare la revisione del codice. Scrivevo anche lunghe funzioni, ma dopo aver letto il Refactoring di Martin Fowler , mi sono fermato. Le funzioni lunghe sono difficili da scrivere correttamente, difficili da capire e difficili da testare. Non ho mai visto una funzione di nemmeno 50 righe che non sarebbe più facilmente comprensibile e testabile se fosse suddivisa in una serie di funzioni più piccole. In una funzione multipagina ci sono quasi certamente intere classi che dovrebbero essere prese in considerazione. È difficile essere più specifici. Forse dovresti pubblicare una delle tue lunghe funzioni su Code Review e qualcuno (forse io) può mostrarti come migliorarlo.


0

Quando sto programmando in Python, mi piace fare un passo indietro dopo aver scritto una funzione e chiedermi se aderisce allo "Zen di Python" (digita 'import this' nel tuo interprete python):

Bello è meglio che brutto.
Esplicito è meglio che implicito.
Semplice è meglio di complesso.
Complesso è meglio che complicato.
Flat è meglio di nidificato.
Sparse è meglio che denso.
La leggibilità conta.
I casi speciali non sono abbastanza speciali da infrangere le regole.
Sebbene la praticità superi la purezza.
Gli errori non dovrebbero mai passare silenziosamente.
A meno che non sia esplicitamente messo a tacere.
Di fronte all'ambiguità, rifiuta la tentazione di indovinare.
Dovrebbe esserci uno - e preferibilmente solo un - modo obsoleto di farlo.
Anche se in quel modo all'inizio potrebbe non essere ovvio a meno che tu non sia olandese.
Adesso è meglio che mai.
Anche se spesso non è mai meglio di giustoadesso.
Se l'implementazione è difficile da spiegare, è una cattiva idea.
Se l'implementazione è facile da spiegare, potrebbe essere una buona idea.
Gli spazi dei nomi sono una grande idea che suona il clacson: facciamo di più!


1
Se esplicito è meglio di implicito, dovremmo codificare in linguaggio macchina. Nemmeno in assemblatore; fa cose brutte e implicite come riempire automaticamente gli slot di ritardo del ramo con le istruzioni, calcolare gli offset relativi del ramo e risolvere i riferimenti simbolici tra i globi. Amico, questi stupidi utenti di Python e la loro piccola religione ...
Kaz,

0

Inseriscili in un modulo separato.

Supponendo che la tua soluzione non sia più gonfia del bisogno di non avere troppe opzioni. Hai già suddiviso le funzioni in diverse sotto funzioni, quindi la domanda è dove dovresti metterlo:

  1. Inseriscili in un modulo con una sola funzione "pubblica".
  2. Inseriscili in una classe con una sola funzione "pubblica" (statica).
  3. Annidali in una funzione (come hai descritto).

Ora sia la seconda che la terza alternativa sono in qualche modo nidificanti, ma la seconda alternativa non sembrerebbe male ad alcuni programmatori. Se non escludi la seconda alternativa, non vedo troppe ragioni per escludere la terza.

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.