如何在C中进行依赖注入?

rdlzhqv9  于 2023-04-19  发布在  其他
关注(0)|答案(6)|浏览(170)

我正在寻找一个很好的技术解决方案,在C中进行DI。
我已经在这里看到了一些DI问题,但我还没有看到任何实际的例子或具体的实现建议。
所以,假设我们有以下情况:
我们在C中有一组模块;我们想重构这些模块,这样我们就可以使用DI来运行单元测试等等。
每个模块实际上由一组c函数组成:
function();
模块相互依赖。即。通常你可能有一个调用,如:

int module1_doit(int x) {
  int y = module2_dosomethingelse(x);
  y += 2;
  return(y);
}

为此,DI的正确方法是什么?
可能的解决办法似乎是:

  • (1)对所有模块函数使用函数指针,并且在调用函数时执行以下操作(或类似操作):

int y = modules-〉module2-〉dossomethingelse(x);

  • (2)使用相同的符号编译多个库(mock、std等),并在正确的实现中动态链接。

(2)看起来是正确的方法,但是很难配置,而且烦人的是迫使您为每个单元测试构建多个二进制文件。
(1)看起来它可能会工作,但在某些时候,你的DI控制器会陷入这样一种情况,你需要动态调用一个通用工厂函数(void factory)(...)),其中有许多其他模块需要在运行时注入?
在C语言中有没有更好的方法来实现这一点?
做这件事的“正确”方法是什么?

bweufnob

bweufnob1#

你可以使用两种方法。你是否真的想要,正如雷夫所指出的,取决于你。
第一:在静态库中创建“动态”注入的方法。链接到库中,在测试过程中简单地替换它。瞧,方法被替换了。
第二:简单地提供基于预处理的编译时替换:

#ifndef YOUR_FLAG

    /* normal method versions */

#else

    /* changed method versions */

#endif

/* methods that have no substitute */
xeufq47z

xeufq47z2#

下面是一个如何在C中完成依赖注入的例子(基本上是OP提到的选项(2)):

// person.h file
struct Person {
    char birthday[sizeof("YYYY-MM-DD")];
};
int get_age(const Person *person);

// person.cc file
#include "person.h"
#include "get_todays_date.h" // we import get_todays_date() dependency

int get_age(const Person *person) { 
    char *today = get_todays_date();
    return compute_difference(today, person->birthday);
}

如果get_todays_date()在生产环境中执行,它应该总是返回准确的日期。
如果get_todays_date()在测试环境中执行,我们让它返回一个固定的日期,这样就可以编写以下测试:

// person_test.cc file
#include "get_todays_date.h"

const char today[] = "2020-01-06";
char* get_todays_date(void) { return today; } 

int main() {
    Person person = {.birthday = "2000-01-05"};
    assert(get_age(&person) == 20); // our test
    return 0;
}

get_todays_dateperson.h文件的依赖项
当您为生产环境编译代码时,链接包含get_todays_date()的真实的实现的转换单元(即,将get_todays_date.cc文件包含到编译过程中)
在编译代码进行测试时,不要链接对应于get_todays_date.cc的转换单元

o4tp2gmn

o4tp2gmn4#

我的结论是,在C语言中没有正确的方法来做这件事。它总是比其他语言更困难和乏味。然而,我认为重要的是不要为了单元测试而混淆你的代码。在C中将所有内容都变成函数指针听起来不错,但我认为这只会让代码在最后调试时变得可怕。
我最新的方法是让事情变得简单。我不改变C模块中的任何代码,除了文件顶部的一个小#ifdef UNIT_TESTING用于外部和内存分配跟踪。然后我将模块编译为删除所有依赖项,以便它无法链接。一旦我检查了未解决的符号,以确保它们是我想要的,我运行一个脚本来解析这些依赖项并为所有符号生成存根原型。这些都被转储到单元测试文件. YMMV中,具体取决于外部依赖项的复杂程度。
如果我需要在一个示例中模拟一个依赖项,在另一个示例中使用真实的的依赖项,或者在另一个示例中使用它,那么我最终会为一个正在测试的模块使用三个单元测试模块。拥有多个二进制文件可能不是理想的,但这是C的唯一真正选择。尽管如此,它们都可以同时运行,所以这对我来说不是问题。

xu3bshqb

xu3bshqb5#

这是Ceedling的完美用例。
Ceedling是一个伞式项目,它将Unity和CMock(以及其他东西)结合在一起,可以自动化您所描述的许多工作。
一般来说,Ceedling/Unity/CMock是一组ruby脚本,可以扫描你的代码,并根据你的模块头文件自动生成mock,还有测试运行器,可以找到所有的测试,并生成运行它们的运行器。
为每个测试套件生成一个单独的测试运行器二进制文件,并根据您在测试套件实现中的请求链接适当的模拟和真实的实现。
我最初很犹豫是否要把ruby作为一个依赖项引入到我们的构建系统中进行测试,它看起来很复杂,很神奇,但是在尝试了一下并使用自动生成的模拟代码编写了一些测试之后,我被迷住了。

wqlqzqxt

wqlqzqxt6#

这个问题有点晚了,但这是我最近工作的一个主题。
我见过的两种主要方法是使用函数指针,或者将所有依赖项移动到特定的C文件。
后者的一个很好的例子是FATFS.http://elm-chan.org/fsw/ff/en/appnote.html
fatfs的作者提供了大量的库函数,并将某些特定的依赖关系降级为库用户编写(例如串行外围接口函数)。
函数指针是另一个有用的工具,使用typedef有助于防止代码变得太难看。
以下是我的模数转换器(ADC)代码中的一些简化片段:

typedef void (*adc_callback_t)(void);

bool ADC_CallBackSet(adc_callback_t callBack)
{
    bool err = false;
    if (NULL == ADC_callBack)
    {
        ADC_callBack = callBack;
    }
    else
    {
        err = true;
    }
    return err;
}

// When the ADC data is ready, this interrupt gets called
bool ADC_ISR(void)
{
    // Clear the ADC interrupt flag
    ADIF = 0;

    // Call the callback function if set
    if (NULL != ADC_callBack)
    {
        ADC_callBack();
    }

    return true; // handled
}

// Elsewhere
void FOO_Initialize(void)
{
    ADC_CallBackSet(FOO_AdcCallback);
    // Initialize other FOO stuff
}

void FOO_AdcCallback(void)
{
    ADC_RESULT_T rawSample = ADC_ResultGet();
    FOO_globalVar += rawSample;
}

Foo的中断行为现在被注入ADC的中断服务例程。
您可以更进一步,向FOO_Initialize传递一个函数指针,这样所有依赖性问题都由应用程序管理。

//dependency_injection.h
typedef void (*DI_Callback)(void)
typedef bool (*DI_CallbackSetter)(DI_Callback)

// foo.c
bool FOO_Initialize(DI_CallbackSetter CallbackSet)
{
    bool err = CallbackSet(FOO_AdcCallback);
    // Initialize other FOO stuff
    return err;
}

相关问题