系统程序员成长计划-Don’t Repeat Yourself(DRY)(下)
转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
实现这两个函数并不是件难事,但真正写好的人并不多。初学者通常的做法有两种:
1.各写一个独立的函数。dlist_find_max用来找出最大值,dlist_sum用来求和。这种做法和前面写dlist_print时所犯的错误一样,会造成重复的代码,让dlist的实现随着应用环境的变化而变化。
2.采用回调函数法。细心的初学者会发现,这两个函数的实现与dlist_print的实现很类似,无非是print那行代码要换成别的功能。能想 到这一点很好,不过在真正动手时,发现每个回调函数都要保存一些中间数据。大部分人选择了用全局变量来保存,这可以实现要求的功能,但违背了禁用全局变量 原则。
这两个函数没有什么实用价值,但是通过它们我们可以学习几点:
1.不要编写重复的代码
按传统的方法写出dlist_find_max之后,每个人都知道这个函数与dlist_print很类似,在写出dlist_sum之后,那种感 觉就更明显了。在这个时候,不应该停下来,而是要想办法把这些重复的代码抽出来。即使因为经验所限,也要极力去想思考和查资料。
写重复的代码很简单,甚至凭本能都可以写出来。但要想成为优秀的程序员,你一定要克服自己的惰情,因为重复的代码造成很多问题:
重复的代码更容易出错。在写类似代码的时候,几乎所有人(包括我)都会选择Copy&Paste的方法,这种方法很容易犯一些细节上的错误,如果某个地方修改不完整,那就留下了”不定时”的炸弹,说不定什么时候会暴露出来。
重复的代码经不起变化。无论是修改BUG,还是增加新特性,往往你要修改很多地方,如果忘掉其中之一,你同样得为此付出代价。请记住古惑仔的话,出来混迟早是要还的。大师们说过,在软件中欠下的BUG,你会为此还得更多。
去除重复代码往往不是件简单的事情,需要更多思考和更多精力,不过事实证明这是最值得的投资。在这里,我们要怎么抽取这些重复的代码呢?
这三个函数无非是要遍历双向链表并做一些事情,遍历双向链表我们可以提供一个dlist_foreach函数,至于要做什么,这是千变万化的行为,可以通过回调函数让调用者去做。
2.任何回调函数都要有上下文
大部分初学者都选择了回调函数法,不过都无一例外的选择了用全局变量来保存中间数据,这里我不想再强调全局变量的坏处了,记性不好的读者可以看看前面的内容。我们要说的是,在这种情况下,如何避免使用全局变量。
很简单,给回调函数传递额外的参数就行了。这个参数我们称为回调函数的上下文,变量名用ctx(context的缩写)。要在这个上下文中存放什么东西呢?那得根据具体的回调函数而定,为了能保存任何数据类型,我们选择void*表示这个上下文。
下面我们看看怎么实现这个dlist_foreach:
DListRet dlist_foreach(DList* thiz, DListVisitFunc visit, void* ctx) { DListRet ret = DLIST_RET_OK; DListNode* iter = thiz->first; while(iter != NULL && ret != DLIST_RET_STOP) { ret = visit(ctx, iter->data); iter = iter->next; } return ret; }
visit是回调函数,ctx就是我们说的上下文。要特别强调的一点是,ctx应该作为回调函数的第一个参数。为什么呢?在前面我们讲过的面向对象 的函数命名规则中,我们以thiz作为函数的第一个参数,而thiz通常也就是函数的上下文。如果在这里恰好ctx==thiz,就不需要因为参数顺序不 同而做转换了。
实现求和的回调函数:
static DListRet sum_cb(void* ctx, void* data) { long long* result = ctx; *result += (int)data; return DLIST_RET_OK; }
调用foreach:
long long sum = 0;
dlist_foreach(thiz, sum_cb, &sum);
是不是很简单?以后在使用回调函数时,记得多加一个ctx参数,即使暂时用不着,留着方便以后扩展。好了,请读者用类似的方法实现查找最大值的功能吧。
3.只做份内的事
我见到不少任劳任怨的程序员,别人让他做什么他就做什么,不管是不是份内的事,不管是上司要求的还是同事要求的,都来者不拒。别人说需要一个XXX 功能的函数,他就写一个函数在他的模块里,日积月累后,他的模块变得乱七八糟的,成了大杂烩。我亲眼见过在系统设置和桌面两个模块里,提供很多毫不相干的 函数,这些函数造成不必要的耦合和复杂度。
在这里也是一样的,求和和求最大值不是dlist应该提供的功能,放在dlist里面实现是不应该的。为了能实现这些功能,我们提供一种满足这些需求的机制就好了。热心肠是好的,但一定不能违背原则,否则就费力不讨好了。
本节的示例请到这里下载。