如何写好C代码之依赖注入

c/c++

浏览数:299

2019-4-5

依赖注入(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_createtype参数即可。

总结:

  • 参数注入:适合简单函数的场景,一般如果单个函数(不属于任何模块的工具式函数)如果需要调用外部的API,可以试着用注入的方法。
  • 设置接口注入:适合运行时设置一个模块调用的外部API。
  • interface注入:适合将一个模块的一组API一起设置到摸个调用这个,是对一个模块的API的整个注入。