Как сделать инъекцию зависимостей в 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. Они все запускаются одновременно, поэтому для меня это не проблема.
Ответ 2
Я не вижу проблемы с использованием DI в C. См.:
http://devmethodologies.blogspot.com/2012/07/dependency-injection.html
Ответ 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 */