Как сделать инъекцию зависимостей в C?

Я ищу хорошее техническое решение для выполнения 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), кажется, правильный способ сделать это, но его сложно настроить и раздражать, заставляя вас создавать несколько двоичных файлов для каждого unit test.

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

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

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

Ответы

Ответ 1

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

Два основных способа, которые я видел, это использование указателей функций или перемещение всех зависимостей в конкретный файл 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;
}

Ответ 5

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

Сначала: создайте "динамически" введенный метод в статической библиотеке. Свяжите с библиотекой и просто замените ее во время тестов. Voila, метод заменяется.

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

#ifndef YOUR_FLAG

    /* normal method versions */

#else

    /* changed method versions */

#endif

/* methods that have no substitute */