#c #dependency-injection
#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 и т.д.) of с одинаковыми символами и динамически соедините в правильной реализации.
(2) кажется правильным способом сделать это, но его сложно настроить и он раздражает вас, заставляя создавать несколько двоичных файлов для каждого модульного теста.
(1) Кажется, что это может сработать, но в какой-то момент ваш контроллер DI застрянет в ситуации, когда вам нужно динамически вызывать общую заводскую функцию (скажем, void ( factory) (…)) с рядом других модулей, которые необходимо внедрить во время выполнения?
Есть ли другой, лучший способ сделать это в c?
Каков «правильный» способ сделать это?
Комментарии:
1. на самом деле нет хорошего ответа на ваш вопрос. То, что вы пытаетесь сделать, не имеет особого смысла в C. «Вы делаете это неправильно» или «выберите другой язык» — не самый лучший ответ. Я не пытаюсь вас разозлить, но я пытаюсь сказать, что вы требуете от C чего-то такого, что на самом деле не соответствует его сильным сторонам.
2. Посмотрите на ядро Linux. Например. каждый драйвер представляет собой модуль, который реализует определенный интерфейс (в зависимости от типа драйвера). Все модули слабо связаны, и в зависимости от конкретной конфигурации компьютера, на котором запущено ядро, модули (/ dependencies) подключаются друг к другу во время выполнения. Это выполняется каждым модулем драйвера, заполняющим структуру указателей на функции (интерфейс), затем структура динамически предоставляется зависимому модулю. На самом деле Is довольно красиво структурирован. 🙂
3. Не могу не согласиться с @RafeKettler — DI — это лучшая практика разработки программного обеспечения, которая стала важным стилем кодирования, особенно за последние 10 лет. Эта операция просто пытается применить «новую» лучшую практику к языку, который существует в течение долгого времени, и в этом нет абсолютно ничего плохого.
4. @RafeKettler Потому что программы стали слишком большими для идиоматического C (и, следовательно, для C, можно было бы поспорить).
5. @RafeKettler Мы просто больше не знаем, как это делается, я говорю серьезно, и базы кода, о которых люди говорят «посмотрите на X», всегда являются ужасающими примерами.
Ответ №1:
Я не вижу никаких проблем с использованием DI в C. Смотрите:
http://devmethodologies .blogspot.com/2012/07/dependency-injection.html
Комментарии:
1. Это хороший пост по теме. Указатели на функции действительно кажутся хорошим подходом; вы просто должны знать, что без оптимизации времени соединения вы немного теряете производительность при их использовании (по сравнению с подходом ABI, заключающимся в связывании или динамической загрузке одного конкретного экземпляра символов, которые вам нужно использовать).
Ответ №2:
Я пришел к выводу, что нет «правильного» способа сделать это в C. Это всегда будет сложнее и утомительнее, чем на других языках. Однако я думаю, что важно не запутывать свой код ради модульных тестов. Превращение всего в указатель на функцию в C может звучать неплохо, но я думаю, что в итоге это просто делает код ужасным для отладки.
Мой последний подход заключался в том, чтобы все было просто. Я не изменяю никакого кода в модулях C, кроме небольшого #ifdef UNIT_TESTING
в верхней части файла для извлечения и отслеживания распределения памяти. Затем я беру модуль и компилирую его со всеми удаленными зависимостями, чтобы он не подключался. После того, как я просмотрел неразрешенные символы, чтобы убедиться, что это то, что я хочу, я запускаю скрипт, который анализирует эти зависимости и генерирует прототипы заглушек для всех символов. Все это сбрасывается в файл модульного теста. YMMV зависит от того, насколько сложны ваши внешние зависимости.
Если мне нужно смоделировать зависимость в одном экземпляре, использовать реальную зависимость в другом или заглушить ее в еще одном, то в итоге я получаю три модуля модульного тестирования для одного тестируемого модуля. Наличие нескольких двоичных файлов может быть не идеальным, но это единственный реальный вариант с C. Однако все они запускаются одновременно, так что для меня это не проблема.
Комментарии:
1. 1 для нескольких двоичных файлов. Похоже, это действительно единственное разумное решение.
2. Можете ли вы выделить зависимость в отдельный
dependency.h
файл, а затем настроить систему сборки для использованияreal_dependency.cc
в производственном коде иtest_dependency.cc
в тестовом коде?
Ответ №3:
Это идеальный вариант использования для Ceedling.
Ceedling — это своего рода зонтичный проект, который объединяет (среди прочего) Unity и CMock, которые вместе могут автоматизировать большую часть работы, которую вы описываете.
В общем случае Ceedling / Unity / CMock — это набор скриптов ruby, которые просматривают ваш код и автоматически генерируют mocks на основе файлов заголовков вашего модуля, а также тестовые программы, которые находят все тесты и создают программы, которые будут их запускать.
Для каждого набора тестов генерируется отдельный двоичный файл test runner, объединяющий соответствующие макетные и реальные реализации по вашему запросу в реализации вашего набора тестов.
Поначалу я не решался вводить ruby в качестве зависимости в нашу систему сборки для тестирования, и это казалось слишком сложным и волшебным, но после того, как я попробовал это и написал несколько тестов с использованием автоматически сгенерированного кода mocking, я был зацеплен.
Ответ №4:
Немного опоздал с обсуждением этого вопроса, но это была недавняя тема, над которой я работаю.
Два основных способа, которые я видел, как это делается, — это использование указателей на функции или перемещение всех зависимостей в определенный файл C.
Хорошим примером более позднего варианта является FATFS. http://elm-chan.org/fsw/ff/en/appnote.html
Автор fatfs предоставляет основную часть библиотечных функций и предоставляет пользователю библиотеки право записи определенных специфических зависимостей (например, функций последовательного периферийного интерфейса).
Указатели на функции — еще один полезный инструмент, а использование typedefs помогает не допустить, чтобы код становился слишком уродливым.
Вот несколько упрощенных фрагментов из моего кода аналого-цифрового преобразователя (АЦП):
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. Но в этом случае прерывание ADC блокируется до тех пор, пока не будет вызвана функция обратного вызова. Если обратный вызов сам по себе вызывает другие обратные вызовы, это увеличило бы ISR.
2. Как и все остальное, это инструмент. Неправильное его использование даст вам плохие результаты. Однако цепочка обратных вызовов не является проблемой, которую я до сих пор видел в чьем-либо встроенном коде. Обычно проблема заключается в обратном.
3. Это хороший метод, вам просто нужно знать, если вы имеете дело с обратными вызовами ISR, чтобы сохранить реализацию как можно более короткой и эффективной. Если вам нужно сделать много вещей, основанных на выполнении этого ISR, просто используйте флаг, или другой указатель на функцию, или сообщение (в случае RTOS), или что-то еще, чтобы обработать это вне ISR.
Ответ №5:
Есть два подхода, которые вы можете использовать. Действительно ли вы этого хотите или нет, как указывает Рейф, зависит от вас.
Первое: создайте «динамически» внедряемый метод в статической библиотеке. Создайте ссылку на библиотеку и просто замените ее во время тестов. Вуаля, метод заменен.
Второе: просто предоставьте замены во время компиляции на основе предварительной обработки:
#ifndef YOUR_FLAG
/* normal method versions */
#else
/* changed method versions */
#endif
/* methods that have no substitute */
Ответ №6:
Вот пример того, как внедрение зависимостей может быть выполнено в C (в основном, упомянутый вариант (2) OP):
// 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(amp;person) == 20); // our test
return 0;
}
get_todays_date
является ли зависимость для person.h
файла
Когда вы компилируете код для производственной среды, свяжите модуль перевода, включающий реальную реализацию для get_todays_date()
(~ т.е. включите get_todays_date.cc
файл в процесс компиляции)
Когда вы компилируете код для тестирования, не связывайте единицу перевода, соответствующую get_todays_date.cc