Quando è opportuno creare una funzione separata quando ci sarà sempre una singola chiamata a detta funzione? [duplicare]

43

Stiamo progettando standard di codifica e stiamo avendo disaccordi sul fatto che sia mai opportuno suddividere il codice in funzioni separate all'interno di una classe, quando tali funzioni verranno sempre chiamate una sola volta.

Ad esempio:

f1()
{
   f2();  
   f4();
}


f2()
{
    f3()
    // Logic here
}


f3()
{
   // Logic here
}

f4()
{
   // Logic here
}

vs

f1()
{
   // Logic here
   // Logic here
   // Logic here
}

Alcuni sostengono che sia più semplice da leggere quando si interrompe una funzione di grandi dimensioni utilizzando funzioni secondarie separate monouso. Tuttavia, leggendo il codice per la prima volta, trovo noioso seguire le catene logiche e ottimizzare il sistema nel suo insieme. Ci sono delle regole tipicamente applicate a questo tipo di layout di funzione?

Si noti che a differenza di altre domande, sto chiedendo il miglior insieme di condizioni per differenziare gli usi consentiti e non consentiti delle singole funzioni di chiamata, non solo se sono consentite.

    
posta David 23.01.2016 - 01:05
fonte

8 risposte

88

La logica alla base della divisione delle funzioni non è il numero di volte in cui saranno chiamate , ma le mantiene piccole e impedisce loro di fare diverse cose.

Il libro di Bob Martin Pulisci codice fornisce indicazioni utili su quando dividere una funzione:

  • Functions should be small; how small? See the bullet bellow.
  • Functions should do only one thing.

Quindi, se la funzione è composta da più schermate, dividerla. Se la funzione fa diverse cose, dividerla.

Se la funzione è composta da passaggi sequenziali finalizzati a un risultato finale, non è necessario dividerlo, anche se è relativamente lungo. Ma se le funzioni fanno una cosa, poi un'altra, poi un'altra e poi un'altra, con condizioni, blocchi logicamente separati, ecc., Dovrebbe essere divisa. Come risultato di questa logica, le funzioni dovrebbero essere generalmente piccole.

Se f1() esegue l'autenticazione, f2() analizza l'input in parti più piccole, se f3() fa calcoli e f4() registra o persiste risultati, allora ovviamente dovrebbero essere separati, anche quando ognuno di essi sarà chiamato solo una volta.

In questo modo puoi refactoring e testarli separatamente, oltre al vantaggio di essere più facile da leggere .

D'altra parte se tutta la funzione è:

a=a+1;
a=a/2;
a=a^2
b=0.0001;
c=a*b/c;
return c;

quindi non è necessario dividerlo, anche quando la sequenza di passaggi è lunga.

    
risposta data 23.01.2016 - 01:19
fonte
43

Penso che la denominazione delle funzioni sia molto importante qui.

Una funzione strongmente sezionata può essere molto auto-documentante. Se ogni processo logico all'interno di una funzione è suddiviso in una propria funzione, con una logica interna minima, il comportamento di ogni istruzione può essere risolto dai nomi delle funzioni e dei parametri che prendono.

Naturalmente, c'è un lato negativo: i nomi delle funzioni. Come i commenti, tali funzioni altamente specifiche possono spesso non essere sincronizzate con ciò che la funzione sta effettivamente facendo. Ma , allo stesso tempo, assegnandogli un nome di funzione appropriato, lo rendi più difficile per giustificare lo scorrimento dello scope. Diventa più difficile fare una sottofunzione fare qualcosa di più di quanto dovrebbe chiaramente.

Quindi suggerirei questo. Se ritieni che una sezione di codice possa essere scomposta, anche se nessun altro la chiama, poniti questa domanda: "Che nome daresti?"

Se rispondere a questa domanda richiede più di 5 secondi, o se il nome della funzione selezionato è decisamente opaco, c'è una buona probabilità che non si tratti di un'unità logica separata all'interno della funzione. O, per lo meno, che non sei abbastanza sicuro di ciò che l'unità logica sta davvero facendo per dividerlo correttamente.

Ma c'è un ulteriore problema che le funzioni pesantemente dissezionate possono incontrare: bug fixing.

Il rilevamento degli errori logici all'interno di una funzione di linea di 200 + è difficile. Ma monitorandoli attraverso più di 10 funzioni individuali, cercando di ricordare le relazioni tra loro? Questo può essere ancora più difficile.

Tuttavia, ancora una volta, l'auto-documentazione semantica attraverso i nomi può avere un ruolo chiave. Se ogni funzione ha un nome logico, tutto quello che devi fare per convalidare una delle funzioni foglia (oltre al test delle unità) è vedere se effettivamente fa quello che dice. Le funzioni fogliari tendono ad essere corte e focalizzate. Quindi, se ogni singola funzione foglia esegue ciò che dice che dovrebbe, l'unico problema possibile è che qualcuno gli ha passato le cose sbagliate.

Quindi in questo caso, può essere un vantaggio per correggere i bug.

Penso che sia davvero necessario decidere se è possibile assegnare un nome significativo a un'unità logica. Se è così, allora può probabilmente essere una funzione.

    
risposta data 23.01.2016 - 01:31
fonte
12

Ogni volta che senti il bisogno di scrivere un commento per descrivere cosa sta facendo un blocco di testo, hai trovato l'opportunità di estrarre un metodo.

Invece di

//find eligible contestants
var eligible = contestants.Where(c=>c.Age >= 18)
eligible = eligible.Where(c=>c.Country == US)

try

var eligible = FindEligible(contestants)
    
risposta data 23.01.2016 - 13:34
fonte
5

DRY - Non ripeterti - è solo uno dei diversi principi che devono essere bilanciati.

Alcuni altri che vengono in mente qui sono i nomi. Se la logica è complicata non ovvia al lettore casuale, l'estrazione in metodo / funzione il cui nome meglio incapsula cosa e perché lo sta facendo può migliorare la leggibilità del programma.

Potrebbero entrare in gioco anche puntare a meno di 5-10 linee di metodo / funzione di codice, a seconda del numero di righe che la //logic diventa.

Inoltre, una funzione con parametri può fungere da apice e può denominare i parametri in modo appropriato per rendere più chiara la logica del codice.

Inoltre, potresti scoprire che nel corso del tempo le raccolte di tali funzioni rivelano un utile raggruppamento della cupola, ad es. admin e quindi possono essere facilmente raccolti sotto di esso.

    
risposta data 23.01.2016 - 01:33
fonte
4

Il punto su come suddividere le funzioni è tutto su una cosa: semplicità.

Un lettore di codice non può avere più di sette cose in mente contemporaneamente. Le tue funzioni dovrebbero rispecchiarlo.

  • Se crei funzioni troppo lunghe, saranno illeggibili perché hai più di sette cose all'interno delle tue funzioni.

  • Se costruisci un sacco di funzioni su una riga, i lettori si confondono allo stesso modo nel groviglio di funzioni. Ora dovresti conservare più di sette funzioni nella memoria per capire lo scopo di ciascuna di esse.

  • Alcune funzioni sono semplici anche se sono lunghe. L'esempio classico è quando una funzione contiene una grande istruzione switch con molti casi. Finché la gestione di ogni caso è semplice, le tue funzioni non sono troppo lunghe.

Entrambi gli estremi (il Megamoth e il brodo di funzioni minuscole) sono ugualmente cattivi, e devi trovare un equilibrio intermedio. Nella mia esperienza, una buona funzione ha qualcosa in giro per dieci righe. Alcune funzioni saranno one-liner, alcune supereranno venti linee. La cosa importante è che ogni funzione serve una funzione facilmente comprensibile pur essendo ugualmente comprensibile nella sua implementazione.

    
risposta data 23.01.2016 - 21:58
fonte
2

Dipende molto da cosa è il tuo // Logic Here .

Se si tratta di un one-liner, probabilmente non hai bisogno di una decomposizione funzionale.

Se, d'altra parte, sono linee e linee di codice, allora è molto meglio metterlo in una funzione separata e nominarlo in modo appropriato ( f1,f2,f3 non passa più qui).

Questo ha a che fare con il cervello umano in media non è molto efficiente con l'elaborazione di grandi quantità di dati a colpo d'occhio. In un certo senso, non importa quali dati: funzione multi-linea, intersezione trafficata, puzzle da 1000 pezzi.

Sii un amico del cervello del manutentore del codice, abbatti i pezzi nel punto di ingresso. Chissà, quel manutentore del codice potrebbe essere anche tu qualche mese dopo.

    
risposta data 23.01.2016 - 03:28
fonte
2

Riguarda la separazione delle preoccupazioni . (ok, non tutti su di esso, questa è una semplificazione).

Questo va bene:

function initializeUser(name, job, bye) {
    this.username = name;
    this.occupation = job;
    this.farewell = bye;
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    ... etc in the same vein.
}

Questa funzione riguarda una singola preoccupazione: imposta le proprietà iniziali di un utente dagli argomenti, dai valori predefiniti, dall'estrapolazione, ecc. forniti.

Questo non va bene:

function initializeUser(name, job, bye) {
    // Connect to internet if not already connected.
    modem.getInstance().ensureConnected();
    // Connect to user database
    userDb = connectToDb(USER_DB);
    // Validate that user does not yet exist.
    if (1 != userDb.db_exec("SELECT COUNT(*) FROM 'users' where 'name' = %d", name)) {
        throw new sadFace("User exists");
    }
    // Configure properties. Don't try to translate names.
    this.username = removeBadWords(name);
    this.occupation = removeBadWords(translate(name));
    this.farewell = removeBadWords(translate(name));
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    userDb.db_exec("INSERT INTO 'users' set 'name' = %s", this.username);
    // Disconnect from the DB.
    userDb.disconnect();
}

Separazione delle preoccupazioni suggerisce che questo dovrebbe essere trattato come una preoccupazione multipla; la gestione del DB, la convalida dell'esistenza dell'utente, l'impostazione delle proprietà. Ognuno di questi è molto facilmente testato come un'unità, ma testarli tutti in un unico metodo rende un insieme di test unità molto contorto, che testerebbe cose tanto diverse quanto il modo in cui gestiva il DB che andava via, come gestiva la creazione di titoli di lavoro vuoti e non validi e come gestisce la creazione di un utente due volte (risposta: male, c'è un bug).

Parte del problema è che si diffonde ovunque in termini di livello elevato: reti di basso livello e roba DB non hanno spazio qui. Questo fa parte della separazione delle preoccupazioni. L'altra parte è quella roba che dovrebbe essere la preoccupazione di qualcos'altro, è invece la preoccupazione della funzione init. Ad esempio, se tradurre o applicare filtri di cattiva lingua, potrebbe avere più senso come preoccupazione per i campi impostati.

    
risposta data 25.01.2016 - 01:45
fonte
1

La risposta "giusta", secondo i dogmi di codifica prevalenti, consiste nel suddividere grandi funzioni in funzioni di e test di piccole dimensioni, facili da leggere, testabili con nomi di auto-documentazione.

Ciò detto, definire "grande" in termini di "linee di codice" può sembrare arbitrario, dogmatico e noioso, che può causare disaccordi, scrupoli e tensioni non necessari. Ma non temere! Perché, se riconosciamo che gli scopi principali alla base del limite delle righe di codice sono la leggibilità e la testabilità, possiamo facilmente trovare il limite di riga appropriato per ogni funzione data! (E inizia a costruire le basi per un livello internamente rilevante soft-limit.)

Accetta come una squadra per consentire le funzioni megalitiche e per estrarre le linee in funzioni più piccole e ben definite al segno prima che la funzione è difficile da leggere nel suo insieme, o quando i sottoinsiemi sono incerti nella correttezza.

... E se tutti sono onesti durante l'implementazione iniziale e se nessuno di loro reclamizza un QI superiore a 200, i limiti di comprensibilità e testabilità possono spesso essere identificati prima che qualcun altro veda il proprio codice.

    
risposta data 23.01.2016 - 06:39
fonte