Quando, se mai, dovrei usare le funzioni a catena?

5

In un programma diviso in molte funzioni con le quali è previsto che si eseguano uno dopo l'altro, quando (se mai) è preferibile a:

A) Esegui le funzioni una dopo l'altra in main() ?

o

B) Esegui una funzione in main() e hai quella funzione daisy chain al resto delle funzioni che devono essere eseguite?

Per illustrare questo

Diagramma di flusso del programma:

Inizio - > Make foo - > Do bar? - > Vero - > Dì addio - > Fine

A:

void makeFoo() {
    [...]
    return;
}
bool doBar() {
    bool bar;
    [...]
    return bar;
}
void sayGoodbye() {
    [...]
    return;
}
int main {
    makeFoo();
    if (doBar()) {
        sayGoodbye();
    }
    return 0;
}

B: (Daisy Chain)

void makeFoo() {
    [...]
    doBar();
    return;
}
void doBar() {
    bool bar;
    [...]
    if (bar) {
        sayGoodbye();
    }
    return;
}
void sayGoodbye() {
    [...]
    return;
}
int main {
    makeFoo();
    return 0;
}
    
posta Akiva 06.02.2015 - 19:19
fonte

5 risposte

11

Daisy chain quando è chiaro allo sviluppatore successivo perché l'hai fatto in quel modo.

"Code is read more often than it is written." -Guido von Rossum, PEP 008

Non c'è una risposta a questo, ma posso darti una buona regola generale: se il nome della funzione implica che il suo compito è di fare una funzione e chiamarne un'altra, allora è sicuro ricollegarle.

Ci sono momenti in cui due funzionalità hanno davvero bisogno di catena. Considera il classico esempio di gestione degli errori

result = doSomething();
if (result == badValue)
    haltAndCatchFire();

Se questo modello si presenta sempre, il prossimo sviluppatore sarà dolorante a leggerlo più e più volte. Peggio ancora, possono diventare pigri o dimenticare, causando risultati inaspettati. Tuttavia, haltAndCatchFire a sé stante è una funzione piuttosto utile da avere, quindi non vogliamo seppellire quella funzionalità in doSomething , solo per non vederla più. Come soluzione:

doSomethingOrHaltAndCatchFire();

È lungo, ma ora è molto chiaro che intendi collegare a margherita. Uno sviluppatore che non desidera seguirti in doSomething generalmente può presumere che chiamerai haltAndCatchFire se qualcosa va storto, quindi hai trasmesso correttamente le informazioni allo sviluppatore successivo. Possono continuare a leggere main() senza dover schivare e intrecciare più funzioni.

La scelta dei nomi dipende da te, purché sia chiara. Ad esempio, su una suite di software che ho sviluppato, c'era uno schema:

int getValue();
int tryGetValue();

Si è capito (contratto sociale) che getValue() dovrebbe chiamare funzioni di gestione delle eccezioni se qualcosa va storto. TryGetValue dovrebbe solo tornare (spesso restituendo un valore fittizio che ci fa sapere che qualcosa è andato storto). Per altre situazioni, in cui il contratto sociale non era così chiaro, abbiamo avuto funzioni come getRegressionResultsForData() , che ha fatto alcune funzioni per ottenere i dati, quindi chiamato getRegressionResults() su di esso.

In sintesi, non devi mai concatenare una situazione in cui uno sviluppatore futuro potrebbe essere fuorviante scorrendo su una funzione (invece di immergerti in tutte le sotto-funzioni per cercare le catene).

    
risposta data 06.02.2015 - 19:29
fonte
7

Il collegamento a margherita è quasi sempre preferito. Guarda il tuo esempio e immagina un futuro manutentore che legge questo codice per correggere un bug, senza il beneficio del diagramma di flusso.

È difficile dire dai nomi fittizi delle funzioni di esempio, ma in un programma reale, può leggere la funzione main per ricostruire il flusso generale e indicare dove guardare dopo i nomi delle funzioni. Nella versione daisy-chain, avrebbe dovuto esaminare ogni singola funzione, che potrebbe estendersi su più file. Allo stesso modo, nella versione con collegamento a margherita, la modifica di un passaggio potrebbe causare un effetto a catena attraverso il resto del codice.

Tuttavia, includere tutti i passaggi nella funzione main non è necessariamente la risposta. Vuoi creare funzioni in cui tutto ciò che leggi è allo stesso livello di astrazione. Forse makeFoo ha diverse sottofasi. Va bene lasciare quelli lì e restituire il sub-risultato a main .

    
risposta data 06.02.2015 - 22:24
fonte
6

I tuoi metodi dovrebbero fare una e una sola cosa.

Ad esempio, sta dicendo addio una parte di doBar ?

  • Se sì, dovrebbe essere usato il secondo approccio.

  • Se saluti dopo doBar ma non parte di doBar , allora il tuo primo approccio è migliore.

Esempio pratico. Una parte di un'applicazione dovrebbe leggere ed elaborare i dati. Questo viene fatto attraverso cinque metodi:

  1. ConnectToDatabase()

    Apre la connessione, rendendo possibile interrogare il database.

  2. GetData()

    Recupera dati validi e disinfettati dal database.

  3. LoadData()

    Carica i dati dal database in memoria per un'ulteriore elaborazione.

  4. ValidateData()

    Verifica che i dati siano validi: se non lo sono, genera un'eccezione, poiché questa situazione è davvero eccezionale.

  5. SanitizeData()

    Trasforma i dati in modo che possano essere elaborati. Sebbene i dati non pubblicitari siano ancora validi, è necessario disinfettarli per soddisfare i vincoli specifici della nostra elaborazione.

  6. ProcessData()

    Elabora i dati creando qualcosa di dark magic con esso.

  7. Output()

    Mostra il risultato dei dati elaborati sullo schermo. Il risultato contiene due grafici.

  8. GenerateCharts()

    Genera i grafici finali da mostrare all'utente.

  9. DisplayGlobalChart()

    Visualizza il primo grafico sullo schermo.

  10. DisplayPerMonthChart()

    Visualizza il secondo grafico sullo schermo.

Sembra che ConnectToDatabase() , LoadData() e SanitizeData() facciano parte di GetData() : quando chiami GetData() , non ti importa se è caricato dal database di un file flat, come è effettivamente caricato o richiede di essere disinfettato. Hai solo bisogno del set di dati pronto per il tuo lavoro.

Sembra anche che la generazione di grafici e due metodi che visualizzano il grafico possano facilmente andare in Output() : ancora una volta, non ti interessa come le informazioni dovrebbero essere visualizzate quando chiami Output() ; sai solo che l'informazione non c'è ancora, e ora dovrebbe essere sullo schermo.

Finora, le chiamate sono:

GetData()
{
    ConnectToDatabase();
    LoadData();
    SanitizeData();
}

Output()
{
    GenerateCharts();
    DisplayGlobalChart();
    DisplayPerMonthChart();
}

Main()
{
    data = GetData();
    result = ProcessData(data);
    Output(result);
}

rimane ValidateData() . Questo è difficile. Può essere chiamato da SanitizeData() : ha senso, poiché prima di disinfettare un'informazione, è necessario prima convalidarlo. D'altra parte, potrebbe essere che quei processi siano abbastanza diversi e uno possa essere chiamato indipendentemente dall'altro. In questo caso, ValidateData() sarebbe piuttosto in GetData() , appena prima di SanitizeData() .

    
risposta data 06.02.2015 - 19:23
fonte
2

assolutamente mai.

Supponiamo che tu abbia due funzioni A() e B() . Se A() deve chiamare B() o non funzionerà, quindi sicuro A() dovrebbe chiamare B() . In caso contrario, A() non dovrebbe preoccuparsi o conoscere l'esistenza di B() .

Altrimenti stai facendo due cose:

  • Sei inutilmente [accoppiamento] [1] le due funzioni. e se in futuro vorresti modificare il loro ordine o chiamarne uno da qualche altra parte?
  • Stai offuscando ciò che sta accadendo nel tuo programma. se hai un flusso che dice: * fai A poi fai B, se B successo poi fai C, quindi da qualche parte nel tuo programma ci dovrebbe essere il codice per descrivere questo processo i.e:

    A();
    result = B();
    if (result) then
        C();
    

Vedi anche: Coupling (programmazione per computer) su Wikipedia

    
risposta data 07.02.2015 - 12:29
fonte
1

Dipende. Di solito non si dovrebbero usare le funzioni daisy-chain, a meno che la daisy-chain non sia un dettaglio di implementazione, non parte del processo principale. Ecco perché, dopo tutto, abbiamo le funzioni main . Se qualcun altro (incluso te in 6 mesi) deve leggere quel codice, è molto più facile trovare una funzione che descriva l'algoritmo di un programma, isolato in funzioni che implementano i passaggi di quell'algoritmo. Soprattutto negli script, vuoi mostrare la procedura nel modo più chiaro possibile, in modo da scrivere codice di auto-documentazione.

Di solito inizio scrivendo una funzione centrale con molte affermazioni di segnaposto non valide, come questa:

def foo_the_bar(xkcd):
    unfooed_bars = discover_bars(xkcd)
    foo = load_foo_from_orbital_cannon()
    return map(foo, unfooed_bars)

Quindi vai a implementare quelle funzioni, fino a quando non c'è un programma completo. Puoi isolare la logica e rendere agnostiche le funzioni di ciò per cui saranno utilizzate.

Per ottenere ciò che fa un programma in questo modo, devi entrare a fondo nelle funzioni interne (purché siano documentate e nominate correttamente).

Il collegamento a margherita produce alcuni di questi effetti negativi:

  • funzioni che fanno le loro cose, quindi adattano i loro risultati al prossimo collegamento a margherita
  • funzioni che si aspettano che un determinato ordine di cose sia successo ai dati
  • overflow dello stack umano

L'ultimo punto è la chiave: gli umani hanno uno stack di contesto, e in un certo senso è molto più limitato di un computer, perché i computer non hanno bisogno di dare un senso all'intera combinazione di procedure, si preoccupano solo dello stato corrente e le loro istruzioni.

Ogni volta che un essere umano legge il tuo codice a margherita, deve aggiungere un nuovo gestore di contesto alla pila del cervello. Questa operazione richiede un po 'di tempo, e poi quando devi fare riferimento ai contesti superiori, devi fare il pop fino al contesto corretto, per poi tornare al posto in cui ti trovavi.

In breve: se stai andando a margherita, potresti rottamare usando le funzioni, potrebbe anche essere più leggibile.

    
risposta data 06.02.2015 - 23:35
fonte

Leggi altre domande sui tag