函数式编程是否可以替代依赖项注入模式?


21

我最近读了一本书,名为《用C#进行函数式编程》,我发现函数式编程的不可变和无状态本质可以实现与依赖注入模式相似的结果,并且可能甚至是更好的方法,尤其是在单元测试方面。

如果对这两种方法都有经验的人可以分享他们的思想和经验以回答主要问题,我将不胜感激:函数式编程是否可以替代依赖项注入模式?


10
这对我来说没有多大意义,不变性并不能消除依赖性。
Telastyn

我同意它不会删除依赖项。我的理解可能是错误的,但是我做出了这种推断,因为如果无法更改原始对象,则必须将其传递(注入)到使用它的任何函数中。
Matt Cashatt'Mar


5
还有“ 如何将OO程序员引入喜欢的函数式编程”中,它实际上是从OO和FP角度对DI进行的详细分析。
罗伯特·哈维

1
这个问题,它链接到的文章以及公认的答案也可能会有用:stackoverflow.com/questions/11276319/… 忽略可怕的Monad这个词。正如Runar在他的回答中指出的那样,在这种情况下,它不是一个复杂的概念(仅仅是一个函数)。
itsbruce 2015年

Answers:


27

依赖管理是OOP中的一个大问题,其原因有两个:

  • 数据和代码的紧密耦合。
  • 普遍使用副作用。

大多数面向对象的程序员都认为,数据和代码的紧密耦合完全有益,但要付出代价。在任何范例中,管理通过层的数据流都是编程中不可避免的一部分。耦合数据和代码会增加一个额外的问题,即如果您想在某个特定位置使用某个函数,则必须找到一种将其对象传递至该位置的方法。

使用副作用会产生类似的困难。如果您对某些功能使用了副作用,但又希望能够交换其实现,则除了注入该依赖关系外,您别无选择。

以垃圾邮件发送程序为例,该程序将网页抓取为电子邮件地址,然后通过电子邮件将其发送出去。如果您有DI的心态,那么现在您正在考虑将要封装在接口后面的服务,以及哪些服务将注入到哪里。我将把该设计留给读者练习。如果您有FP思维定势,那么现在您正在考虑最低功能层的输入和输出,例如:

  • 输入网页地址,输出该页面的文字。
  • 输入页面的文本,输出该页面的链接列表。
  • 输入页面的文本,输出该页面上的电子邮件地址列表。
  • 输入电子邮件地址列表,输出已删除重复项的电子邮件地址列表。
  • 输入电子邮件地址,然后为该地址输出垃圾邮件。
  • 输入垃圾邮件,输出SMTP命令以发送该电子邮件。

当考虑输入和输出时,没有函数依赖性,只有数据依赖性。这就是使它们如此容易进行单元测试的原因。您的下一层安排将一个功能的输出馈入下一个功能的输入,并可以根据需要轻松地交换各种实现。

从非常真实的意义上讲,函数式编程自然会促使您始终反转函数的依赖关系,因此,事后您通常不必采取任何特殊措施。当您这样做时,诸如高阶函数,闭包和部分应用程序之类的工具使使用更少的样板变得更容易完成。

请注意,问题本身并不是依赖项本身。依赖关系指出了错误的方式。下一层可能具有以下功能:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

对于这一层,将这样的依赖项进行硬编码是完全可以的,因为它的唯一目的是将较低层的功能粘合在一起。交换实现就像创建不同的组合一样简单:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

由于没有副作用,使这种简单的重组成为可能。下层功能彼此完全独立。下一层可以processText根据一些用户配置选择实际使用的层:

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

同样,这不是问题,因为所有依赖项都指向一种方式。我们不需要反转某些依赖关系就可以使它们全部指向相同的方向,因为纯函数已经迫使我们这样做。

请注意,通过config向下传递至最低层而不是在顶部进行检查,可以使此过程更加紧密。FP不会阻止您执行此操作,但是如果您尝试这样做,它的确会使您更加烦恼。


3
“使用副作用会带来类似的困难。如果对某些功能使用副作用,但又想交换其实现,则除了注入依赖关系外,您别无选择。” 我认为副作用与此无关。如果要交换Haskell中的实现,则仍然必须进行依赖注入。对类型类进行解糖,您就将接口作为每个函数的第一个参数传递。
2015年

2
问题的症结在于,几乎每种语言都迫使您硬编码对其他代码模块的引用,因此交换实现的唯一方法是在各处使用动态分配,然后您就不得不在运行时解决依赖关系。模块系统可以让您在类型检查时表达依赖关系图。
2015年

@Doval-感谢您的有趣和发人深省的评论。我可能误会了您,但是我从您的评论中推断出,如果我要使用一种功能风格的编程方式来实现DI风格(在传统的C#意义上),那我是正确的,那么我将避免与运行时相关的调试失败解决依赖性?
Matt Cashatt'Mar

@MatthewPatrickCashatt这与样式或范例无关,而与语言功能有关。如果该语言不支持将模块作为一流的东西,那么您将必须进行某种形式的动态分派和依赖注入以交换实现,因为无法静态表示依赖关系。换句话说,如果您的C#程序使用字符串,则它对具有硬编码依赖性System.String。模块系统可以让您替换System.String为变量,这样就不会对字符串实现的选择进行硬编码,但仍可以在编译时解决。
2015年

8

功能编程是依赖注入模式的可行替代方案吗?

这使我感到奇怪。函数式编程方法在很大程度上与依赖项注入有关。

当然,具有不变的状态会导致副作用或将类状态用作函数之间的隐式契约,从而使您不致作弊。它使数据传递更加明确,我认为这是依赖项注入的最基本形式。传递函数的函数式编程概念使操作变得更加容易。

但这并不能消除依赖性。您的操作仍然需要状态可变时所需的所有数据/操作。而且您仍然需要以某种方式获得那些依赖。因此,我不会说功能编程方法完全可以取代 DI,因此别无选择。

如果有的话,他们只是向您展示了不良的OO代码如何创建隐式依赖关系,这是程序员很少考虑的。


再次感谢您为对话做出的贡献,Telastyn。正如您所指出的那样,我的问题不是很好理解的(我的话),但是由于这里的反馈,我开始更好地了解这是什么激发了我的脑筋:我们都同意(我认为)如果没有DI,单元测试可能是一场噩梦。不幸的是,由于DI可以在运行时解决依赖关系,因此使用DI(特别是与IoC容器一起使用)会创建新的调试梦night。与DI相似,FP使单元测试更加容易,但是没有运行时依赖性问题。
马特Cashatt

(续上)。。无论如何,这是我目前的理解。如果我错过商标,请告诉我。我不介意承认我只是这里的巨人中的凡人!
Matt Cashatt'Mar

@MatthewPatrickCashatt-DI不一定表示运行时相关性问题,正如您注意到的那样,这是可怕的。
Telastyn 2015年

7

您问题的快速答案是:

但是,正如其他人所断言的那样,这个问题结合了两个彼此无关的概念。

让我们一步一步地做。

DI导致非功能样式

函数编程的核心是纯函数-将输入映射到输出的函数,因此对于给定的输入,您始终可以获得相同的输出。

DI 通常意味着您的装置不再是纯净的,因为输出可能会因进样而变化。例如,在以下函数中:

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

getBookedSeatCount(一个函数)可能会有所不同,对于相同的给定输入,会产生不同的结果。这也使bookSeats不纯。

对此有一些例外-您可以注入实现相同输入-输出映射的两种排序算法之一,尽管使用的算法不同。但是这些都是例外。

系统不能是纯粹的

正如功能编程源中所断言的那样,系统不能纯净的事实同样被忽略。

系统必须具有副作用,其中显而易见的示例是:

  • 用户界面
  • 数据库
  • API(在客户端-服务器架构中)

因此,系统的一部分必须包含副作用,并且该部分还可能包含命令式风格或OO风格。

壳核范式

借用Gary Bernhardt关于边界的精彩演讲中的术语,一个好的系统(或模块)体系结构将包括以下两层:

  • 核心
    • 纯功能
    • 分枝
    • 没有依赖
  • 贝壳
    • 不纯(副作用)
    • 没有分支
    • 依存关系
    • 可能势在必行,涉及OO风格等。

关键要点是将系统“拆分”为纯部分(核心)和不纯部分(外壳)。

尽管提供了稍微有缺陷的解决方案(和结论),但Mark Seemann的文章提出了完全相同的概念。Haskell实现特别具有洞察力,因为它表明可以使用FP来完成所有工作。

DI和FP

即使您的应用程序很纯,使用DI也是完全合理的。关键是将DI限制在不纯的外壳中。

一个示例将是API存根-您需要在生产中使用真正的API,但在测试中使用存根。坚持壳核心模型将在这里大有帮助。

结论

因此,FP和DI并非完全替代。您可能同时在系统中拥有这两个建议,建议是确保将系统的纯净部分和不纯净部分分开,FP和DI分别驻留在该部分中。


当您提到shell-core范例时,如何在shell中实现无分支?我可以想到许多示例,其中应用程序需要基于值来做一件不纯的事情或另一件要做的事情。此无分支规则适用于Java之类的语言吗?
jrahhali

@jrahhali请参阅Gary Bernhardt的演讲以获取详细信息(答案中链接)。
伊扎基

另一个relavent塞曼系列blog.ploeh.dk/2017/01/27/...
JK。

1

从OOP的角度来看,可以将功能视为单方法接口。

接口比功能更强大。

如果您使用功能性方法并进行大量DI,那么与使用OOP方法相比,您将为每个依赖项获得更多候选。

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

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.

3
可以包装任何类来实现该接口,因此“更强的契约”并不是很强大。更重要的是,为每个函数指定不同的类型将使其无法进行函数组合。
2015年

函数式编程并不意味着“使用高阶函数进行编程”,它是指范围更广的概念,高阶函数只是该范例中有用的一种技术。
Jimmy Hoffa'3
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.