La programmazione funzionale è una valida alternativa ai modelli di iniezione di dipendenza?

18

Recentemente ho letto un libro intitolato Programmazione funzionale in C # e mi viene in mente che l'immutabile e La natura stateless della programmazione funzionale realizza risultati simili ai modelli di iniezione di dipendenza ed è forse anche un approccio migliore, specialmente per quanto riguarda i test unitari.

Sarei grato se qualcuno che ha esperienza con entrambi gli approcci possa condividere i propri pensieri ed esperienze per rispondere alla domanda principale: la Programmazione Funzionale è una valida alternativa ai modelli di iniezione di dipendenza?

    
posta Matt Cashatt 10.03.2015 - 21:24
fonte

4 risposte

23

La gestione delle dipendenze è un grosso problema in OOP per i seguenti due motivi:

  • Lo stretto accoppiamento di dati e codice.
  • Uso onnipresente di effetti collaterali

La maggior parte dei programmatori OO ritiene che l'accoppiamento stretto di dati e codice sia del tutto vantaggioso, ma comporta un costo. Gestire il flusso di dati attraverso i livelli è una parte inevitabile della programmazione in ogni paradigma. Accoppiamento i dati e codice aggiunge l'ulteriore problema che, se si desidera utilizzare una funzione di a un certo punto, si deve trovare un modo ottenere il suo oggetto a quel punto.

L'uso di effetti collaterali crea difficoltà simili. Se si utilizza un effetto collaterale per alcune funzionalità, ma si vuole essere in grado di sostituirne l'implementazione, praticamente non si ha altra scelta che quella di iniettare tale dipendenza.

Considerare come esempio un programma di spammer che gratta le pagine Web per gli indirizzi e-mail, quindi li invia per e-mail. Se hai una mentalità DI, in questo momento stai pensando ai servizi che incapsulerai dietro le interfacce e quali servizi verranno iniettati dove. Lascerò quel disegno come un esercizio per il lettore. Se hai una mentalità FP, in questo momento stai pensando agli input e output per il livello più basso di funzioni, come:

  • Inserisci un indirizzo di una pagina web, genera il testo di quella pagina.
  • Inserisci il testo di una pagina, genera un elenco di link da quella pagina.
  • Inserisci il testo di una pagina, genera un elenco di indirizzi email su quella pagina.
  • Inserisci un elenco di indirizzi email, genera un elenco di indirizzi email con i duplicati rimossi.
  • Inserisci un indirizzo email, invia un'email di spam per quell'indirizzo.
  • Inserisci un messaggio di spam, invia i comandi SMTP per inviare quell'email.

Quando si pensa in termini di input e output, non ci sono dipendenze di funzioni, solo dipendenze dei dati. Questo è ciò che li rende così facili da testare l'unità. Il tuo livello successivo organizza l'output di una funzione da inserire nell'input del successivo e può facilmente scambiare le varie implementazioni secondo necessità.

In senso molto reale, la programmazione funzionale ti spinge naturalmente a invertire sempre le dipendenze tra le funzioni, e di conseguenza non devi prendere misure speciali per farlo dopo il fatto. Quando lo fai, strumenti come le funzioni di ordine superiore, le chiusure e l'applicazione parziale rendono più facile l'esecuzione con una quantità minore di boilerplate.

Si noti che non sono le stesse dipendenze a essere problematiche. Sono le dipendenze che indicano la strada sbagliata. Il livello successivo può avere una funzione come:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

Va perfettamente bene per questo strato avere dipendenze codificate in questo modo, perché il suo unico scopo è quello di incollare insieme le funzioni di livello inferiore. Scambiare un'implementazione è semplice come creare una composizione diversa:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

Questa facile ricomposizione è resa possibile dalla mancanza di effetti collaterali. Le funzioni di livello inferiore sono completamente indipendenti l'una dall'altra. Il prossimo livello in alto può scegliere quale% diprocessText viene effettivamente utilizzato in base a qualche configurazione utente:

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

Ancora una volta, non un problema perché tutte le dipendenze puntano in un modo. Non è necessario invertire alcune dipendenze per farle puntare tutte allo stesso modo, perché le funzioni pure ci hanno già obbligato a farlo.

Nota che potresti renderlo molto più accoppiato passando config fino al livello più basso invece di controllarlo in alto. FP non ti impedisce di farlo, ma tende a renderlo molto più fastidioso se provi.

    
risposta data 11.03.2015 - 14:21
fonte
8

is Functional Programming a viable alternative to dependency injection patterns?

Questo mi sembra una domanda strana. Gli approcci alla programmazione funzionale sono in gran parte tangibili all'iniezione di dipendenza.

Certo, avere uno stato immutabile può spingerti a non "ingannare" avendo effetti collaterali o usando lo stato della classe come un contratto implicito tra le funzioni. Rende il passaggio dei dati più esplicito, che suppongo sia la forma più semplice di iniezione di dipendenza. E il concetto di programmazione funzionale di passare le funzioni lo rende molto più semplice.

Ma non rimuove le dipendenze. Le tue operazioni hanno ancora bisogno di tutti i dati / operazioni di cui avevano bisogno quando il tuo stato era mutabile. E hai ancora bisogno di ottenere quelle dipendenze lì in qualche modo. Quindi non direi che la programmazione funzionale si avvicini a sostituire DI, quindi non ci sono alternative.

Se non altro, ti hanno appena mostrato quanto il codice OO cattivo può creare dipendenze implicite di quanto raramente i programmatori pensano.

    
risposta data 11.03.2015 - 14:46
fonte
4

La risposta rapida alla tua domanda è: No .

Ma come altri hanno affermato, la domanda sposa due concetti, in qualche modo non correlati.

Facciamo questo passo dopo passo.

DI produce uno stile non funzionale

Nel nucleo della programmazione delle funzioni ci sono pure funzioni - funzioni che mappano l'input in output, in modo da ottenere sempre lo stesso output per un dato input.

DI tipicamente significa che la tua unità non è più pura poiché l'uscita può variare a seconda dell'iniezione. Ad esempio, nella seguente funzione:

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount (una funzione) può variare producendo risultati diversi per lo stesso input dato. Questo rende anche bookSeats impura.

Ci sono delle eccezioni a questo proposito: è possibile iniettare uno dei due algoritmi di ordinamento che implementano la stessa mappatura input-output, anche se utilizzano algoritmi diversi. Ma queste sono eccezioni.

Un sistema non può essere puro

Il fatto che un sistema non possa essere puro viene ugualmente ignorato come viene affermato nelle fonti di programmazione funzionale.

Un sistema deve avere effetti collaterali con gli esempi ovvi che sono:

  • UI
  • database
  • API (nell'architettura client-server)

Quindi parte del tuo sistema deve coinvolgere effetti collaterali e quella parte potrebbe anche implicare uno stile imperativo, o stile OO.

Il paradigma shell-core

Prendendo in prestito i termini da superbo discorso di Gary Bernhardt sui confini , una buona architettura di sistema (o modulo) includerà questi due strati:

  • Nucleo
    • Funzioni pure
    • Branching
    • Nessuna dipendenza
  • Shell
    • Impure (effetti collaterali)
    • Nessuna diramazione
    • Dipendenze
    • Può essere imperativo, coinvolgere lo stile OO, ecc.

Il takeaway della chiave è 'dividere' il sistema nella sua parte pura (il nucleo) e la parte impura (la shell).

Sebbene offra una soluzione leggermente errata (e conclusione), this L'articolo di Mark Seemann propone lo stesso concetto. L'implementazione di Haskell è particolarmente perspicace in quanto mostra che tutto può essere fatto usando FP.

DI e FP

L'utilizzo di DI è perfettamente ragionevole anche se la maggior parte della tua applicazione è pura. La chiave è limitare il DI all'interno della shell impura.

Un esempio sarà costituito da stub API: vuoi la vera API in produzione, ma usa stub in testing. Aderire al modello shell-core ti aiuterà molto qui.

Conclusione

Quindi FP e DI non sono esattamente alternative. È probabile che tu abbia entrambi nel tuo sistema, e il consiglio è di assicurare la separazione tra la parte pura e impura del sistema, dove FP e DI risiedono rispettivamente.

    
risposta data 03.05.2017 - 16:16
fonte
1

Dal punto di vista OOP, le funzioni possono essere considerate come interfacce a metodo singolo.

L'interfaccia è un contratto più strong di una funzione.

Se stai utilizzando un approccio funzionale e fai molti DI allora, rispetto all'utilizzo di un approccio OOP otterrai più candidati per ogni dipendenza.

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

vs

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.
    
risposta data 11.03.2015 - 18:45
fonte