Как сделать внедрение зависимости в C? - PullRequest
18 голосов
/ 08 мая 2011

Я ищу хорошее техническое решение для DI в C.

Я уже видел некоторые вопросы DI здесь, но я не видел ни одного с реальными примерами или конкретными предложениями по реализации.

Итак, допустим, у нас следующая ситуация:

У нас есть набор модулей в c; мы хотим реорганизовать эти модули, чтобы использовать DI для запуска модульных тестов и т. д.

Каждый модуль состоит из набора функций c:

module_function (...);

Модули зависят друг от друга. То есть. Обычно у вас может быть звонок, такой как:

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

Каков правильный подход к DI для этого?

Возможные решения:

  • (1) Использование указателей на функции для всех функций модуля и при вызове функции сделать это (или аналогично):

    int y = modules-> module2-> dosomethingelse (x);

  • (2) Компилировать несколько библиотек (mock, std и т. Д.) С одинаковыми символами и динамически связывать их в правильной реализации.

(2), кажется, правильный способ сделать это, но это сложно настроить и раздражает, заставляет вас создавать несколько двоичных файлов для каждого модульного теста.

(1) Похоже, что он может работать, но в какой-то момент ваш DI-контроллер застрянет в ситуации, когда вам нужно динамически вызвать универсальную фабричную функцию (void ( factory) (. ..) скажем) с рядом других модулей, которые должны быть внедрены во время выполнения?

Есть ли другой, лучший способ сделать это в c?

Каков «правильный» способ сделать это?

Ответы [ 5 ]

9 голосов
/ 08 августа 2012

Я не вижу проблем с использованием DI в C. См .:

http://devmethodologies.blogspot.com/2012/07/dependency-injection.html

9 голосов
/ 08 мая 2011

Я пришел к выводу, что в Си нет «правильного» способа сделать это. Это всегда будет сложнее и утомительнее, чем в других языках. Однако я думаю, что важно не запутывать ваш код ради модульных тестов. Сделать все указателем на функцию в C может показаться хорошим, но я думаю, что в конце код просто ужасен для отладки.

Мой последний подход состоял в том, чтобы все было просто. Я не изменяю никакого кода в модулях C, кроме небольшого #ifdef UNIT_TESTING в верхней части файла для отслеживания внешних ресурсов и выделения памяти. Затем я беру модуль и компилирую его со всеми удаленными зависимостями, чтобы он не смог соединиться. После того, как я проверил неразрешенные символы, чтобы убедиться, что они мне нужны, я запустил скрипт, который анализирует эти зависимости и генерирует прототипы-заглушки для всех символов. Все это помещается в файл модульного теста. YMMV в зависимости от того, насколько сложны ваши внешние зависимости.

Если мне нужно смоделировать зависимость в одном случае, использовать реальную в другом или заглушить ее в другом, то я получаю три модуля модульного тестирования для одного тестируемого модуля. Наличие нескольких двоичных файлов может быть не идеальным, но это единственный реальный вариант с C. Однако все они запускаются одновременно, поэтому для меня это не проблема.

9 голосов
/ 02 июня 2012

Это идеальный вариант использования для Ceedling .

Ceedling - это проект общего вида, объединяющий (помимо прочего) Unity и CMock, которые вместе могут автоматизировать многиеработу, которую вы описываете.

В целом Ceedling / Unity / CMock - это набор скриптов ruby, которые сканируют ваш код и автоматически генерируют макеты на основе заголовочных файлов вашего модуля, а также бегунов, которые находят всетесты и запускающие их исполнители.

Для каждого набора тестов создается отдельный двоичный файл для запуска тестов, связывая соответствующие имитационные и реальные реализации, как вы запрашиваете в реализации набора тестов.

Сначала я не хотел вводить ruby ​​в качестве зависимости для нашей системы сборки для тестирования, и это казалось сложным и волшебным, но после того, как я попробовал его и написал несколько тестов с использованием автоматически сгенерированного кода для насмешки, я попался на крючок.

2 голосов
/ 21 мая 2016

Немного опоздал на вечеринку по этому вопросу, но это была недавняя тема, в которой я работаю.

Два основных способа, которые я видел, - это использование указателей на функции или перемещение всех зависимостей вспецифический файл C.

Хороший пример последнего - FATFS.http://elm -chan.org / fsw / ff / en / appnote.html

Автор fatfs предоставляет основную часть функций библиотеки и предоставляет определенные конкретные зависимости для пользователябиблиотека для записи (например, функции последовательного периферийного интерфейса).

Еще одним полезным инструментом являются указатели на функции, и использование typedefs помогает предотвратить слишком уродливый код.

Вот несколько упрощенных фрагментов из моего аналога.в цифровой преобразователь (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 теперь вводится в подпрограмму обработки прерываний АЦП.

Вы можете сделать еще один шаг и передать указатель функции в 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;
}
1 голос
/ 08 мая 2011

Есть два подхода, которые вы можете использовать. Вы действительно хотите или нет, как указывает Рэйф, решать только вам.

Первое: создайте «динамически» внедренный метод в статической библиотеке. Ссылка на библиотеку и просто подставить ее во время тестов. Вуаля, метод заменен.

Второе: просто предоставьте замены во время компиляции на основе предварительной обработки:

#ifndef YOUR_FLAG

    /* normal method versions */

#else

    /* changed method versions */

#endif

/* methods that have no substitute */
...