如何写好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的整个注入。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
javascript 几种常用继承方法和优缺点分析
1.原型链继承(最简单) 核心 (实现思路):用父类的实例充当子类原型对象 function Person(name) { this.name = name; this.fav = ['basketball', 'football']; this.detail = {country : '中国', city : '江苏'} } function Man(name) { this.say = function(){console.log('I am man')}; } Man.prototype = new Person(); //核心 var sam = new Man('sam'); console.log(sam.fav); //'basketball', 'football' 优点 1.简单,容易理解,容易实现 缺点: 1.创建子类实例时无法向父类传参(创建Man时无法传递name信息) 2.引用类型的属性(数组和对象),被所有实例共享,接着上面的代码举个例子 var tyler = new Man('tyler'); tyler.fav.push('bad...
- 下一篇
扩展Zuul实现ignored-patterns的byPass功能
前言 2018年年底的一天,我们的架构师公司如何扩展Zuul时,说了1个功能,如下: 对zuul的ignoredPath,加入了byPass(旁路功能),可以单独开放指定的url。 例如:公司屏蔽 /**/*Manage/*, 设置byPassUrl,/**/hello2Manage/* 这时所有满足/**/hello2Manage/* 都可以被外网访问。 这个功能我觉得很一般,应该非常的简单,完成也就10分钟,结果哎,不说了都是泪啊! 初步设想 zuul 可以跟Eureka结合实现默认配置 zuul可以设置zuul:ignored-patterns 用于设置屏蔽的url, 还可以指定路由配置例如: zuul: route: hello-service-ext: path: /user-service/ext/* serviceId: user-service-ext 初步想法很简单,就是在Zuul和Eureka实现默认配置基础上,加入一个指定路由配置,之后再配置zuul:ignored-patterns ,为了保证配置按顺序生效YAML文件进行配置。 初次尝试的错误配置如下: #**...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,CentOS8安装Elasticsearch6.8.6
- 2048小游戏-低调大师作品
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7安装Docker,走上虚拟化容器引擎之路