标签 重构 下的文章

一次函数设计讨论

近期在考虑对底层函数库进行一些重构,今天下午花了两个小时考量现有的函数库的接口设计,发现目前函数库的实现存在着一个普遍的问题:与特定的内存分配实现耦合的太紧。

我们的应用是多进程结构的,并使用了共享内存这种最快捷的IPC机制,鉴于此很多同事在实现一些数据结构或者算法时可能只考虑到了我们常见的应用场景-多进程共享,而对非共享内存分配的情况考虑不足。那如何将目前某些库函数实现与内存分配之间的强耦合解开呢?针对这道题我发起了一次mail讨论。

题目再重述一下:“目前底层库中的一些数据结构,比如xx_tree、xx_hash_table等,在它们的实现中都会有“分配内存空间”的需求,现有的实现多是直接调用已有的xx_shm_malloc和xx_shm_free在共享内存上动态申请和释放内存,但实际上有些场合我们并不需要在共享内存上分配内存,进程私有堆上的内存完全可以满足需求。如果让大家考虑修改目前xx_tree的实现或重新设计xx_tree的接口,以达到让xx_tree支持多种内存分配策略的目的,你是如额考虑的,请谈谈你的设计思路。"

Mail没发出去多久,就收到了同事A的回复。同事A近期正在对一个老应用的底层库做”翻新“,收到我这封Mail后他产生了共鸣,因为他也遇到了类似的问题。他目前的解决方法是:在xx_tree_t结构体里增加一个成员shared来表示是否在共享内存上申请内存,然后将原xx_tree_new接口改名为内部static接口new_tree,new_tree在申请内存时通过判断shared变量的值来决定到底使用哪种具体的内存分配方式。同样释放内存时也要根据shared的值来判断如何释放内存。原xx_tree_new外部接口通过将shared置为1,并调用new_tree实现;另外新增一个外部接口xx_tree_new_private,选择在进程堆上分配内存的实现方式。他对于这种修改的评价是:"这种方法的好处是原接口没有变化,其它使用这个接口的程序代码无需修改。"

在某些场合下同事A的这种修改方案确也无可厚非,因为有时的确需要考虑对现有代码的影响。但如果该方案是对于xx_tree的一个全新的设计,那显然它没能很好解决我在题目中提出的耦合问题。xx_tree的创建还是依赖于具体的内存分配实现,虽然目前扩展成支持两种内存分配方式了,但付出了新增一个接口,内部增加若干”if…else“判断逻辑的代价。如果我再提供一种新的yy_malloc内存分配机制,那么这种方案下的xx_tree显然无法很好的应对这种变化。

一转眼间又有几封Mail回来,几位同事的思路居然与同事A的都不谋而合,看来大家对于接口变动带来影响还是心有余悸的。

同事A思维很活跃,很快他的第二种方案又公布出来了。其大致思路就是:可以在多种内存分配接口外面套上一个通用的接口,比如void* common_malloc(size, type)和void common_free(ptr)。xx_tree只需增加一个type字段,并在xx_tree_new中传入相应type,xx_tree内部实现一概使用common_malloc/common_free接口,并将type值传入相应接口。这样xx_tree与具体的内存分配机制实现的耦合就解开了。

这种方案中的common_malloc和common_free则似乎成了一种代理,解除了xx_tree与具体内存分配实现之间的耦合,但是这个代理却需要关心到具体的内存分配机制的实现。如果有新增内存分配方式,common_malloc和common_free依然需要修改,仍未达到理想状态。

同事B的Mail接踵而来。Mail中他谈了他的回调函数方案:
定义两个回调函数指针原型如下,
typedef void* (*hook_malloc)(int size);
typedef void (*hook_free)(void *);
修改xx_tree_new的接口,在接口原型中增加两个参数,一个是hook_malloc malloc_func,一个是hook_free free_func。这样在xx_tree的其他接口实现中只需回调malloc_func和free_func即可,它无须关心这两个指针对应的具体实现。

这个方案其实已经接近我的方案,但是我提出的问题是:如果xx_tree中使用calloc和realloc了怎么办?calloc与realloc从语意上与malloc又不同,函数原型上与malloc也无法兼容?当然了最简单的答案就是再定义两个针对calloc和realloc的函数原型,然后再为xx_tree_new增加两个参数。但这样下来参数数量是不是太多了!如果再增加其他语义的分配行为,xx_tree接口还得修改。

讨论到这里已接近下班时间,这个问题依旧留给大家继续思考。其实这个解耦合问题主要是想要大家知道通过面向接口而不是具体实现的编程可以降低甚至消除一些代码耦合,而C语言中”接口“的概念可以通过函数指针体现出来。

我这里也给出一种看似还不错的方案,(函数设计没有最好只有更好^_^,如果你有更好的想法,欢迎交流)
我定义了一个名为xx_allocator_t的结构体,其成员均为函数指针变量,这个xx_allocator_t就类似于Java里的interface,只定义了接口的行为规范(C的函数指针原型)。所有类似xx_tree需要支持多种内存分配方式的数据结构只要支持xx_allocator_t即可(将xx_allocator_t类型变量作为参数传入)。

/* x_allocator.h */
typedef void* (*x_alloc_func)(size_t size);
typedef void  (*x_free_func)(void *ptr);
typedef void* (*x_calloc_func)(size_t nmemb, size_t size);
typedef void* (*x_realloc_func)(void *ptr, size_t size);

struct xx_allocator_t {
        x_alloc_func         af;
        x_free_func          ff;
        x_calloc_func        cf;
        x_realloc_func       rf;
};

业务层使用xx_tree的方法:
tree = xx_tree_new(&(struct xx_allocator_t){.af = malloc, .ff = free});
这里利用C99中的结构体赋值方式,即使你为xx_allocator_t增加一种新行为,这里的代码也不需要做什么修改。

这次讨论感觉大家都动了脑并参与了进来,达到了预期的效果,大家也很喜欢,我觉得可继续做下去,至于形式上可灵活变通。

读'代码修改艺术',可观其大略

早在几个月前就从网上下载到了"Working Effectively With Legacy Code"这本书的E版,之所以下这本书是因为看到了"Legacy Code"这两个单词了,说实话当时我并不知晓这本书的价值,只是想当然的认为:这本书可能会有助我改善我所从事的项目中的"Legacy Code"。早在上个月去逛书店时,就看到了书架上的这本"修改代码的艺术",遗憾的是没有给予足够关注。在最近看到这本书译者刘未鹏的博客以及Dreamhead关于这本书的评价后,才又从电脑中找到这本书开始翻看。从与这本书几次"擦身而过"的经历来看,自己识书的能力实在是差劲。

我需要这本书,首先是因为我目前的项目中就有大量大量的"Legacy Code",所以我已经开始迫不及待的想看完这本书了。但是翻看一些后,我觉得作为使用C的开发者独观其大略即可。为什么呢?书中代码多以面向对象的语言Java或C++作为例子代码,很多细节对使用结构化语言的开发者意义不大。毕竟结构化的思想与面向对象的思想有着较大的差别。

作者提出来的修改软件的四个起因基本上大家都是应该认同的:
(1) 添加新特性;
(2) 修正bug;
(3) 改善设计;
(4) 优化资源使用。

同时作者又给出了为了减少修改行为带来的风险而应该考虑的三个问题:
(1) 我们要进行哪些修改?
(2) 我们如何得知已经正确地完成了修改?
(3) 我们如何得知没有破坏任何(既有的)东西?

在第一部分第二章的最后作者给出了一个解决这些问题的算法:
以下算法可以用于对遗留代码基进行修改:
(1) 确定改动点;
(2) 找出测试点;
(3) 解依赖;
(4) 编写测试;
(5) 修改、重构。

看完这些我觉得就可以直接进入第二部分了,作者给出了细致的、具体的面对不同情形应该如何去做。建议:在读每一个章节之前先回顾一下自己是否遇到过类似情形,自己当时是如何做的,当时的做法是否有改善的地方,哪些是值得发扬广大的,哪些是应该摒弃的,如果是现在我还会如何去做?之后,再看看Michael Feathers是如何去做的,这样效果应该是很好的。有如下这些情形值得我们去考虑:
(1) I Don’t Have Much Time and I Have to Change It 时间紧迫,但必须修改
(2) It Takes Forever to Make a Change 漫长的修改
(3) How Do I Add a Feature? 添加特性
(4) I Can’t Get This Class into a Test Harness 无法将类放入测试用具中
(5) I Can’t Run This Method in a Test Harness 无法在测试用具中运行方法
(6) I Need to Make a Change. What Methods Should I Test? 修改时应当测试哪些方法
(7) I Need to Make Many Changes in One Area. Do I Have to Break Dependencies for All the Classes Involved? 在同一地进行多处修改,是否应该将相关的所有类都解依赖
(8) I Need to Make a Change, but I Don’t Know What Tests to Write 修改时应该怎样写测试  
(9) Dependencies on Libraries Are Killing Me 棘手的库依赖问题
(10) My Application Is All API Calls 到处都是API调用   
(11) I Don’t Understand the Code Well Enough to Change It 对代码的理解不足
(12) My Application Has No Structure 应用毫无结构可言
(13) My Test Code Is in the Way 测试代码碍手碍脚   
(14) My Project Is Not Object Oriented. How Do I Make Safe Changes? 对非面向对象的项目,如何安全地对它进行修改
(15) This Class Is Too Big and I Don’t Want It to Get Any Bigger 处理大类
(16) I’m Changing the Same Code All Over the Place 需要修改大量相同的代码
(17) I Need to Change a Monster Method and I Can’t Write Tests for It 要修改一个巨型方法,却没法为它编写测试
(18) How Do I Know That I’m Not Breaking Anything? 降低修改的风险
(19) We Feel Overwhelmed. It Isn’t Going to Get Any Better 当你感到绝望时

当你看到这些具体情形的列表,眼前是否浮现出是曾相识的经历呢?"代码修改艺术"应该说是一本实用主义的书,如果你是使用面向对象语言的程序员,那你很幸运,建议你将一本"修改代码的艺术"放在你的办工桌旁,随时翻看、思考和领悟;如果你和我一样是结构化语言的使用者,也没有关系,观其大略,品其思想,细读你兴致之所在。

以上的书中文字的中文翻译部分摘自于刘未鹏的中文译文。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats