Funzioni che chiamano solo altre funzioni. È una buona pratica?


13

Attualmente sto lavorando a una serie di report che hanno molte sezioni diverse (tutte richiedono una formattazione diversa) e sto cercando di capire il modo migliore per strutturare il mio codice. Rapporti simili che abbiamo fatto in passato finiscono per avere funzioni molto grandi (oltre 200 righe) che eseguono tutta la manipolazione e la formattazione dei dati per il rapporto, in modo che il flusso di lavoro assomigli a questo:

DataTable reportTable = new DataTable();
void RunReport()
{
    reportTable = DataClass.getReportData();
    largeReportProcessingFunction();
    outputReportToUser();
}

Vorrei poter suddividere queste grandi funzioni in blocchi più piccoli, ma temo che finirò per avere dozzine di funzioni non riutilizzabili e una funzione simile "fai tutto qui" il cui unico compito è chiama tutte queste funzioni più piccole, in questo modo:

void largeReportProcessingFunction()
{
    processSection1HeaderData();
    calculateSection1HeaderAverages();
    formatSection1HeaderDisplay();
    processSection1SummaryTableData();
    calculateSection1SummaryTableTotalRow();
    formatSection1SummaryTableDisplay();
    processSection1FooterData();
    getSection1FooterSummaryTotals();
    formatSection1FooterDisplay();

    processSection2HeaderData();
    calculateSection1HeaderAverages();
    formatSection1HeaderDisplay();
    calculateSection1HeaderAverages();
    ...
}

Oppure, se facciamo un ulteriore passo avanti:

void largeReportProcessingFunction()
{
    callAllSection1Functions();

    callAllSection2Functions();

    callAllSection3Functions();
    ...        
}

È davvero una soluzione migliore? Da un punto di vista organizzativo suppongo che lo sia (cioè tutto è molto più organizzato di quanto potrebbe essere altrimenti), ma per quanto riguarda la leggibilità del codice non sono sicuro (catene potenzialmente grandi di funzioni che chiamano solo altre funzioni).

Pensieri?


Alla fine della giornata ... è più leggibile? Sì, quindi è una soluzione migliore.
Sylvanaar,

Risposte:


18

Questa è comunemente definita decomposizione funzionale ed è generalmente una buona cosa da fare, se eseguita correttamente.

Inoltre, l'implementazione di una funzione dovrebbe essere all'interno di un singolo livello di astrazione. Se lo prendi largeReportProcessingFunction, il suo ruolo è quello di definire quali diverse fasi di elaborazione devono essere prese in quale ordine. L'implementazione di ciascuno di questi passaggi è su un livello di astrazione di seguito e largeReportProcessingFunctionnon dovrebbe dipendere direttamente da esso.

Si noti che questa è una cattiva scelta di denominazione:

void largeReportProcessingFunction() {
    callAllSection1Functions();
    callAllSection2Functions();
    callAllSection3Functions();
    ...        
}

Vedete callAllSection1Functionsè un nome che in realtà non fornisce un'astrazione, perché in realtà non dice quello che fa, ma piuttosto come lo fa. Dovrebbe essere chiamato processSection1invece o qualunque cosa sia effettivamente significativa nel contesto.


6
Concordo in linea di principio con questa risposta, tranne per il fatto che "processSection1" è un nome significativo. È praticamente equivalente a "callAllSection1Functions".
Eric King,

2
@EricKing: callAllSection1Functionsperde dettagli sull'implementazione. Dal punto di vista esterno, non importa se processSection1difende il suo lavoro in "tutte le funzioni della sezione 1" o se lo fa direttamente o se usa la polvere di folletto per evocare un unicorno che farà il lavoro vero e proprio. Tutto ciò che conta davvero è che dopo una chiamata a processSection1, sezione1 può essere considerata elaborata . Concordo sul fatto che l' elaborazione sia un nome generico, ma se si ha un'elevata coesione (per la quale si dovrebbe sempre lottare), il contesto di solito è abbastanza ristretto da non chiarirlo.
back2dos,

2
Intendevo solo che i metodi chiamati "processXXX" sono quasi sempre nomi terribili e inutili. Tutti i metodi "elaborano" le cose, quindi denominarlo "processXXX" è utile quanto nominarlo "doSomethingWithXXX" o "manipulateXXX" e simili. C'è quasi sempre un nome migliore per i metodi denominati 'processXXX'. Da un punto di vista pratico, è utile (inutile) come 'callAllSection1Functions'.
Eric King,

4

Hai detto che stai usando C #, quindi mi chiedo se potresti costruire una gerarchia di classi strutturata sfruttando il fatto che stai usando un linguaggio orientato agli oggetti? Ad esempio, se ha senso suddividere il report in sezioni, disporre di una Sectionclasse ed estenderlo per requisiti specifici del cliente.

Potrebbe avere senso pensarci come se stessi creando una GUI non interattiva. Pensa agli oggetti che potresti creare come se fossero componenti della GUI diversi. Chiediti come sarebbe un rapporto di base? Che ne dici di uno complesso? Dove dovrebbero essere i punti di estensione?


3

Dipende. Se tutte le funzioni che stai creando sono veramente una singola unità di lavoro, allora va bene, se sono solo le linee 1-15, 16-30, 31-45 della loro funzione di chiamata che è cattiva. Le funzioni lunghe non sono male, se fanno una cosa, se hai 20 filtri per il tuo rapporto con una funzione validata che controlla che tutte e venti saranno lunghe. Va bene, suddividerlo per filtro o gruppi di filtri sta solo aggiungendo ulteriore complessità senza alcun vantaggio reale.

Avere molte funzioni private rende anche più difficile il testing automatico delle unità, quindi alcuni cercheranno di evitarlo per quel motivo, ma questo è un ragionamento piuttosto scarso secondo me poiché tende a creare un design più ampio e complesso per adattarsi ai test.


3
Nella mia esperienza, le lunghe funzioni sono malvagie.
Bernard,

Se il tuo esempio fosse in linguaggio OO, creerei una classe di filtro di base e ciascuno dei 20 filtri sarebbe in diverse classi di derivazione. L'istruzione switch verrebbe sostituita da una chiamata di creazione per ottenere il filtro giusto. Ogni funzione fa una cosa e tutte sono piccole.
DXM

3

Dato che stai usando C #, prova a pensare di più negli oggetti. Sembra che tu stia ancora codificando proceduralmente avendo variabili di classe "globali" dalle quali dipendono le funzioni successive.

Rompere la tua logica in funzioni separate è decisamente meglio che avere tutta la logica in una singola funzione. Scommetto che potresti andare oltre separando anche i dati su cui funzionano le funzioni suddividendoli in diversi oggetti.

Considerare di avere una classe ReportBuilder in cui è possibile passare i dati del report. Se riesci a suddividere ReportBuilder in classi separate, fai anche questo.


2
Le procedure svolgono una funzione perfettamente valida.
DeadMG

1

Sì.

Se si dispone di un segmento di codice che deve essere utilizzato in più posizioni propria funzione. In caso contrario, se è necessario apportare una modifica alla funzionalità, sarà necessario apportare la modifica in più punti. Ciò non solo aumenta il lavoro, ma aumenta il rischio che vengano visualizzati bug quando il codice viene modificato in un posto ma non in un altro o dove viene introdotto un ma in un posto ma non nell'altro.

Aiuta anche a rendere il metodo più leggibile se vengono utilizzati nomi di metodi descrittivi.


1

Il fatto che usi la numerazione per distinguere le funzioni suggerisce che ci sono problemi sottostanti con la duplicazione o una classe che fa troppo. La suddivisione di una funzione di grandi dimensioni in molte funzioni più piccole può essere un passaggio utile per il refactoring della duplicazione, ma non dovrebbe essere l'ultima. Ad esempio, si creano le due funzioni:

processSection1HeaderData();
processSection2HeaderData();

Non ci sono somiglianze nel codice utilizzato per elaborare le intestazioni di sezione? Puoi estrarre una funzione comune per entrambi parametrizzando le differenze?


Questo non è in realtà un codice di produzione, quindi i nomi delle funzioni sono solo esempi (in produzione i nomi delle funzioni sono più descrittivi). In generale, no, non ci sono somiglianze tra le diverse sezioni (anche se quando ci sono somiglianze, provo a riutilizzare il codice il più possibile).
Eric C.

1

La suddivisione di una funzione in sottofunzioni logiche è utile, anche se le sottofunzioni non vengono riutilizzate: la suddividono in blocchi brevi e facilmente comprensibili. Ma deve essere fatto da unità logiche, non solo n # di linee. Il modo in cui dovresti romperlo dipenderà dal tuo codice, temi comuni sarebbero loop, condizioni, operazioni sullo stesso oggetto; praticamente tutto ciò che puoi descrivere come un'attività discreta.

Quindi, sì, è un buon stile - questo può sembrare strano, ma dato il tuo limitato riutilizzo descritto, ti consiglio di pensare attentamente al TENTATIVO riutilizzo. Assicurati che l'uso sia veramente identico e non coincida casualmente. Tentare di gestire tutti i casi nello stesso blocco è spesso il modo in cui si finisce con la grande funzione sgraziata al primo posto.


0

Penso che questa sia principalmente una preferenza personale. Se le tue funzioni fossero riutilizzate (e sei sicuro di non poterle rendere più generiche e riutilizzabili?) Direi sicuramente di dividerle. Ho anche sentito che è un buon principio che nessuna funzione o metodo dovrebbe contenere più di 2-3 schermate di codice. Quindi, se la tua grande funzione continua, potrebbe rendere il tuo codice più leggibile per dividerlo.

Non menzioni la tecnologia che stai utilizzando per sviluppare questi rapporti. Sembra che potresti usare C #. È corretto? In tal caso, potresti anche considerare la direttiva #region come alternativa se non vuoi dividere la tua funzione in più piccole.


Sì, sto lavorando in C #. È possibile che sarò in grado di riutilizzare alcune funzioni, ma per molti di questi rapporti le diverse sezioni hanno dati e layout molto diversi (requisito del cliente).
Eric C.

1
+1 tranne che per le regioni suggerite. Non userei le regioni.
Bernard,

@Bernard - Qualche motivo particolare? Suppongo che il richiedente avrebbe preso in considerazione l'uso di #regions se avesse già escluso la divisione della funzione.
Joshua Carmody,

2
Le regioni tendono a nascondere il codice e possono essere abusate (ovvero regioni nidificate). Mi piace vedere tutto il codice contemporaneamente in un file di codice. Se devo separare visivamente il codice in un file, utilizzo una linea di barre.
Bernard,

2
Le regioni sono di solito un segno che il codice deve essere sottoposto a refactoring
codificatore
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.