J'ai récemment lu un livre intitulé Programmation fonctionnelle en C # et il me semble que la nature immuable et sans état de la programmation fonctionnelle produit des résultats similaires aux modèles d'injection de dépendance et est peut-être même une meilleure approche, en particulier dans en ce qui concerne les tests unitaires.
Je serais reconnaissant si quelqu'un qui a de l'expérience avec les deux approches pouvait partager ses pensées et ses expériences afin de répondre à la question principale: La programmation fonctionnelle est-elle une alternative viable aux modèles d'injection de dépendance?
La gestion des dépendances est un gros problème dans OOP pour les deux raisons suivantes:
La plupart des programmeurs OO considèrent que le couplage étroit des données et du code est tout à fait bénéfique, mais cela a un coût. La gestion du flux de données à travers les couches est une partie inévitable de la programmation dans tout paradigme. Le couplage de vos données et de votre code ajoute le problème supplémentaire que si vous souhaitez utiliser une fonction à un certain point, vous devez trouver un moyen pour que son objet atteigne ce point.
L'utilisation d'effets secondaires crée des difficultés similaires. Si vous utilisez un effet secondaire pour certaines fonctionnalités, mais que vous souhaitez pouvoir échanger son implémentation, vous n'avez pratiquement pas d'autre choix que d'injecter cette dépendance.
Prenons comme exemple un programme de spammeur qui gratte les pages Web des adresses e-mail puis les envoie par e-mail. Si vous avez un état d'esprit DI, en ce moment, vous pensez aux services que vous encapsulerez derrière les interfaces, et quels services seront injectés où. Je vais laisser cette conception comme un exercice pour le lecteur. Si vous avez un état d'esprit FP, en ce moment, vous pensez aux entrées et sorties pour la couche de fonctions la plus basse, comme:
Lorsque vous pensez en termes d'entrées et de sorties, il n'y a pas de dépendances de fonction, seulement des dépendances de données. C'est ce qui les rend si faciles à tester unitairement. Votre couche suivante organise la sortie d'une fonction à alimenter dans l'entrée de la suivante et peut facilement échanger les différentes implémentations selon les besoins.
Dans un sens très réel, la programmation fonctionnelle vous pousse naturellement à toujours inverser vos dépendances de fonction, et donc vous n'avez généralement pas à prendre de mesures spéciales pour le faire après coup. Lorsque vous le faites, des outils tels que des fonctions d'ordre supérieur, des fermetures et des applications partielles facilitent l'exécution avec moins de passe-partout.
Notez que ce ne sont pas les dépendances elles-mêmes qui posent problème. Ce sont des dépendances qui pointent dans le mauvais sens. La couche suivante peut avoir une fonction comme:
processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses
Il est parfaitement normal que cette couche ait des dépendances codées en dur comme celle-ci, car son seul but est de coller les fonctions de la couche inférieure ensemble. L'échange d'une implémentation est aussi simple que de créer une composition différente:
processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses
Cette recomposition facile est rendue possible par un manque d'effets secondaires. Les fonctions de la couche inférieure sont complètement indépendantes les unes des autres. La couche suivante peut choisir quelle processText
est réellement utilisée en fonction d'une configuration utilisateur:
actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText
Encore une fois, ce n'est pas un problème car toutes les dépendances pointent dans un sens. Nous n'avons pas besoin d'inverser certaines dépendances pour les faire pointer toutes de la même manière, car les fonctions pures nous y obligent déjà.
Notez que vous pouvez rendre cela beaucoup plus couplé en passant config
jusqu'au calque le plus bas au lieu de le vérifier en haut. FP ne vous empêche pas de le faire, mais cela a tendance à le rendre beaucoup plus ennuyeux si vous essayez.
la programmation fonctionnelle est-elle une alternative viable aux modèles d'injection de dépendance?
Cela me semble être une question étrange. Les approches de programmation fonctionnelle sont largement tangentielles à l'injection de dépendances.
Bien sûr, avoir un état immuable peut vous pousser à ne pas "tricher" en ayant des effets secondaires ou en utilisant l'état de classe comme contrat implicite entre les fonctions. Cela rend le transfert de données plus explicite, ce qui, je suppose, est la forme la plus élémentaire d'injection de dépendance. Et le concept de programmation fonctionnelle de passer des fonctions rend cela beaucoup plus facile.
Mais cela ne supprime pas les dépendances. Vos opérations ont toujours besoin de toutes les données/opérations dont elles avaient besoin lorsque votre état était modifiable. Et vous devez toujours obtenir ces dépendances d'une manière ou d'une autre. Je ne dirais donc pas que la programmation fonctionnelle approche remplacer DI du tout, donc il n'y a aucune sorte d'alternative.
Si quoi que ce soit, ils viennent de vous montrer à quel point le code OO peut créer des dépendances implicites auxquelles les programmeurs pensent rarement.
La réponse rapide à votre question est: Non .
Mais comme d’autres l’ont affirmé, la question associe deux concepts quelque peu indépendants.
Faisons cette étape par étape.
Au cœur de la programmation des fonctions se trouvent des fonctions pures - des fonctions qui mappent l'entrée à la sortie, de sorte que vous obtenez toujours la même sortie pour une entrée donnée.
DI typiquement signifie que votre unité n'est plus pure car la sortie peut varier en fonction de l'injection. Par exemple, dans la fonction suivante:
const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }
getBookedSeatCount
(une fonction) peut varier, produisant des résultats différents pour la même entrée donnée. Cela rend également bookSeats
impur.
Il existe des exceptions à cela - vous pouvez injecter l'un des deux algorithmes de tri qui implémentent le même mappage entrée-sortie, bien qu'en utilisant des algorithmes différents. Mais ce sont des exceptions.
Le fait qu'un système ne peut pas être pur est également ignoré comme cela est affirmé dans les sources de programmation fonctionnelles.
Un système doit avoir des effets secondaires, les exemples évidents étant:
Une partie de votre système doit donc impliquer des effets secondaires et cette partie peut également impliquer un style impératif, ou un style OO.
En empruntant les termes de le superbe discours de Gary Bernhardt sur les frontières , une bonne architecture de système (ou module) comprendra ces deux couches:
La clé à retenir est de "diviser" le système en sa partie pure (le cœur) et la partie impure (le Shell).
Bien qu'offrant une solution (et une conclusion) légèrement défectueuse, cet article de Mark Seemann propose le même concept. L'implémentation Haskell est particulièrement intéressante car elle montre que tout peut être fait en utilisant FP.
L'emploi de DI est parfaitement raisonnable même si la majeure partie de votre application est pure. La clé est de confiner l'ID dans la coquille impure.
Un exemple sera les stubs API - vous voulez la vraie API en production, mais utilisez des stubs dans les tests. Adhérer au modèle Shell-core aidera beaucoup ici.
Donc FP et DI ne sont pas exactement des alternatives. Vous êtes susceptible d'avoir les deux dans votre système, et le conseil est d'assurer la séparation entre la partie pure et impure du système, où FP et DI résident respectivement.
Du point de vue OOP), les fonctions peuvent être considérées comme des interfaces à méthode unique.
L'interface est un contrat plus fort qu'une fonction.
Si vous utilisez une approche fonctionnelle et faites beaucoup de DI, alors en comparaison avec une approche OOP vous obtiendrez plus de candidats pour chaque dépendance.
void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.
contre
void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.