在遗留代码上开发(Development on legacy code)
遗留代码
笔者从开发伊始到现在,大多数系统是构建在之前的遗留系统之上的,在开始,很难把遗留系统直接丢弃,特别是一些业务逻辑非常复杂的金融电信系统。 这些代码往往有如下特点:
1.旧的编程语言开发低效。
2.代码冗繁,质量差。
3.添加新的功能和修改错误(Bugs)的周期时间长而痛苦。
4.这些代码没有单元测试,甚至没有功能测试,冒烟测试,回归测试。
5.无法交接这些代码,因为写代码的这些人很多已经离职。
6.维护这些代码代价高,大家心惊肉跳,特别是系统遇见特殊情况(节假日,高峰访问期等),无法安宁。
7.但是这些代码能够完成当时的功能,直接抛弃这些代码,重新开发将会耗费很大的资源,且不一定成功,如果新的需求不断变化,往往没有时间来重新开发这些代码。而这些遗留代码的功能没有完善的文档说明,甚至没有。 在此现状下,如何改善这些代码,将是考验程序员和一个团队的智慧。
开发
笔者在多年的遗留系统的开发硝烟中,也尝尽许多痛苦,技术层面的,政治层面的等等。然而,没有这些磨练,也就不会有任何收获,废话少说,看看现在我们如何改造这些遗留系统。
保留系统还是取代?
1.具有性能瓶颈的遗留系统:
曾经遇到过一个应用服务,是C语言写的,周围其他系统都是采用Java开发,与此系统的交互都是采用非标准的协议完成,而且此系统处于比较核心的位置,但是维护非常复杂,不稳定,访问压力大是经常宕机,无法水平扩展,只能提高硬件设备水平等方式考虑。
在原有协议基础上开发,使其具有水平扩展能力,代价和开发一套标准协议的实现没有任何区别,往往会带了协议不够完善所产生的问题。于是,我们为其开发新的标准协议,WS,MQ等等,然后在标准协议上负载均衡实施水平扩展,非常方便。
随着后来的发展,此遗留代码也慢慢被新开发系统取代,期间经历了新系统和旧系统同时存在,此时新系统未完全具有旧系统的全部功能,这部分功能还是使用旧系统完成,只不过在原有均衡负载层多了层查找和分配的,后来到旧系统完全取代。此过度还算平顺。
2.功能性改造型系统:大多数就是这种系统,保留的话,代码及其复杂,维护麻烦,丢弃的话,无法一夜之间写出新的系统。
曾经接手了一个开发失败的项目,包括代码和文档都未完善,如果重新来过,终究不划算,后来在此系统山进行改造,特别是花了大量时间写单元测试,保证代码测试覆盖率极高,这样一边熟悉代码,一边重构代码,系统的健壮性发生根本改变,前提是有时间。这只是特例。
很多时候遇见的系统,同样测试代码很少,在添加新的功能和修复错误(Bugs)时,为这些能够接触到的代码完善测试,新代码必须测试覆盖率必须很高,经过4个月,,此系统代码覆盖率已经达到50%以上。以后的迭代开发越来越快。
以下将这种讲解这种方式的开发过程。
在遗留代码上编程
找代码修改点
遗留系统代码往往测试少,或者没有,导致软件开发者对软件发布没有信心。但是为所有的遗留代码写单元测试,初始代价非常高,在添加一个很小的功能时,并没有时间大动干戈。
如何找切入点呢?
为了新的改动,我们总能找到应该修改的代码在哪里,不外乎以下几种情况:
1.修改一处代码即可,这个时候非常简单,修改代码处即为切入点,找到这处修改即可,为此处代码写完善的单元测试代码,特别是对于输入条件和测试条件尽量能够完整测试。
2.修改多处代码,位置分散,并且修改大妈如果有多种方案,我们找出最少修改代码的地方,而不是最佳的修改方式,很多时候,此时最佳的代码修改会修改很多代码,导致测试代码无法一下子完善,另外,此时认为的最佳方案随着时间的推移,或许又是糟糕的代码,所以没有必要花费更多的精力在上面,当然也可以选择比较中庸的方式。
技巧
1.找测试方便,改动较小的方式来修改遗留代码。
2.重构在一个类中那些重复的方法,并且保证其健壮性。
3.为依赖的具体类提取新的接口,并使用注射依赖技术,使得测试更加容易,不管是使用Mock tool还是自己编写Mock对象,都会非常容易测试。
比如,
class Manager{
...
public void kickOff(){
...
DoSomething doSomething = new DoSomething(...);
doSomething.doSomething(objects);
}
...
}
可修改为:
class Manager{
...
DoSomething doSomething;
public void setAction(Action doSomething){
this.doSomething = doSomething;
}
public void kickOff(){
...
doSomething.doSomething(objects);
}
...
}
interface Action {
void doSomething(List marks);
}
为DoSomething提供了接口Action,采用诸如依赖,mock这个DoSomething这个类,测试kickOff()方法。
4.尽量使测试的范围缩小在受修改影响的类中,对类中的改动进行全面测试。保证每处修改完全测试,保证测试类减少。
5.类之间交互的代码重构,如果这些交互仅在修改的代码之中,只要保证修改的代码完全测试即可。而对于那些可能影响此时其他不需要进行修改代码的类,可以先放下,为其创建新的方法,在此次修改和以后修改中,使用和重构新的方法。对于老的方法,等到以后代码覆盖率提高,能够覆盖所有此类交互方法的代码时,重构此方法,这是你会发现,修改很简单,并且如果修改错误,或者不能处理极端的逻辑,也会和容易找出问题所在。
6.努力汲取业务逻辑知识。
7.《修改代码的艺术》(Michael Feathers 著)建议找到切入点(Inflection Point),往往我们找的点很多,每一次修改都可能不一样,为此花很多代价找寻,还不如直接进入修改,找出最佳的修改方式避免代码过度重构和修改,减少影响,这才是有有实践价值和有意义的。
质量保证
总之,和遗留代码打交道时,为了提高发布信心和效率,速度和质量是我们的追求。
1.尽可能让一切自动化:
单元测试自动化是最基本要求,尽可能让一切测试变得自动化,不管是单元测试,还是功能测试,还是压力测试,冒烟测试,回归测试。
发布自动化也是非常重要的。
2.为项目加入冒烟测试和回归测试。逐渐保证代码质量。
3.坚持可控变化,逐渐渗透的原则,保持系统稳步得朝健壮的方向进行。
关于重构推荐的书籍:
《重构:改善既有代码的设计》Martine Fowler著
《重构与模式》:Joshua Kerievsky著
《修改代码的艺术》: Michael Feathers 著
参考:
《修改代码的艺术》: Michael Feathers 著
本文作者:
现就任某公司金融软件高级资深技术架构师,著有《漫谈设计模式》一书(清华大学出版社出版)。