其实软件最初的设计是非常单纯的,按照业务流程一行一行往下写就行了。但是,随着业务流程越来越复杂,软件规模越来越大,考虑到软件的可维护性、易变更性等诸多因素,软件设计就变得不那么单纯了,而软件设计的功底就展现出来了。而在这些需要考虑的诸多因素中,代码的复用性是最考验程序员基本功的。
人们常说,有经验、能力比较强的程序员往往能写出优秀的代码。然而,依我以往的经验,情况并非如此。许多人开发过大量的程序,熟悉许多的软件架构,但要编写出优秀的代码却是完全不同的另一回事,它需要我们去掌握另一套软件设计理论,而这正是许多老程序员不屑一顾但又非常欠缺的知识。
扯得有些远了,我们言归正传。为什么说代码的复用性是最考验程序员基本功的呢?回想一下,我们在以往编写代码的过程中,有多少次在粘贴复制代码?不用说吧,一定是无数次。据说程序员操作最熟练的快捷键就是“ctrl+C”与“ctrl+V”。毫无疑问,我们编写的代码中有许许多多的功能是相似甚至完全相同的,关键在于你怎样去看待它。粘贴复制是最简单常用的方式,但在一个软件系统中,如果同一功能被粘贴复制了上百次,一旦这段代码需要变更时,那简直就是一种灾难。
这样的灾难,在我以往的软件开发中就发生过,令我深感其害。记得那是2005年,我参与维护一套老系统。该系统以往在各个省运行一切都正常,但此时要应用到直辖市却乱做了一团糟,因为直辖市和省不一样,单位上下级关系的判断必须做出修改。如果系统采用公用代码来判断上下级关系,问题会变得异常简单,但事实却不是这样。在这个老系统中,判断上下级关系的这段代码被粘贴复制了无数次,几乎贯穿了整个系统的所有类。剩下时间就是蚂蚁搬家式的代码搬运,在所有代码中去搜索、修改。同样的事情也发生在另一个项目,当客户对系统界面的某个地方不满意,而这个地方在系统的所有界面中都用到的时候。
同一段代码,当你拷贝第一次时也许还能容忍,但当你拷贝第二次时,你真的需要停下来仔细思考了,这是一种优秀的编程习惯。
有一次,我参与编写了一个供应链管理系统。起初,我为系统中的应付单编写了一段生成财务凭证的代码。随后,我又开始编写付款单生成财务凭证的代码。其实,应付单生成的财务凭证与付款单生成的财务凭证虽然有些许区别,但其实都是生成数条借方科目与贷方科目的分录(分录就是财务凭证的明细),分别拥有各自的科目、摘要、金额等等。再仔细分析,还有应收单、采购发票、各种核销单等等一系列单据,都需要生成财务凭证。接下来,我重构了之前仅仅为应付单编写的代码,将公用的部分写成独立的公用代码,而将不同的部分抽象为接口,并为各个单据编写了各自不同的实现。
在之后的开发中,这部分的需求发生了变更,要求能批量生成凭证,并提供三种方式的汇总,即所有单据生成一张汇总凭证、按单据类型分别生成汇总凭证,以及不汇总各自生成凭证。由于这里代码得到了合理的复用,实现这个功能变得异常简单——让公用代码实现汇总功能,让各个客户程序按照规则调用就可以了。经过这样的设计,系统向着可维护、易变更的良性方向发展。
但要写好代码复用并不容易。你也许可以在这里唾沫纷飞的说出一大堆理论,但真正到了实践中却可能面临诸多的难题。我经常编写软件开发平台,平台中的功能要提供给无数客户程序使用。我需要面临客户程序提出的许多变态需求,我既要满足这些新需求,又必须保证已有客户的正常使用,让它们感受不到我的变更。这常常让我花费数天的时间去思考,去分析,制订方案,最后才能动手去写代码。记住,在这时候草率的动手常常会给我们带来不可挽回的损失。关于代码复用的话题,还有经典的“不要重复自己原则(DRY)”,我会在后面慢慢与大家讨论。
最后,代码复用一个非常常见,大家应当注意的问题就是分包的问题。想象一个这样的场景,当我在进销存模块编写了一段不错的公用程序而财务核算模块需要使用它时,就产生了财务核算模块对进销存模块的引用。假如财务核算模块也有一段代码进销存模块需要使用呢?如此往复,模块与模块之间相互引用,形成了一张无形的网,剪不清,理还乱。遇到这种情况,正确的办法应当是对原有代码进行重新规划,把公用代码划分到基础模块中。如上例,当进销存模块的代码需要在财务核算模块使用时,这段代码就不应再存在于进销存模块了,而是应当提升到公用代码模块,最后做到包引用仅仅是一种树形结构——上层模块只能引用下层模块,而不能引用平级模块。
一次软件编程技术的探讨之旅
重新理解一切皆对象
重新审视代码复用
(续)