Dependency injection 过程编程中的依赖注入

Dependency injection 过程编程中的依赖注入,dependency-injection,procedural-programming,Dependency Injection,Procedural Programming,假设我决定用C或任何其他过程编程语言编写一个大型应用程序。它的函数具有如下所示的调用依赖项: A | +-------------+ | | B1 B2 | | +------+ +------+ | | | | C11 C12 C21 C22 显然,对leaves函数C11、C12、C21和C22进行单元测试非常简单:设置输入、调用函数、断言输出 但是,为B1、B2

假设我决定用C或任何其他过程编程语言编写一个大型应用程序。它的函数具有如下所示的调用依赖项:

A
|
+-------------+
|             |
B1            B2
|             |
+------+      +------+
|      |      |      |
C11    C12    C21    C22
显然,对leaves函数C11、C12、C21和C22进行单元测试非常简单:设置输入、调用函数、断言输出

但是,为B1、B2和A实现良好的单元测试,正确的策略是什么

建议将
B1
(以及
B2
)声明如下

// Declare B1 with dependency injection for invoking C11 and C12.
int B1(int input, int (*c11)(int), int(*c12)(int));
但是,如果我有很多层的调用,那么这种策略似乎是不可伸缩的。想象一下
A
的声明会是什么样子:

int A(int input, int (*b1)(int, int (*)(int), int(*)(int)), 
                 int(*b2)(int, int (*)(int), int(*)(int)),
                 int (*c11)(int),
                 int (*c12)(int),
                 int (*c21)(int),
                 int (*c22)(int));
恶心!一定有更好的办法

有时,我觉得DI和其他类似的模式,旨在促进模块化和易于维护,实际上阻碍了代码的清晰性,并使本应简单的编码复杂化为无意义的抽象和复杂的间接


C语言中的大型软件项目,如Perl和Ruby,是如何处理单元测试的?

我喜欢这个问题。在过程语言中会变得有点棘手。。。。但我认为你可以借用OO世界的一个想法,在OO世界中,人们经常使用构造函数重载来处理一些DI工作。因此,例如,您将使用默认构造函数像往常一样设置所有依赖项。。。但是还有另一个构造函数,它允许注入依赖项

既然你是程序性的。。。我想你可以使用函数重载来处理这个问题。此外,当您正在测试时,您只需要在调用。。。因此,您可以为此目的简化DI。换句话说,如果你真的只在单元测试中使用DI,那么你不必注入整个依赖树,只需注入第一级依赖

所以从某个角度来看,你可能

int A(int input){
// create function point to b1 & b2 and call "return A(input, {pointer to b1},{pointer to b2})"
}

请原谅我的psuedo代码,我已经很久没有使用C了。

您可以将依赖项放在C-struct中,它将成为函数调用的一个参数。在c中,这类似于文件api,其中第一个参数始终是文件句柄

A
只需要调用
B1
B2
。它不需要了解C级别的任何内容

为了测试
a
,您可以将不同版本的函数
B1
B2
注入
a


这将
A
从需要整个结构中分离出来,并意味着您可以单独测试每个函数。

如果您只需要DI进行单元测试,则可以使用链接器进行测试

我的意思是,函数B1和B2在头中声明并由函数a使用,因此B函数的实现由链接器提供。您只需要为单元测试提供不同的C文件。这应该不是一个大问题,因为您可能有自己的makefile用于单元测试


如果在运行时需要动态依赖项解析,那么应该使用工厂模式(返回函数指针的函数)作为函数指针,并在需要时从工厂中提取它们。工厂可能会根据全局上下文决定返回什么函数。

您可以在不使用DI的情况下对B1、B2和A进行适当的单元测试。与leaf函数一样,B1和B2具有有效的输入和输出,您可以对其进行测试,与A相同。B1可以在内部使用C11和C12来帮助您完成其单元测试,这并不意味着在不需要灵活性的情况下必须注入它们。

但它如何扩展?在规范中,
A
将涵盖
B1
B2
的功能,这两种功能也涵盖
C11
C12
C21
C22
的功能。因此,叶函数
C11
等的所有测试都必须对它们上面的所有节点函数重复。现在,想象一组深层次的代码。您可能会发现编写测试非常费劲。@Kirakun在实际情况中,A与B1和B2做的不一样,它添加了一些额外的功能。如果您对B1和B2进行了测试,那么您可以对代码的这一部分充满信心。对于A,您只需测试您需要确信的B1和B2是如何连接在一起的。@t尝试IMHO单元测试(在上述场景中)意味着测试
A
,而不测试(即调用)B级函数,更不用说C级函数了。B级可能有争议,但想象一下
C11
连接到数据库。您不希望
a
(或
B1
)的单元测试调用实际的
C11
。相反,应该调用某种mock-
C11
。这是一个有趣的想法。我得试试。你也可以用C语言编写OO,而不仅仅是程序性的。只需声明一个结构,其中数据成员和函数将该结构作为第一个参数(伪此指针)。在c文件中实现,并且您自己创建了一个伪类。多态性和类似的事情是可能的,只是很复杂;)我们在其他语言中也做了类似的事情,我们有一个服务提供者,其中包含各种依赖关系的实现。它还有一个额外的好处,就是对函数签名的更改更少,重构更容易。