Quando si decompone una funzione grande, come posso evitare la complessità dalle sottofunzioni extra?

5

Dire che ho una grande funzione come la seguente:

function do_lots_of_stuff(){

    { //subpart 1
      ...
    }

    ...

    { //subpart N
      ...
    }
}

un modello comune è quello di scomporlo in sottofunzioni

function do_lots_of_stuff(){
    subpart_1(...)
    subpart_2(...)
    ...
    subpart_N(...)
}

Di solito trovo che la decomposizione abbia due vantaggi principali:

  1. La funzione scomposta diventa molto più piccola. Questo può aiutare le persone a leggerlo senza perdersi nei dettagli.
  2. I parametri devono essere passati esplicitamente alle sottofunzioni sottostanti, invece di essere implicitamente disponibili solo per essere inclusi nell'ambito. Questo può aiutare a migliorare la leggibilità e la modularità in alcune situazioni.

Tuttavia, trovo anche che la decomposizione abbia alcuni svantaggi:

  1. Non ci sono garanzie che le sottofunzioni "appartengano" a do_lots_of_stuff quindi non c'è nulla che impedisca a qualcuno di chiamarle accidentalmente da un posto sbagliato.
  2. La complessità di un modulo cresce in modo quadratico con il numero di funzioni che aggiungiamo ad esso. (Ci sono più modi possibili in cui le cose si possono chiamare)

Quindi:

Ci sono utili convenzioni o stili di codifica che mi aiutino a bilanciare i pro ei contro della scomposizione delle funzioni o dovrei semplicemente usare un editor con il codice che si piega e chiamarlo un giorno?

EDIT: Questo problema si applica anche al codice funzionale (anche se in modo meno urgente). Ad esempio, in un'impostazione funzionale avremmo le sottoparti che restituiscono i valori che sono combinati alla fine e il problema di decomposizione di avere molte sottofunzioni che possono utilizzare l'un l'altro è ancora presente.

Non possiamo sempre presumere che il dominio del problema possa essere modellato su alcuni piccoli tipi semplici con solo poche funzioni altamente ortogonali. Ci saranno sempre algoritmi complicati o lunghi elenchi di regole aziendali che vogliamo ancora essere in grado di gestire correttamente.

function do_lots_of_stuff(){
   p1 = subpart_1()
   p2 = subpart_2()
   pN = subpart_N()
   return assembleStuff(p1, p2, ..., pN)
}
    
posta hugomg 20.06.2012 - 20:15
fonte

5 risposte

9

Mantieni ogni funzione il più semplice possibile.

Pensaci in termini semplici, il modo in cui una funzione deve essere :

  • ottiene input da 0 a N,
  • restituisce 0 o 1 risultato (possibile un composto o una raccolta),
  • e non è legato allo stato.

Seseguileespressioni programmazione funzionale , ti sbarazzi della maggior parte di queste domande ti stai chiedendo. Certo, la tua classe diventerà più grande in termini di numero di funzioni. Ma se i metodi non sono legati a ciascuno da cambiamenti di stato interni, diventano più facili da capire, gestire e comporre per ottenere un risultato finale.

Inoltre,cercadifornirelorogliaccessiappropriatiinbasealledecisioniprogettualisoprariportate.Glihelpersarannocomunementedichiaratistatici(esenonsembranoaverbisognodiessereprivatiopossonoessereriutilizzati,potrebberoessereestrattiadunaclassehelper),ilchedàunstrongsuggerimentoaglialtrisviluppatori:questacosaèpensataperessereindipendenteeprivodieffetticollaterali.

Ripetiiseguentimantrapermirarea purezza :

  • La mia funzione deve essere:
    • breve ,
    • effetti collaterali gratis ,
    • realizzare solo una funzione .
  • La mia funzione deve essere strict on output . [1]
  • La mia funzione deve essere verificabile e testata .
  • La mia funzione deve essere leggibile e essere letta come un'espressione di linguaggio naturale .
  • La mia funzione deve essere documentata . [2]
  • La mia funzione deve essere null-ostile .

[1] Indipendentemente dal fatto che l'input debba essere rigoroso o indulgente dipende dal codice del consumatore o dal codice della libreria.
[2 ] Conteggi di autocomposizione, anche i commenti per le parti difficili contano.

Naturalmente, se ci si trova in una base di codice generalmente non orientata al FP, non si riesce a evitare lo stato mutabile condiviso per sempre, ma è molto buono, sensibile e no- Linee guida BS da seguire. Anche se ti capita di sbagliare componendo eccessivamente la modularità e complicando la tua classe, sarà ancora più facile riprenderti da lì e rifattorizzare di nuovo che da un gigantesco dump di codice con elevata complessità e accoppiamento stretto.

Per quanto riguarda le regole della composizione delle tue funzioni, queste sono le tue regole aziendali. Sono dettate da ciò che vuoi ottenere, non c'è un modo automatico di determinarlo per te.

    
risposta data 20.06.2012 - 21:45
fonte
2

Solo perché le tue funzioni possono chiamarsi l'una con l'altra una varietà di modi non significa che lo facciano. La complessità del tuo programma non è improvvisamente quadratica perché hai nominato i blocchi di codice che hai eseguito prima ...

Tutti gli altri consigli sono buoni, ma manca un problema chiave: hai una funzione che fa un sacco di cose. Rompere quella funzione in una serie di chiamate di funzione non lo fa fare meno cose, ma semplicemente spinge la roba in giro. Avere una funzione che fa troppo è un segno che hai malamente astratto il tuo problema. Risolvi la tua astrazione in modo che la funzionalità rappresenti una mappatura migliore di ciò che devi fare e la funzione sarà naturalmente meno complessa.

    
risposta data 20.06.2012 - 22:26
fonte
0

Più linguaggi dinamici come javascript e python hanno la capacità di dichiarare funzioni all'interno delle funzioni

function a(param1, param2) {
  function helperFunc1() {
    return param1 % param2 * param2
  }
  function helperFunc2(param3) {
    return (param1 - param2) * param3
  }
  return helperFunc1() + helperFunc2(2) - helperFunc2(1.5)
}

Queste funzioni interne dovrebbero avere accesso a tutte le vars all'interno della funzione principale nascondendole dall'esterno, come farebbe una classe.

    
risposta data 20.06.2012 - 21:48
fonte
0

Suggerirei di passare a uno stile di programmazione più funzionale. Il fatto che tu stia parlando di "chiamare nell'ordine sbagliato" e di mostrare funzioni che non restituiscono dati, significa che stai lavorando con funzioni molto impure.

L'approccio migliore è quello di lavorare con funzioni pure:

1) Rifiuta le variabili di stato globali. Sono cattivi. Detto abbastanza.

2) Ogni funzione dovrebbe prendere argomenti e restituire qualcosa.

3) Dati gli stessi argomenti, le funzioni dovrebbero generare gli stessi dati.

Quando segui questi obiettivi scoprirai che il tuo codice diventa molto più pulito, le funzioni possono chiamare altre funzioni senza timore di effetti collaterali perché le funzioni non sono abilitate a fare confusione con lo stato globale.

Ora, in un'applicazione web / gui, a un certo punto dovrai modificare lo stato globale, in tal caso puoi infrangere le regole precedenti in piccoli casi marginali.

A questo punto, le funzioni di decomposizione sono estremamente semplici. È sufficiente iniziare a prendere parti logiche nel codice e trasformarle in funzioni. Quando trovi il codice duplicato, unisci le funzioni in un'unica funzione. Se ti ritrovi a scrivere lo stesso codice più e più volte, allora trova un modo per scriverlo come una singola funzione.

Ad esempio, in una pseudo lingua, un ciclo for potrebbe essere riscritto come una mappa. Quindi, invece di questo:

arr = [1, 2, 3, 4, 5]

for (x = 0; x < arr.length; x ++)
{
   arr[x] += 1
}

Potresti scrivere una funzione mappa una volta:

function map(func, arr)
{
   out = []
   for (x = 0; x < arr.length; x ++)
   {
     out.add(func(arr[x]))
   }
   return out;
}

E in futuro, invece di un ciclo for, scrivi semplicemente:

function inc (x) { return x + 1; }

map(inc, [1 2 3 4])

Quindi invece di un ciclo continuo imperativo per caso singolo. Ora hai 2 funzioni che puoi riutilizzare in migliaia di posti nel tuo programma.

Risorse:

link

link

EDIT:

Aggiorna per indirizzare le modifiche all'OP:

Nel tuo esempio, ti suggerisco di usare le mappe in una situazione come questa. Quindi nel tuo esempio:

function foo(arg) 
{
    data = {"p1": func1(arg),
            "p2": func2(arg),
            "pN": funcN(arg)}
    return assemble(data);
}

Quindi, in generale, prova ad iniziare con una mappa hash vuota e costruisci la struttura applicando piccole funzioni fino a quando non crei l'intero set di dati.

O in un modo molto più pulito:

function reduce(fn, data, init)
{
   for (x = 0; x < data.length; x ++ 1)
       init = fn(init, data[x])
   return init
}

Quindi semplicemente:

function foo(arg)
{
    fns = [p1, p2, pN]
    data = reduce((l, f) -> f(l), fns, {})
    reduce assembleData(data)
}

Utilizzando la riduzione puoi anche implementare facilmente le regole aziendali. Concentrati sui dati e sulla programmazione funzionale e inizierai a vedere quanto possano essere semplici queste cose.

Ogni giorno trascorro quasi 10 ore di programmazione sia in C # che in Clojure, quindi ho visto entrambi gli estremi e l'approccio funzionale semplifica notevolmente le cose.

Forse un esempio del mondo reale sarebbe in ordine?

    
risposta data 20.06.2012 - 21:07
fonte
0

Suggerirei che l'OP consideri il refactoring del problema e della soluzione prima di iniziare a scrivere il codice. Se qualcosa è così complesso che inizi a preoccuparti di questi problemi, devi davvero saltare fuori dalla scatola e osservare il problema da una diversa angolazione.

Solo per divertimento, forse puoi creare una DSL per risolvere il tuo problema ^^

    
risposta data 20.06.2012 - 22:28
fonte