代码地震
(王咏刚 2004 年1 月)
1 问题引入
很不幸,这又是一次失败的软件开发经历。
一个经验丰富的项目经理率领着七个身手不凡的程序
员,用了五个月时间为某银行开发了一套“银证通”管理
系统——在这里,大家用不着细究“银证通”系统的具体
流程和功用,我们只要知道这是一套涉及银行、证券公司、
客户三方的实时交易和转账系统就足够了。因为系统需要
连接位于不同物理位置、隶属于不同企业或机构的后台服
务,需要通过尽可能多的手段向客户提供7×24 小时的交
易服务(这是一种典型的异构环境下的分布式电子商务模
型),项目组决定选择Web Service 作为系统的核心支撑技
术,并选用C#语言来编码实现。五个月的开发顺利结束,
项目组已经成功地构建出了如图 1 所示的一套功能齐备、
结构清晰、符合组件服务模型要求的应用系统。
图 1 系统架构示意图
图 1 显示,系统主要由两个分布在异地的Web Service
服务程序和三种不同类型(GUI、Web、呼叫中心)的客
户程序组成。两个服务程序都通过ADO.NET 数据访问接
口分别与各自的数据库连接。服务程序B 因为要在复杂的
并发条件下管理共享资源,比服务程序A 多了一个并发控
制组件。三种类型的客户程序都通过SOAP 协议调用服务
程序提供的Web Service 接口完成交易功能。
开发工作结束以后,项目组几乎没碰到什么真正的困
难就成功地在客户现场完成了系统的安装和调试。系统在
实际运行环境下表现良好,无论是功能还是性能都可以满
足用户的需要。看上去,这个项目自始至终都那么十全十
美,如果不是三个月以后发生的那起突发事件,项目经理
和他那些满怀自信的组员们一定可以在年终时拿到全公司
最高的奖金了。
系统正式运行的第三个月末,客户提出要更改一下系
统的功能。其实,这算不上什么特别大的功能调整。简单
说来,就是客户改变了现有的业务制度,在新制度的要求
下,“银证通”系统需要增加一项新的业务代码,并在另一
项原本由数字构成的业务代码前增加3 个英文字母组成的
前缀。
项目经理只瞥了一眼《需求变更说明书》,就非常有把
握地对负责该项目的销售人员说:“小意思,一个人一天时
间就能摆平。这样吧,为了稳妥起见,你可以向客户承诺
三天内解决问题,五天内新版本正式上线。”
当然,事情并不像项目经理想象的那样简单——如果
一切顺利的话,我也就不用在这里多费口舌了——就是这
样一次看似轻松的修改工作,让整个项目组陷入了困境。
好吧,我们现在就来看看,需求变更提出以后,项目组里
到底发生了什么。
第一天,项目经理根据自己“一个人一天时间就能摆
平”的判断,要求程序员甲在一天的时间内独立完成所有
相关代码的改动和系统测试。
第二天一早,项目经理惊讶地发现,倒霉的程序员甲
两眼通红、一夜未眠,还在埋头查找和修改代码。不得已,
程序员乙在项目经理的安排下,也加入到了代码修改者的
行列中来。
第三天,情况依然没有好转,项目经理的心中隐约产
生了不祥的预感。
第四天,距离对客户承诺的最后期限越来越近了。项
目经理又将程序员丙补充到了修改小组中。
第五天清晨,参加修改工作的三名程序员一致认为,
所有与新需求相关的代码改动都已完成,可以着手对新系
统进行测试了。项目经理立即调来了两名专门负责测试的
程序员。不幸的是,测试工作从一开始就陷入了混乱。起
初是系统无法在测试环境中顺利运行,在好几次重新编译
和链接之后,客户端和服务端程序才能正常启动;接下来,
程序员们发现,系统中有两项重要交易无法完成;更糟糕
的是,在执行压力测试时,服务程序很快就会失去响应..
第五天、第六天、第七天都在周而复始的Bug 报告、
代码调试、Bug 定位、代码修改、编译链接中飞逝而过,
项目看起来还没有好转的迹象。销售人员每天都要用各种
杜撰的理由向客户解释修改工作延期的原因。项目经理在
心急如焚之余,也必须小心地向主管领导汇报工作的进展
情况。
第八天,项目经理再也不能容忍这种混乱继续下去了。
ADO.NET ADO.NET
数据库A 数据库B
服务程序
A
服务程序
B
客户程序
A
..
GUI..
图形界面组件
Web Service
连接组件
客户程序
B
..
Web..
Web 界面组件
Web Service
连接组件
客户程序
C
..呼叫中心..
语音界面组件
Web Service
连接组件
SOAP SOAP
SOAP
Web Service 服务端组件
交易处理组件
数据访问控制组件
Web Service 服务端组件
交易处理组件 并发控制组件
数据访问控制组件
他把所有熬红了双眼的程序员召集在一起,命令所有人放
下手头的工作。他竭力克制住自己的情绪,坚定地说:
“项目组遇到了困难,这全是我的错——我错误地估
计了形势。现在,我们必须回到起点,重新调查研究,直
到我们把下面三个问题彻底搞清楚:为了适应新的需求,
需要修改的代码到底有多少处?这些改动是不是会引发系
统架构上的缺陷或风险?在代码改动之前,是不是需要先
调整某些组件的内部设计?”
项目经理在关键时刻的沉着冷静将项目从崩溃的边缘
拉了回来。程序员们花了整整两天的时间仔细审查了需求
变更前的所有代码和设计文档,并将每一处必需的代码改
动列表如下:
程序 组件/模块 修改内容
数据库A/B 表结构
增加一个代码字段,修改一个代
码字段的定义
服务程序A/B
数据访问控制
组件
与表结构对应的struct 结构的改
变
服务程序A/B
数据访问控制
组件
接口方法参数列表及相关调用代
码的改变
服务程序B 并发控制组件 共享存储区数据结构的改变
服务程序B 并发控制组件 共享存储区操作代码的改变
服务程序A/B 交易处理组件 交易接口及相关调用代码的改变
服务程序A/B 交易处理组件 交易处理代码的改变
服务程序A/B
Web Service
服务端组件
Web Service 接口参数的改变
服务程序A/B
Web Service
服务端组件
客户端传入参数的合法性校验代
码的改变
客户程序A
(GUI)
图形界面组件
数据录入界面增加新编辑框,修
改一个编辑框的属性
客户程序A
(GUI)
图形界面组件
数据修改界面增加新编辑框,修
改一个编辑框的属性
客户程序A
(GUI)
图形界面组件
数据录入界面中数据合法性校验
模块的改变
客户程序A
(GUI)
图形界面组件
数据修改界面中数据合法性校验
模块的改变
客户程序A
(GUI)
图形界面组件
数据录入界面中数据格式转换代
码的改变
客户程序A
(GUI)
图形界面组件
数据修改界面中数据格式转换代
码的改变
客户程序B
(Web)
Web 界面组件
查询界面增加新编辑框
修改一个编辑框的大小
客户程序C(呼
叫中心)
语音界面组件
为新业务代码增加一个语音播报
项目
客户程序C(呼
叫中心)
语音界面组件 临时文件结构的改变
客户程序C(呼
叫中心)
语音界面组件 临时文件存、取代码的改变
客户程序
A/B/C
Web Service
连接组件
Web Service 调用代码的改变
直到现在,程序员们才清楚地看到了此次系统升级的
复杂程度。客户仅仅想增加一项业务代码,修改一项业务
代码的构成规则,程序员们居然要在系统中的几十个地方
修改程序!更重要的是,在列出了上面那张庞大的修改工
作清单之后,项目组还必须仔细评估每一项修改需要什么
样的程序员,需要多长时间,修改后的代码应当进行什么
样的单元测试,代码改动可能引起什么样的系统风险。对
于并发控制组件等关系到服务程序稳定性的模块,项目组
还必须在修改前明确风险评估和防范的原则和方法。为了
保证软件的质量不受影响,在所有代码修改完成后,项目
组还要对整个系统的功能和性能进行严格的测试。
按照这样的思路,项目组在项目经理的带领下,又花
了整整三周时间完成了所有代码修改和系统测试工作。当
升级后的系统在客户现场成功上线的时候,客户方的项目
负责人只对可怜的项目经理说了一句话:
“你们真是挺辛苦的,升级的结果也还不错——可你
们当初为什么要骗我说五天之内就能做完呢?你这不是明
摆着让我欺上瞒下吗?”
项目经理哭笑不得,感慨万千。为什么一处小小的需
求变更就能带来如此大的修改工作量呢?混沌学家曾经预
言,一只蝴蝶扇动翅膀就足以引发几千公里外的一场热带
风暴。这一次,一个微不足道的需求变更,竟实实在在地
引发了软件系统的一场“代码地震”:数据库表结构的修改
导致数据访问控制组件的变更,然后又引出交易处理和并
发控制代码的改动,接下来还有服务接口、客户端连接代
码、客户端界面组件需要修改..更要命的是,所有代码
改动都可能引发风险,都需要重新测试,关键的代码变更
甚至会触及软件的基础架构..这种“地震效应”一旦在
软件中出现,就必然像滚雪球一样一发而不可收拾。如果
不是项目经理沉着冷静,“银证通”项目的升级历程恐怕只
会更加糟糕。
现在,我和这位项目经理都迫切想知道的是,类似的
需求变更一定会引发一次“代码地震”吗?系统的设计和
编码过程与这种“代码地震”有什么必然的联系吗?是否
存在有效的“防震”、“减震”或“抗震”的措施?
2 一些题外话
前面的案例提到了技术人员年终奖金的问题。对于软
件企业来说,这始终是一个敏感的话题。应当说,项目经
理、程序员等技术人员的基本薪金(月薪或年薪)反映的
是技术人员的经验水平和工作能力;各种形式的奖励(包
括年终奖、项目奖、股权分配等)反映的则是技术人员在
考核期限内的工作业绩。前者通常和技术人员的职位或技
术级别挂钩,有相对明确的标准;后者,也即奖励数额的
多少,就不那么好确定了。
一些公司直接根据技术人员的职位或级别来确定奖励
额度,这实际上抹煞了基本薪金和奖励之间的根本区别,
体现不出奖金的鼓励和惩戒作用;另一些公司由老板或部
门经理凭经验决定下属的奖励额度,在这种分配体系下,
即便负责奖金分配的领导没有半点私心杂念,最终也很可
能出现按“苦劳”而不是按“功劳”分配的不合理局面。
有没有一种合理的、客观的工作业绩评价方法呢?其
实,只要项目管理和财务制度健全,我们完全有可能像评
定销售人员的销售业绩那样,利用技术人员贡献给公司的
“钱”数来评定其工作业绩,而且,我们还可以进一步把
这种评定方式简化为计算技术人员在单位时间内的“贡献
率”。
计算“贡献率”的方法有很多种,我个人比较喜欢以
项目组为单位来考核“贡献率”的做法。即,先计算项目
组的“贡献率”,然后给出项目组总的奖金额度。项目组内
部各成员的奖金分配由项目经理自行决定。与单独考察每
个技术人员“贡献率”的做法相比,这种做法既能保证评
价规则的简明和参考数据的准确,也能赋予项目经理在项
目组内部实施奖惩的基本权力。
我们可以用公式计算项目组在单位时间内的贡献率:
项目组的贡献率(R)=单位时间内项目组为公司创造的实
际效益(V)÷单位时间内项目组的成本和费用总和(C)。
公式的分母部分很容易确定(查财务数据,一般包括
管理均摊和设备折旧成本、差旅和办公费用、工资福利等)。
比较难办的是分子V。通常的想法是直接用项目的销售额
来反映项目组为公司创造的效益。但我们必须考虑公司的
一些特殊项目。比如,为了打入某个新的行业,公司可以
牺牲利润与客户签单。这样一来,销售额就反映不出该项
目对公司的价值了。再比如,一些研发性项目在短时间内
无法带来任何实际的销售额,其效益该如何计算呢?我个
人的意见是,项目的实际效益可以比照正常项目的销售额
来计算,但最终的决定权在公司的项目监管部门。比如,
一个在正常竞争情况下可以获得100 万元销售额的项目,
因为在特殊情况下的让利,实际销售额是75 万元,那么,
项目监管部门在计算项目组的贡献时,应当以100 万元为
计算依据(确定研发项目实际效益的方法相对复杂一些,
这里就不再展开了)。
拿本文案例中提到的“银证通”项目来说,项目组按
时完成了开发工作,假如保内维护工作也相对正常,那么,
项目组的“贡献率”自然不会很低。对于案例中简单的需
求变更,无论是否向用户收费,项目监管部门都必须重新
评估,在升级工作中,项目组对公司的贡献是否有所增加。
客观地说,类似的软件升级在任何情况下都不大可能将项
目的实际收益提高多少,大多数公司都会视其为正常的保
内维护工作而免收用户的升级费用。但是,因为出现了“代
码地震”问题,项目组耗费了六人四周的时间才完成修改
工作,该项目的成本和费用陡然增加,项目组的“贡献率”
相应地就会大幅下滑。这样一来,项目组成员年终收益的
减少也就在所难免了(在这里,我们评价的是项目组对公
司的贡献而不是其工作的辛苦程度)。
也就是说,我们必须在设计和开发过程中严防“代码
地震”的出现,这不仅能减轻项目的维护工作量,也能切
实保障项目组成员的奖金收益。
3 案例分析
还是回到技术问题上来吧。简单地说,案例中的“银
证通”系统在体系设计上并没有太大的问题,系统依靠
Web Service 等通用接口技术建立的分层构建、组件独立、
分布式部署的基本模型也无可指摘。从项目组的修改过程
看,小小的需求改动之所以能引发可怕的“代码地震”,这
主要是由于系统中存在着大量的代码重复。
Kent Beck 和Martin Fowler 将“重复的代码(Duplicated
Code)”视为代码中的“坏味道”之一①。通常,代码重复
意味着错误和风险的重复,当需求变更发生的时候,它必
然导致修改工作量、修改成本和修改风险的成倍增加。
Martin Fowler 在《重构》一书中列举的代码重复还仅
限于方法与方法之间、类与类之间、算法与算法之间的代
码重复,这些常见的代码重复问题可以通过“提炼函数”、
“提炼类”、“值域上移”、“塑造模板函数”、“替换算法”
等《重构》一书推荐的方法或手段加以解决。但是,在案
例中的“银证通”项目里,代码重复的问题已经深入到了
软件开发的每一个层面。项目组碰到的代码重复问题实际
上涵盖了数据库和应用程序间代码重复、模块之间的代码
重复、组件之间的代码重复、界面之间的代码重复、分布
式系统间的代码重复等更为复杂的情况,以至于数据库结
构的微小调整就足以牵一发而动全身,将项目组毫不留情
地拖入“代码地震”的梦魇之中。
为了从根本上解决复杂系统内的代码重复问题,项目
组必须在设计和开发的全过程中,有针对性地、分门别类
地预防和管理每一种潜在的代码重复风险。下面,我们将
以“银证通”项目为例,逐一列举项目中存在的主要代码
重复现象,并讨论每一类问题的预防与解决办法。
数据库和应用程序间的代码重复
“银证通”项目的两个服务程序都需要访问各自的数
据库。每个服务程序都有一个相对独立的数据访问控制组
件,以便隔离业务功能与数据操作。具体的编码方式是这
样的:每个数据库有一套初始化脚本(用SQL 语言编写),
程序中的数据访问控制组件使用struct 结构来表示数据库
中每张表的结构。例如,对于表Transactions,数据库初始
化脚本中的创建语句为(以Transact-SQL 语言为例):
create table [Transactions]
(
[Name] Varchar(20),
[Code] Varchar(20),
[RefCode] Varchar(20),
[RoleID] Int,
[OpNodeID] Int,
[ValidDate] Datetime
);
在C#语言中定义的,对应的数据结构为:
struct Transaction
{
public string Name;
public string Code;
public string RefCode;
public int RoleID;
public int OpNodeID;
public DateTime Datetime;
}
为了正确地存取和维护内存中的Transaction 数据结
构,C#代码中还必须包含Transactions 表中所有字段名称、
限制条件以及相关查询语句或存储过程的定义。
这种编码方式在数据库应用系统的开发中十分常见,
它给我们带来的麻烦也非常明显:需要修改表结构时,程
序员必须首先改变每个数据库初始化脚本中的SQL 语句,
然后再依次修改数据访问控制组件中的struct 结构和与变
更字段相关的所有定义语句。
此外,“银证通”项目组在设计数据访问控制组件的接
口时还存在一个致命的问题——每个接口方法都将需要传
递的字段一字排开,像下面这样罗列在参数表里:
public bool NewTrans(string Name, string Code, string
RefCode, int RoleID, int OpNodeID, DateTime
Datetime);
这样做的后果是,数据库结构改变时,所有接口方法
的参数列表都要调整,其他组件中所有调用这些接口方法
的语句也要相应地加以调整。
上面这两个问题都属于与数据库相关的代码重复问
题。解决后一个问题的办法相对简单,只要坚持在类似的
参数传递中只传递数据结构的指针或引用就可以了,像这
样:
public bool NewTrans(Transaction data);
前一个问题,即,表结构的数据库表示(SQL 脚本)
与代码表示(C#中的struct 和相关定义)之间的重复问题,
就不那么好解决了。而且,在基于组件和分层的系统设计
中,系统的逻辑层次越多,类似的重复也就越多。其实,
对此类问题,即便是微软的工程师也很难给出既简单又有
效的解决方案。在Visual Studio .NET 提供的示例程序
Duwamish 中,数据库里几乎每一个表结构都会在数据库
创建脚本、存储过程代码、业务实体层代码、数据访问层
代码中反复出现。在Duwamish 中修改数据库的结构显然
也是一件苦差事,因为任何一项数据结构上的改动都必然
引发上述四类源代码的修改操作。
我所见过的一种相对简单的解决方案是,将数据库的
生成、修改、查询等SQL 语句作为字符串常量嵌入到代码
中,与struct 结构以及相关的定义放在一起。系统提供的
数据库安装程序也使用C#语言实现,并直接利用代码中保
存的SQL 指令创建表结构。如果发生表结构的变更,程序
员只要在一个源文件里,集中修改所有与该表相关的代码
就可以了。
第二种可行的方法是使用.NET 平台提供的RTTI 和
Reflection 机制,直接从C#代码中的数据结构(在运行时
获取struct 结构中每个元素的名称和数据类型)组合出与
该表的创建、修改和查询相关的SQL 语句。这种方法对编
程语言和编程技术的要求较高,而且只适用于提供了完善
的RTTI 和Reflection 机制的语言(如C#、Java 等语言)。
郝刚在一篇文章中谈到过另一种可行的解决方案,即,
使用XML 语言定义数据库结构,数据库系统直接根据
XML 定义创建表结构,数据访问控制代码(包括代码中的
数据结构定义)则由通用控件根据XML 定义自动生成②。
这种方法较难实现,但应能适用于大多数程序设计语言。
J2EE 体系中的CMP Entity Bean 技术综合了前两种方
案的优点:程序员在ejb-jar.xml 文件中定义表结构,同时
在Entity Bean 的实现代码中声明相应的操作接口(只声明
抽象的接口方法),应用服务提供的封装工具根据上述定义
自动生成数据库中的表结构和相关的操作代码。应当说,
J2EE 的CMP 技术,为我们解决类似问题提供了一个相当
出色的范例。
交易处理组件中的代码重复
“银证通”项目中,在两个服务程序的交易处理组件
之间,存在大量的代码重复问题。
两个服务程序中的交易处理组件是由不同的程序员开
发的。程序员在开发这些组件时,并没有意识到代码重复
的危险,也没有认真核对过两组代码之间是否存在相互重
复的部分。事实上,两个服务程序提供的尽管是完全不同
的业务功能,但二者中有许多基本的业务操作,如开销户、
币种转换、利率和汇率计算等,都是相互重复的。如果在
开发时不加注意,这些重复的业务逻辑就必然会产生重复
的代码。
在“银证通”系统的源代码里,交易处理组件的重复
现象包括描述业务属性的常量重复、与基本业务逻辑相关
的表达式重复、数据结构的重复定义、类成员的重复定义、
函数或方法内部代码的重复等等。《重构》一书给出的“提
炼函数”、“提炼类”等方法可以有效解决交易处理组件中
的代码重复问题。但现在的问题是,对于分布在不同服务
程序中的交易处理组件,我们该如何完成关键代码的“提
炼”呢?
这个问题可以归结为在不同程序或不同组件间提炼共
享组件的问题。例如,要解决“银证通”系统内不同服务
程序间的代码重复问题,通常的做法是把涉及相同或类似
业务逻辑的代码提炼出来,组成基本的业务操作或工具类,
并把这些类合并到一个公共的库中(对于C#语言,这里的
公共库就是指可以被不同程序共享的.NET Class Library),
由不同的服务程序共享。
不过,在软件开发完成后再去查找重复的代码并提炼
公共库,这充其量是一种亡羊补牢的方案。最好的做法当
然是在设计中尽可能多地甄别和提取不同组件间的公共特
性,在开发过程中随时注意不同的程序员是否在实现相同
的功能模块,并尽可能早地阻止代码重复的产生,提炼共
享的公共组件——不消说,这种高屋建瓴的工作理应由从
事总体设计的系统设计师全权负责。
通信接口中的代码重复
“银证通”系统的所有服务程序和客户程序之间均使
用Web Service 交换信息、完成交易。在Web Service 的两
端的接口代码中,也存在着比较严重的代码重复问题。例
如,在服务程序A 和服务程序B 提供的Web Service 接口
方法里,所有参数都平行地罗列在参数表中:
[WebMethod()]
public int GetTransactionsCount(string Code, string
RefCode, int RoleID, int OpNodeID, DateTime
Datetime);
因为几乎所有交易都和数据库相关,大多数参数都直
接对应于数据库中相关字段。和前面讲过的,数据库访问
控制组件的接口参数表重复问题类似,这种通信接口的设
计一样存在着重复和难于变更的问题。显然,客户端调用
Web Service 接口的代码必须以类似的方式传递所有参数,
客户端的更高层组件(如界面组件)在调用底层接口时,
多半也会重复这种冗长的参数表。一旦数据库结构需要调
整,那么,以Web Service 为中心,向客户端和服务端两
个方向延伸的每一层代码都必须修改方法的声明、定义和
调用语句。修改时的任何一个遗漏都可能造成编译、链接
乃至运行时的致命错误。
避免通信接口中参数表重复的方法是使用提炼后的整
个数据结构作为参数来代替繁冗的参数表,例如,上面的
接口方法例子可以改为:
[WebMethod()]
public int GetTransactionsCount(GTCParamStruct
Param);
这样,当数据库结构的变化需要调整Web Service 接
口时,只要改写GTCParamStruct 结构的定义就可以了。
用户界面中的代码重复
在“代码地震”的案例里,客户端程序用户界面组件
中的代码重复占了代码重复总量的40%左右。其中,界面
控件的重复相当普遍。例如,客户程序A 的数据录入界面、
数据修改界面都需要罗列重复的控件,以录入或修改同样
的数据内容,客户程序B 中的HTML 控件显示的也是完全
相同的字段内容。当需求变更发生时,这些用户界面无一
例外都需要修改。客户程序C 是呼叫中心使用的语音处理
程序,其中的多个IVR 语音响应模块需要播报同样的字段
内容,它们之间也存在相当多的代码重复。
一些程序员倾向于使用动态绑定和动态界面生成技术
来解决用户界面的重复问题,即由动态生成程序根据数据
库中的表结构或特定格式的界面描述语言③自动生成用户
界面。不过,动态界面生成技术往往和具体的语言及开发
环境相关,很难在不同语言间移植;动态生成的用户界面
也总不如人工绘制的用户界面那样美观、精致。所以,大
多数程序员目前仍然会选择自行绘制用户界面,并容忍界
面代码重复的“权宜之计”。
界面编程中另一个值得注意的问题是数据合法性校验
模块的代码重复问题。在数据录入、数据修改、数据查询
等界面中,都需要对同样的字段进行合法性校验,这些校
验代码显然都是一模一样的。如果在设计和开发时没有把
校验代码提炼到公共组件中,那么,一旦业务需求改变了
字段的构成规则,我们就会手忙脚乱、顾此失彼了。
并发控制组件中的代码重复
“银证通”项目组在需求变更时遇到的最大障碍是:
服务程序B 管理共享资源的代码和数据库结构密切相关,
其中也存在不少代码重复的问题。解决这些代码重复问题
并不困难,真正困难的是,修改并发控制组件代码时的任
何疏漏,都有可能酿成并发冲突、服务终止、数据不完整
等难以预测和难以调试的“编程噩梦”。
因此,“银证通”项目组在系统升级的过程里,反复修
改代码、反复测试并反复遭遇失败的经历提醒我们,对于
涉及后台服务、并发控制等关键技术的软件系统,我们在
此前反复强调的,在设计和开发过程里及早防范和减少代
码重复的做法通常会具有更为重要的意义。
4 补充说明
有关代码重复的另一个定理是:在多人项目组内,如
果没有好的配置管理工具和完善的版本控制手段,避免代
码重复的提法就几乎等同于痴人说梦。
这是因为,解决程序之间、组件之间代码重复问题的
主要手段是将重复的代码提炼出来,放到共享的公共库中。
而在一个多人项目组内,开发和维护公共库代码的程序员
和开发和维护其他组件的程序员,必须借助一个版本控制
系统才能有效地协作,才不至于引发需求、版本或代码的
混乱。当项目组拥有一套支持共享的版本控制机制、功能
完善的版本控制软件时,我们甚至可以允许不同的程序员
同时维护公共库的代码,这显然可以大幅提高工作效率,
并在最大程度上防止代码的重复(参见《凌波微步》一书
中有关版本管理的章节④)。
5 总结一下
.. 重复代码意味着重复错误和重复风险;重复代码意味
着增加维护成本和减少项目收益。
.. 在设计和开发过程中应及早防范和减少代码重复。
① Fowler M 等著. 侯捷, 熊节译. 重构:改善既有代码的设
计. 北京: 中国电力出版社, 2003
② 郝刚. Duwamish7 大剖析之业务实体. CSDN 开发高手.
2003.11
③ 王咏刚. 设计“好看”的用户界面. 程序员. 2003.10
④ 王咏刚, 周虹. 凌波微步:软件开发警戒案例集. 北京:
清华大学出版社, 2002