依赖倒置与高阶函数有何关系?


41

今天我刚刚看到这篇文章,它描述了SOLID原理在F#开发中的相关性-

F#和设计原则– SOLID

在谈到最后一个问题时-“依赖倒置原则”,作者说:

从功能的角度来看,这些容器和注入概念可以通过简单的高阶函数或内置在语言中的“中间钻孔”模式来解决。

但是他没有进一步解释。因此,我的问题是,依赖项反转与高阶函数有何关系?

Answers:


38

OOP中的依赖倒置意味着您针对接口进行编码,该接口随后由对象中的实现提供。

支持高级语言功能的语言通常可以通过将行为作为函数传递,而不是通过在OO感觉中实现接口的对象来解决简单的依赖关系反转问题。

在这种语言中,函数的签名可以成为接口,并且可以传递函数而不是传统对象来提供所需的行为。中间图案中的孔就是一个很好的例子。

它使您可以用更少的代码和更富表现力的方式实现相同的结果,因为您无需实现符合(OOP)接口的整个类即可为调用者提供所需的行为。相反,您可以只传递一个简单的函数定义。简而言之:当使用高阶函数时,代码通常更易于维护,更具表现力和灵活性。

C#中的一个例子

传统方法:

public IEnumerable<Customer> FilterCustomers(IFilter<Customer> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter.Matches(customer))
        {
            yield return customer;
        }
    }
}

//now you've got to implement all these filters
class CustomerNameFilter : IFilter<Customer> /*...*/
class CustomerBirthdayFilter : IFilter<Customer> /*...*/

//the invocation looks like this
var filteredDataByName = FilterCustomers(new CustomerNameFilter("SomeName"), customers);
var filteredDataBybirthDay = FilterCustomers(new CustomerBirthdayFilter(SomeDate), customers);

具有高阶功能:

public IEnumerable<Customer> FilterCustomers(Func<Customer, bool> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter(customer))
        {
            yield return customer;
        }
    }
}

现在,实现和调用变得不再那么麻烦了。我们不再需要提供IFilter实现。我们不再需要为过滤器实现类。

var filteredDataByName = FilterCustomers(x => x.Name.Equals("CustomerName"), customers);
var filteredDataByBirthday = FilterCustomers(x => x.Birthday == SomeDateTime, customers);

当然,这可以由LinQ在C#中完成。我只是使用此示例来说明,使用高阶函数而不是实现接口的对象更加容易和灵活。


3
很好的例子。但是,像Gulshan一样,我试图找到有关函数式编程的更多信息,并且我想知道与“面向对象的DI”相比,这种“函数式DI”是否不牺牲某些严格性和重要性。高阶签名仅声明传递的函数必须以Customer为参数并返回布尔值,而OO版本则强制传递以下事实:传递的对象过滤器(实现IFilter <Customer>)。它还使过滤器的概念明确,如果它是域的核心概念(请参阅DDD),则可能是一件好事。你怎么看 ?
guillaume31

2
@ ian31:这确实是一个有趣的话题!传递给FilterCustomer的所有内容都将隐式地充当某种过滤器。当过滤器概念是域的重要组成部分并且您需要在系统中多次使用的复杂过滤器规则时,最好封装它们。如果不是,或者只是非常低的程度,那么我的目标是技术简单和实用。
猎鹰

5
@ ian31:我完全不同意。实施IFilter<Customer>根本不强制执行。高阶函数要灵活得多,这是一个很大的好处,而能够内联编写它们是另一个巨大的好处。Lambda也更容易捕获局部变量。
DeadMG

3
@ ian31:该函数也可以在编译时进行验证。您还可以编写一个函数,对其进行命名,然后将其作为参数传递,只要它可以满足明显的合同(接客户,返回布尔)即可。您不必传递lambda表达式。因此,您可以在一定程度上弥补表达能力的不足。但是,合同及其意图并未明确表达。有时,这是一个主要的缺点。总而言之,这是表达力,语言和封装的问题。我认为您必须自己判断每种情况。
猎鹰

2
如果您强烈想弄清楚注入函数的语义,可以在C#名称函数签名中使用委托:public delegate bool CustomerFilter(Customer customer)。在纯函数式语言(如haskell)中,别名类型很简单:type customerFilter = Customer -> Bool
sara

8

如果要更改功能的行为

doThis(Foo)

你可以通过另一个功能

doThisWith(Foo, anotherFunction)

实现您想要与众不同的行为。

“ doThisWith”是一个高阶函数,因为它使用另一个函数作为参数。

例如你可能有

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)


0

比较一下:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = new LinkedList<String>();
for (String name : names) {
    if (name.startsWith("S")) {
        namesBeginningWithS.add(name);
    }
}

与:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = names.stream().filter(n <- n.startsWith("S")).collect();

第二个版本是Java 8通过提供高阶函数来减少样板代码(循环等)的方法,filter该函数允许您传递最基本的最小值(即,要注入的依赖项-lambda表达式)。


0

附带LennyProgrammers示例...

其他示例缺少的一件事是,您可以将高阶函数与部分函数应用程序(PFA)结合使用,以将依赖项绑定(或“注入”)到函数中(通过其参数列表)以创建新函数。

如果不是:

doThisWith(Foo, anotherFunction)

我们(通常在PFA的常规处理方式中)具有底层工作程序功能,如(交换arg顺序):

doThisWith( anotherFunction, Foo )

然后,我们可以像下面这样部分地应用 doThisWith:

doThis = doThisWith( anotherFunction )  // note that "Foo" is still missing, argument list is partial

这使我们以后可以像这样使用新功能:

doThis(Foo)

甚至:

doThat = doThisWith( yetAnotherDependencyFunction )
...
doThat( Bar )

另请参阅:https : //ramdajs.com/docs/#partial

...是的,加法器/乘法器是没有想象力的示例。一个更好的示例是一个函数,该函数接收消息,然后将其记录或通过电子邮件发送,具体取决于作为依赖项传递的“消费者”函数。

扩展此思想,可以将越来越长的参数列表逐渐缩小到具有越来越短的参数列表的越来越专业的功能,当然,这些功能中的任何一个都可以作为依赖项的一部分传递给其他功能。

如果您需要一堆具有多个紧密相关的操作的东西,那么OOP会很好,但是它变成了用一对公共的“ do it”方法(例如“名词王国”)制作一堆类的方法。

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.