2010-04-12 4 views
8

Manypeople hanno discusso sulla dimensione della funzione. Dicono che le funzioni in generale dovrebbero essere piuttosto brevi. Le opinioni variano da qualcosa come 15 righe a "circa uno schermo", che oggi è probabilmente di circa 40-80 linee.
Inoltre, le funzioni devono sempre soddisfare solo un'attività.È una cattiva pratica avere un lungo metodo di inizializzazione?

Tuttavia, esiste un tipo di funzione che spesso non riesce in entrambi i criteri nel mio codice: Funzioni di inizializzazione.

Ad esempio in un'applicazione audio, l'hardware/API audio deve essere impostato, i dati audio devono essere convertiti in un formato adatto e lo stato dell'oggetto deve essere inizializzato correttamente. Questi sono chiaramente tre diversi compiti e, a seconda dell'API, questo può comprendere più di 50 linee.

La cosa con le funzioni di init è che in genere vengono chiamati solo una volta, quindi non è necessario riutilizzare nessuno dei componenti. Li interrompereste in diverse funzioni più piccole, considerereste valide le funzioni di inizializzazione?

risposta

12

sarei ancora rompere la funzione da compito, e quindi chiamare ciascuna delle funzioni di livello più basso da dentro il mio pubblico rivolto inizializzare la funzione:

void _init_hardware() { } 
void _convert_format() { } 
void _setup_state() { } 

void initialize_audio() { 
    _init_hardware(); 
    _convert_format(); 
    _setup_state(); 
} 

Scrittura di funzioni succinta è tanto di isolare colpa e il cambiamento come mantenere le cose leggibili. Se si conosce l'errore in _convert_format(), è possibile rintracciare le ~ 40 linee responsabili di un errore un po 'più veloce. La stessa cosa si applica se si commettono modifiche che toccano solo una funzione.

Un ultimo punto, utilizzo spesso lo assert() così posso "fallire spesso e fallire presto", e l'inizio di una funzione è il posto migliore per un paio di asserzioni di controllo della sanità.Mantenere la funzione breve consente di testare la funzione in modo più approfondito in base al suo insieme più ristretto di funzioni. È molto difficile testare una funzione a 400 linee che fa 10 cose diverse.

+1

+1 per 'assert()' solo. – ndim

+0

+1: "non è necessario riutilizzare nessuno dei componenti." Il riutilizzo non è il problema. Scrivere qualcosa che possa essere compreso e mantenuto da altre persone è molto, molto più importante. –

+0

Ricordo un consiglio di lasciare gli identificatori che iniziano con caratteri di sottolineatura all'uso interno del compilatore C ed evitandoli nei programmi. Inoltre, dovresti contrassegnare quelle tre funzioni init una tantum come 'static'. Per una volta, non verranno utilizzati al di fuori del file sorgente corrente. E come ulteriore vantaggio, un compilatore intelligente vedrà che vengono chiamati solo una volta e solo inline il codice (nel caso in cui siate preoccupati per il sovraccarico di chiamata). – ndim

5

Se la suddivisione in parti più piccole rende il codice più strutturato e/o più leggibile, farlo indipendentemente dalla funzione. Non riguarda il numero di linee che riguarda la qualità del codice.

2

In una situazione come questa penso che dipenda da una questione di preferenze personali. Preferisco che le funzioni facciano solo una cosa, quindi dividerei l'inizializzazione in funzioni separate, anche se sono chiamate solo una volta. Tuttavia, se qualcuno volesse fare tutto in un'unica funzione, non mi preoccuperei troppo (purché il codice fosse chiaro). Ci sono cose più importanti su cui discutere (come se le parentesi graffe appartengano alla loro linea separata).

1

Se si dispone di molti componenti che devono essere collegati l'uno all'altro, può essere ragionevolmente naturale disporre di un metodo di grandi dimensioni, anche se la creazione di ciascun componente viene rifatta in un metodo separato laddove possibile.

Un'alternativa a questo è usare un quadro di Iniezione di dipendenza (ad es. Primavera, Castello di Windsor, Guice ecc.). Ciò ha pro e contro definitivi ... mentre il tuo modo di lavorare su un metodo di grandi dimensioni può essere piuttosto doloroso, almeno hai una buona idea di dove tutto è inizializzato, e non c'è bisogno di preoccuparsi di cosa "magia" potrebbe andare avanti . Inoltre, l'inizializzazione non può essere modificata dopo la distribuzione (come con un file XML per Spring, ad esempio).

penso abbia senso progettare il corpo principale del codice in modo che può essere iniettato - ma se tale conferimento avviene tramite un quadro o semplicemente un elenco hard-coded (e potenzialmente lunga) di chiamate di inizializzazione è una scelta che potrebbe cambiare per diversi progetti. In entrambi i casi è difficile testare i risultati se non eseguendo semplicemente l'applicazione.

3

Proverei ancora a suddividere le funzioni in unità logiche. Dovrebbero essere lunghi o brevi come è logico. Ad esempio:

SetupAudioHardware(); 
ConvertAudioData(); 
SetupState(); 

Assegnare loro nomi chiari rende tutto più intuitivo e leggibile. Inoltre, spezzandoli, rende più facile il riutilizzo di modifiche future e/o di altri programmi.

1

In primo luogo, è necessario utilizzare una fabbrica anziché una funzione di inizializzazione. Cioè, piuttosto che avere initialize_audio(), si ha un new AudioObjectFactory (si può pensare ad un nome migliore qui). Ciò mantiene la separazione delle preoccupazioni.

Tuttavia, fare attenzione anche a non astrarre troppo presto. Chiaramente avete già due dubbi: 1) inizializzazione audio e 2) usando quell'audio. Finché, ad esempio, non si somministra il dispositivo audio da inizializzare, o il modo in cui un determinato dispositivo può essere configurato durante l'inizializzazione, il metodo di produzione (audioObjectFactory.Create() o qualsiasi altra cosa) deve essere mantenuto in un solo grande metodo. L'astrazione precoce serve solo a offuscare il design.

Si noti che audioObjectFactory.Create() non è qualcosa che può essere testato unitamente. Provarlo è un test di integrazione, e finché non ci saranno parti che possono essere astratte, rimarrà un test di integrazione. In seguito, potresti scoprire che hai più fabbriche diverse per diverse configurazioni; a quel punto, potrebbe essere utile astrarre le chiamate hardware in un'interfaccia, così da poter creare test unitari per garantire che le varie fabbriche configurino l'hardware in modo corretto.

+0

In realtà, questo è praticamente quello che sto facendo. La classe stessa dovrebbe essere un oggetto lettore audio che astrae la gestione del caos che è CoreAudio. In realtà, la preparazione dei dati audio e l'inizializzazione dell'hardware audio sono strettamente collegate poiché i diversi dati audio richiedono una configurazione diversa e viceversa. – bastibe

1

Penso che sia l'approccio sbagliato per provare e contare il numero di linee e determinare le funzioni basate su quello. Per qualcosa come il codice di inizializzazione, spesso ho una funzione separata, ma soprattutto perché le funzioni Load o Init o New non sono confuse e disordinate. Se riesci a separarlo in poche attività, come suggerito da altri, puoi nominarlo come qualcosa di utile e aiutare a organizzarlo. Anche se lo chiami solo una volta, non è una cattiva abitudine e spesso scopri che ci sono altre volte in cui potresti voler riavviare le cose e puoi usare di nuovo quella funzione.

1

Ho solo pensato di buttarlo lì, poiché non è stato ancora menzionato - lo Facade Pattern viene talvolta citato come interfaccia per un sottosistema complesso. Non ho fatto molto con questo, ma le metafore sono solitamente come accendere un computer (richiede diversi passaggi) o accendere un sistema home theater (accendere la TV, accendere il ricevitore, spegnere le luci, ecc. .)

A seconda della struttura del codice, potrebbe essere qualcosa che vale la pena considerare per astrarre le grandi funzioni di inizializzazione. Sono ancora d'accordo con il punto di vista di Meagar, sebbene suddividere le funzioni in _init_X(), _init_Y(), ecc. Sia un buon modo per andare. Anche se non riutilizzerai i commenti in questo codice, nel tuo prossimo progetto, quando dici a te stesso: "Come ho inizializzato quel componente X?", Sarà molto più facile tornare indietro e selezionarlo della funzione più piccola di _init_X() rispetto a quella che sarebbe quella di selezionarla da una funzione più grande, specialmente se l'inizializzazione X è sparsa per tutto il tempo.

1

La lunghezza della funzione è, come hai taggato, una questione molto soggettiva. Tuttavia, una best practice standard consiste nell'isolare il codice che viene spesso ripetuto e/o può funzionare come una propria entità. Ad esempio, se la funzione di inizializzazione sta caricando file di libreria o oggetti che verranno utilizzati da una libreria specifica, tale blocco di codice dovrebbe essere modularizzato.

Detto questo, non è male avere un metodo di inizializzazione lungo, a patto che non sia lungo a causa di un sacco di codice ripetuto o di altri frammenti che possono essere sottratti.

Speranza che aiuta,
Carlos Nunez