从“回调”看软件设计

2022-09-10

回调 (Callback) , 在有些特定环境下被称为勾子 (hook) 。一般情况下的调用就是调用, 然而当被调用者能够反过来调用调用者时就叫回调, 而被调用者如果是函数的话, 就叫回调函数。回调出自过程语言, 过程语言中的最著名代表无疑是C语言, 下面就以最简单的代码示例一个C语言回调:

这里call函数调用函数callback1和callback2来实现不同的打印输出, 回调的“回”体现在回调函数中调用了call函数的参数, 回调函数可以象普通函数一样被程序调用, 但是只有它被当作参数传递给被调函数时才能被称作回调函数。如果赋了不同的值给该参数, 那么调用者将调用不同地址的函数。赋值可以发生在运行时, 这样使你能实现动态绑定, 以此为基础的应用就拥有了运行时的动态特性。

显而易见, 回调的特点有两个, 一个是“回”, 回调函数能拿到调用者的东西, 第二个是动态特性, 因为回调函数被作为了参数, 这样运行时就能切换不同的回调函数, 以此达到不同的执行效果, 很多软件的运行时插件系统就是通过回调来实现的。除了前面的两个, 进一步思考就会想到第三特性, 可扩展性, 不需要修改原来的调用者, 只需要提供新的回调函数即可实现不同的效果。

可扩展性是软件设计的一个重要考量因素, 而回调是实现可扩展的一个很好选择, 不但代码清晰易读, 而且没有引入多余的flag和if判断。假如我们在软件中将一个用户相关的逻辑写入一个函数, 而应用具有易变性, 不同用户对业务逻辑可能不完全一致, 那你就得修改逻辑对应的函数, 打开这个函数, 找到这个发生变化的地方, 加入if判断:如果是A用户, 采用这个过程;如果是B用户, 则采用那个过程。问题是软件中不会就一个需要改变的地方, 而且可能不会只卖给一两个用户, 那么这个函数越来越臃肿, flag和if越来越多, 到最后人人都不愿意碰这个函数, 也不是人人都有“能力”改这个函数, 只有“资深”工程师才能搞定它。这就是软件扩展性要解决的关键问题。

如果采用回调, 将所有用户都一致的整体流程写进这个函数, 而将不一致的地方写成一个个回调函数, 那么这个函数就变得易于阅读和修改, 当然这个过程不是一蹴而就的, 是一个不断进行调整和改进的过程, 最后这个主体函数慢慢就固定了下来, 每次卖给新的用户, 就只要增加一个或一套新的回调函数即可, 你或许发现, 这里再称这个模式为“回调”似乎有点不是那么贴切了, 因为“回调”已经上升到了解决扩展性和多态性层面上了, 已经进入了面向对象程序设计模式领域。

1 模板模式

模板 (Templete) 模式是面向对象的设计模式中最为简单的模式之一, 但确是用处最广、功能最强大的模式, 也是最难灵活应用的一个, 用得好, 事半功倍, 用得不好, 代码反而臃肿难读。

模板模式, 顾名思义, 就是和模板有关, 何为模板, 就是大家做某件事可以遵循的某种即成样板, 我如果要做这个事情, 拿到这个模板后, 添加自己相关的东西, 这件事就搞定了, 那对我来说, 这事情就变得简单易行得多了, 而且还不容易出错, 因为主要步骤模板都帮着理清楚了。下面是一个用java实现的模板模式, 这是标准的模板模式, 看上去和回调还不是那么靠近, 一会再看和回调很像的变形模板模式:

这不就是抽象类别与具体实现的关系而已么?回到模板这两个字上, 对于一些程序而言, 我们希望规定一些处理的步骤、流程或骨架, 就象是上例中的step1到step3一样, 至于流程中的step1到step3如何实现, 我们并不规定, 而留给实现的人自行决定, 这就是模板模式的目的。

模板模式太简单了, 以至于根本不学就会用了, 为什么前人要提出来单独作为一个模式, 这是因为它太重要了, 它是框架 (framework) 的基石。框架是一个可供扩展的半成品, 打个不太恰当的比喻, 就好比是一个刚造好还未装修使用的大楼, 一切插头、门窗框和管道都已经布置好了。A要开个健身中心, 买了这大楼添加电器和各种设备后成了健身中心, B想开个软件公司, 买了这大楼添加了各种设备后就成了软件公司, 这不是和前面卖软件的例子很类似吗?如果这软件新逻辑的实现不是由卖软件的来实现, 而是由买软件的自己实现, 并且这软件有一定专业领域, 那这软件就可以被称为框架了, 框架是大部分中间件软件厂商的主要产品之一。

现在来看和回调很像的变形模板模式, 之所以说它是模板模式, 因为它是通过对象来实现的;而称它是“变形”, 因为它不是通过继承抽象类来实现的。和回调非常类似, 最典型的使用是Spring里头的各种template类, 举JdbcTemplate为例, 之所以Spring提供了JdbcTemplate, 因为一般情况下我们处理jdbc相关的操作, 不管业务代码怎么变化, 都要先建立资源和连接, 然后执行业务代码, 再写上异常处理代码, 再接着写资源和连接释放的代码, 资源和连接释放又要加上他们自己的异常代码。这是一个非常符合模板模式用途的地方, JdbcTemplate就把这些通用步骤放到模板里, 然后执行回调接口的实现类, 看上去和回调无比相似。值得注意的是, spring中的调用者叫xxxTemplate, 而大部分回调接口也确实都叫xxx Callback, 把回调和模板模式之间的关系表露无遗。下面这段代码描述了通过jdbc调用存储过程将学生记录备份到归档表中, 假设这个这个存储过程叫ARCHIVE_STUDENTS:

这是一个典型的JdbcTemplate使用场景 (摘自《Spring In Action》) , Spring提供了对存储过程的调用支持, 通过一个叫Callable Statement Callback的回调接口来供开发人员扩展, 开发人员实现这个回调接口, 然后在其do In Callable Statement方法中写入业务逻辑, 程序员享受到了所有资源管理和异常处理方面的现成好处, 程序员要做的仅仅是定义这个存储过程的名字, 然后执行它。这其实已经和回调非常相似了, 业务实现者仅仅只需要关注业务逻辑就可以了, 其它的通用过程全由模板代劳了。这样不但提高了编程效率, 而且也提升了软件整体质量, 开发人员也不大容易因为疏忽而出现错误, 这就是模式的力量。spring作为一个中间件产品, 全称是Spring Framework, 上述例子已经把框架的概念体现得淋漓尽致。

软件的各种理念和方式方法都有着一定的内在联系, 框架的优点是你不需要修改框架本身的代码, 但是你可以扩展框架。OOAD (面向对象的分析设计) 中的一个原则叫开闭原则, 完完全全是同理, 框架是一个更高层次上满足开闭原则的实例, 而模板模式和之前的回调可以减少if判断和flag, 把要增加的代码放到了回调之中, 这也是一个典型的开闭原则实践, 思想殊途同归了。

2 块

块 (Block) 脱胎于函数编程, 是最近流行的脚本语言 (ruby、groovy等) 的一个很重要的特性, 其本质就是回调 (到底是回调先有, 还是函数编程理论先出来就没必要追究了) , 只不过以语言特性固定了下来, 用起来很方便也很直观, 语言本身内置了很多支持块的方法, 以groovy的文件操作为例:

这段代码的作用是遍历一个目录, 将此目录下的文件名都打印出来, 其中花括号{}括起来的部分就是块, 说白了就是一个代码块, each File{...}的意思在每个文件上执行此代码块, 非常直观易用。这里并不试图对块作全面的论述, 只说一点, 块的形式和作用类似于模板模式, 但是在脚本语言中其主要目的是提高代码的直观程度和易用性。模板模式的高层次的应用如作为框架基石估计不是它考虑的范畴, 因为脚本就是脚本, 一般应用于小型软件, 或者起辅助作用, 并且其动态性不但使得大部分设计模式的存在价值大打折扣, 而且如果应用于大规模软件的话, 开发模式可能也会作巨大的调整, 这就不在本文的范畴之内了。

3 结语

从过程语言到面向对象语言, 从结构化程序设计到面向对象设计, 从模版到框架, 从组件到SOA, 软件的发展演进一直在推进。在这个过程中, 我们需要抓住一些本质的东西。当前大型软件开发面临最大的困难在于, 如何让软件能快速适应业务的变化。站在软件开发的角度看, 软件的变数越少越好, 变数越少越可控;但站在客户提升核心竞争力的角度看, 业务的易变性又是必然的。快速、高效进行大型应用软件开发是现今一个非常重要课题, 框架提供了提高软件开发效率、质量的途径, 而所有框架的背后其实都可以看到“回调”思想的影子。

摘要:软件编程语言和开发框架发展很快, 纷繁复杂的技术形态层出不穷, 对于软件编程人员似乎总是被动跟随。但是透过这些表面的多样性, 深入分析, 就会发现一些根本性的设计模式和设计理念, 一直得到传承, 本文试图以“回调”作为切入点, 探究模版模式、块以及框架之间的内在的继承性和延续性。

关键词:回调,模板模式,块,框架,Spring,开闭原则

参考文献

[1] 王千祥.构件化软件-超越面向对象编程[M].电子工业出版社, 2004, 9.

[2] 周伟明.多任务下的数据结构与算法[M].华中科技大学出版社, 2006, 1.

[3] (美) Clovis.Tondo Bruce P.Leung.C++Primer[M].武汉:华中科技大学出版社, 2002.

上一篇:“营改增”对工程造价的影响及对策与分析下一篇:农药助剂:转型升级正当时