La gestione delle dipendenze è un grosso problema in OOP per i seguenti due motivi:
- Lo stretto accoppiamento di dati e codice.
- Uso ubiquitario di effetti collaterali.
La maggior parte dei programmatori OO ritiene che lo stretto accoppiamento di dati e codice sia del tutto vantaggioso, ma comporta un costo. La gestione del flusso di dati attraverso i livelli è una parte inevitabile della programmazione in qualsiasi paradigma. L'accoppiamento di dati e codice aggiunge l'ulteriore problema che se si desidera utilizzare una funzione in un determinato punto, è necessario trovare un modo per portare il suo oggetto a quel punto.
L'uso di effetti collaterali crea difficoltà simili. Se usi un effetto collaterale per alcune funzionalità, ma vuoi essere in grado di scambiarne l'implementazione, praticamente non hai altra scelta che iniettare quella dipendenza.
Consideriamo ad esempio un programma di spammer che raschia le pagine Web per gli indirizzi e-mail, quindi le 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 esercizio per il lettore. Se hai una mentalità FP, in questo momento stai pensando agli ingressi e alle uscite per il livello più basso di funzioni, come:
- Immettere un indirizzo per la pagina Web, generare il testo di quella pagina.
- Inserisci il testo di una pagina, genera un elenco di collegamenti da quella pagina.
- Inserisci il testo di una pagina, visualizza un elenco di indirizzi e-mail su quella pagina.
- Inserisci un elenco di indirizzi e-mail, genera un elenco di indirizzi e-mail con i duplicati rimossi.
- Inserisci un indirizzo e-mail, invia un'e-mail di spam per quell'indirizzo.
- Inserisci un'email di spam, invia i comandi SMTP per inviare l'e-mail.
Quando si pensa in termini di input e output, non ci sono dipendenze di funzioni, ma solo dipendenze di 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 sostituire le varie implementazioni secondo necessità.
In un senso molto reale, la programmazione funzionale ti spinge naturalmente a invertire sempre le dipendenze delle tue funzioni, e quindi di solito non devi prendere alcuna misura speciale per farlo dopo il fatto. Quando lo fai, strumenti come le funzioni di ordine superiore, le chiusure e l'applicazione parziale rendono più facile eseguire con meno boilerplate.
Si noti che non sono le dipendenze stesse ad essere problematiche. Sono le dipendenze che indicano la direzione sbagliata. Il livello successivo può avere una funzione come:
processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses
Va perfettamente bene che questo livello abbia dipendenze codificate in questo modo, perché il suo unico scopo è incollare insieme le funzioni del 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 del livello inferiore sono completamente indipendenti l'una dall'altra. Il livello successivo può scegliere quale processText
viene effettivamente utilizzato in base a una configurazione utente:
actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText
Ancora una volta, non è un problema perché tutte le dipendenze indicano un modo. Non è necessario invertire alcune dipendenze per farle puntare tutte allo stesso modo, perché le funzioni pure ci hanno già costretto a farlo.
Nota che potresti renderlo molto più accoppiato passando config
al livello più basso invece di controllarlo in alto. FP non ti impedisce di farlo, ma tende a renderlo molto più fastidioso se ci provi.