如何写好C代码之依赖注入
依赖注入(Dependency Injection 简写为DI)开发过程中解除耦合的经典手段,但是似乎从一开始这货就是为面向对象而生的,我所看到的示例都没有将C语言考虑在内。难道C语言不能使用这么经典的设计模式?本文就来介绍一下C语言如实使用依赖注入来解除耦合。
参数注入
对应于面向对象语言的构造函数注入,C语言作为过程语言,参数注入法是最简单、也是最直接的方法。最常见的排序方法qsort就是用这种方法:
void qsort(void* base, size_t num, size_t size, int (*compar)(const void*,const void*));
可以看到qsort函数的第四个参数compar就是外部依赖的对象(函数),因为不同场景有不同的比较元素大小的方式,通过参数将外部依赖注入,使该函数更加具有通用型,因为实际上我们用qsort,只是用他的排序算法,其他的都是和具体使用场景有关。
设置(set)接口注入
上一篇我们介绍的设置回调函数的方法其实就是使用这种方法,其本质就是专门对外提供一个接口,用来将依赖的外部对象或者函数注入到本模块中来。比如开发一个模块,需要申请内存,但是为了易用性,除了使用系统自带的内存申请函数,我们需要支持第三方的内存池模块来申请内存,我们就可以提供一个API来设置申请和释放内存的函数,如下示例:
///默认申请内存方式为系统自带的函数
static void *(*malloc_function)(size_t size) = malloc;
static void (*free_function)(void *p) = free;
int sample_module_init()
{
return 0;
}
///设置新的分配内存的函数
int sample_module_set_memory_api(void *(*get)(size_t size), void(*put)(void *p))
{
malloc_function = get;
free_function = put;
return 0;
}
///申请一个size大小的int类型数组
int *sample_module_create_int_array(size_t *size)
{
int *p = (int *)malloc_function(sizeof(int) * size);
return p;
}
这样我们在使用这个模块时,就可以设置三方的内存申请方式了。比如想使用jemalloc的内存分配方式,调用sample_module_set_memory_api 将内存分配的函数指针设置为该库的内存申请API就可以了。
sample_module_set_memory_api(mallocx, freex);
int *p = sample_module_create_int_array(2);
基于Interface的注入
面向对象编程有一个接口(Interface)的概念。从概念上讲,接口是一个抽象类,代表一系列行为相同的类;从实现上来讲,接口就是一堆方法的集合。C语言虽然没有接口的的概念,但是完全可以实现接口的功能,通过结构体将一系列函数指针组合起来,就可以实现接口的功能。比如我们需要实现一个缓存功能模块,包括set_value和get_value两个方法,为了使模块更具有扩展性,我们先定义抽象接口
struct cache_interface
{
///store kv in cache
int (*set_value)(void *instance, const char *key, const char *value);
///find kv in cache
int (*get_value)(void *instance, const char *key, char **value_out);
};
在面向对象语言中,接口不能实例化。C语言中虽然这个结构体可以实例化,但是实例化后没有任何意义,其中的函数指针仍然无值可赋,所以我们要在另外的文件中实现这个接口:
///实现方式---本地文件缓存
struct cache_local_file
{
///必须是第一个成员
struct cache_interface methods;
char file_path[32];
FILE *fp;
};
int set_value(struct cache_interface *instance, const char *key, const char *value)
{
struct cache_local_file *ins = (struct cache_local_file *)instance;
fprintf(ins->fp, "%s:%s", key, value);
return 0;
}
int get_value(struct cache_interface *instance, const char *key, const char **value_out)
{
*value_out = "sample data";
return 0;
}
//创建实例(创建一个实例,相当于构造函数)
int cahce_local_file_create(const char *path, struct cache_interface **instance)
{
struct cache_local_file *ins = (struct cache_local_file *)malloc(sizeof(cache_local_file));
strncpy(ins->file_path, sizeof(ins->file_path), path);
ins->fp = fopen(path);
ins->methods.get_value = get_value;
ins->methods.set_value = set_value;
*instance = &(ins->methods);
return 0;
}
上面实现一个利用本地文件存储数据的缓存方法,为了更变的使用这些实现,我们需要提供统一的API来共用户使用,这样当缓存的具体实现有变化时,使用缓存的用户不用大规模修改代码,甚至不用修改代码。下面我们类似于面向对象里的工厂模式来提供这些API。
struct cache_implement
{
char name[32],
int (*create)(const char *input, struct cache_interface **instance);
}
struct cache_implement impl[32] = {0};
void init()
{
strncpy(impl[0].name, sizeof(impl[0].name), "local_file");
impl[0].create = cahce_local_file_create;
///more implement to add
};
int cache_create(const char *type, const char *param, struct cache_interface **ins)
{
for(int i =0 ; i < 32; i++)
{
if (0 == strcmp(type, impl[i].name))
{
impl[i].create(type, param, ins);
}
}
return 0;
}
int cache_set_value(struct cache_interface *ins, const char *key, const char *value)
{
///another user code
///set kv
return ins->set_value(ins, key, value);
}
int cache_get_value(struct cache_interface *ins, const char *key, const cahr **value_out)
{
///another user code;
///get kv
return ins->get_value(ins, key, value);
}
这样的话,使用者就只关注到三个API,如果要变换不同的缓存实现(比如使用redis或者memcache存储数据),只要修改cache_create的type参数即可。
总结:
- 参数注入:适合简单函数的场景,一般如果单个函数(不属于任何模块的工具式函数)如果需要调用外部的API,可以试着用注入的方法。
- 设置接口注入:适合运行时设置一个模块调用的外部API。
- interface注入:适合将一个模块的一组API一起设置到摸个调用这个,是对一个模块的API的整个注入。