关于代码质量退化的思考


一个软件项目从探索阶段到发展方向明确阶段,会经历从简单到复杂的一个过程,需求的不断叠加,会让系统越来越庞大,功能繁多,公司业务的扩展也让软件系统的生命周期变的更长。在业务变复杂软的过程中,各种原因的驱使,代码质量会退化,维护和开发新功能的成本也会相应的变高,推倒重新开发的成本也是高的吓人。

代码质量退化的步骤

大多情况下编码设计质量最高的时候是根据第一版需求进行编码实现的时候,但只要需求一变更,就会打乱原来的编码设计,软件质量也就会越来越差。或者就没有了设计。

到了项目中期,有新的功能或者bug的修复,老板就给我了一天时间,让我写好处理代码?逾期是要被骂的;这个没用的功能,做了也没人用,随便写吧,早点结束,早去干别的;我手上现在这么多活,你又插进来个新功能,我只能乱搞了,团队内人员水平的不同写的代码更是天差地别,等等,这都是我们实际工作中会遇到的问题。责任心让我们也会想先这样写,以后再重构,一般以后重构表示永远不会重构
上面说的这些都会让我们增加糟糕的代码,混乱的业务逻辑分布在我们系统的各个地方,部门人员变动,新的员工更不可能理解那些杂乱无章的东西,再接着推糟糕的代码,想要理清楚一个业务逻辑,非常容易在混乱的代码中迷路。最直接的后果就是这些混乱的代码会增加新功能的开发周期,领导层问为啥现在开发个功能这么慢?是不是人手不够,再招几个人吧。这杂乱的项目,不是新员工能理的清的,你会发现,虽然员工变多了。但开发效率还是上不去。

那我们重新来做一个新的系统完全替代这个老项目吧,我们可以用最新的框架,更好的实现方式去完成这个系统,这种天真的想法会在团队成员的脑海里无数次出现,旧的系统业务很复杂,新的系统在兼容旧系统逻辑的同时,旧的系统也在更新需求,增加功能,在新系统完全可以抗衡旧系统之前,旧的系统会一直运行。如果你的新系统开发的时间过长,等完成的时候,可能员工都已经不知道换了几批了,代码又乱成了一锅粥,周而复始。

软件的退化变的越来越严重的过程中,我们也在思考和改变现有的系统,如何才能让系统的在拥有更长的生命周期的同时,提高代码的质量,不让其退化,并拥有更好的可维护性和扩展性?那就是根据需求的变化去调整架构、代码,不断的打破原来的设计,保持清晰,而不是让他烂在那里。

渐进式架构

大多数人能想到的最直接的方案是从架构入手,引入多维度的架构,微服务化,领域驱动模型(DDD)等等
从顶层设计出发引入新的架构模型,或者说根据需求的变动不断的调整代码的分层和模块,加上理论知识的应用,会让业务代码在结构归属上更清晰。分层的严密能让整体的业务边界更明确,前提是我们要从多维度去审视系统的构架,思考如何去现有的架构做出合理的改动。

从不同的角度去分析和改进现有架构

比如在项目初期业务比较简单,最简单的分层架构就实现了项目需求,观察我们的架构可能是这样子的,从上而下的松散分层架构

后来又加入了缓存,又加入了消息队列,业务的不断扩张又加入了不同的数据库nosql,业务的升级有了v2.0,v3.0,新业务要兼容旧功能等等,如果还是原来的分层结构,很快就会出现逻辑代码堆积的问题,业务层之间引用杂乱,一个代码文件几千行代码,需求变动时牵一发动全身,及时调整架构的必要性就体现出来了。
一定要复用好依赖倒置原则,层与层之间不应该依赖实现,要依赖于抽象,比如我们的基础设施层要为其他三层提供支持,基础设施层可以实现其他层定义的接口来进行抽象,从这个角度来开的话我们的基础设施层应该在最上面,也可以是左边或者右边

应用依赖倒置后,我们调用的是抽象接口,你会发现层的概念没有了,层的概念被打破了,我们可以更激进一点把基础设施层剥离出去用各种适配器去接入各种组件,把层的关系拉平,把架构调整为六边形构架

不要固化自己的思维,根据业务和系统的发展去调整你的系统架构,能让系统能更高的可扩展和可维护性。
对于非常老的项目调整架构是痛苦的,一定要得到管理层充分的支持下再去做改造,这样的工作只能是从上往下推进,痛苦的过程终会换来后期维护的喜悦。

代码层面

在团队内除了要有代码规范,所有人都要遵守,这样代码的风格才能更统一,和使用Lint工具去检查代码,各种语言lint工具,能在早期查检出你代码中不合理的地方。还有下面一些办法

功能模块化

程序员最喜欢的就是编码实现具体的功能,在这里才是我们真正秀内功的地方,可以应用各种模式把代码和逻辑写的很漂亮,但是放到整个项目结构里,被调用和使用的过程又感觉那么的不协调,根源是我们模块划分不正确,模块之间的依赖耦合性太强。
这就是典型的写的很优雅,使用的很粗糙。依赖倒置原则,依然适用于模块间的划分,模块与模块之间的依赖是倒置的,用依赖注入的方式去解耦,模块对外暴露出尽可能少的接口,之间的调用依赖于接口。抽象的好处能让你把模块的边界定义的更明确。

对象之间是协作关系,不是纠缠

业务越复杂,需要操作的对象也就越多,对象的边界不明确就会出现纠缠不清的情况,要不就是一个对象负责的东西过多;要不就是几个对象同时做一件事,逻辑杂乱。
当你发现你的对象之前不再是协作关系时就要停下来,从高处去看你组织的代码,把大对象分解,职责界线理清楚也就是功能单一原则,很多同学不知道如何确定一个对象的职责,不清楚一个属性是不是属于某个对象,最简单的方法就是,判断这个属性的变动会不影响某个对象,如果没有就不属于这个对象。
还有就是,面对新的业务需求敢于打破原有的代码设计,不破不立。

不要过度开发,删除没用的代码

定期要检查和删除没用的代码。少写或者不写感觉未来可能会用到的方法,这些多出来的代码会成为将来重构的绊脚石,会浪费精力在这些没有用到的代码上,查找有没有地方在使用他。

SOLID 原则

不能不提的,就是Bob大叔(Robert C. Martin)的SOLID编码原则,他是设计模式的基石,要不断的去应用和实践。
随着编码时间的增长,越来越感觉SOLID真的是一盏明灯,当你在黑暗中找不到方向的时候,指引你回归正确的道路。
如果你对SOLID原则应用的比较熟练,我上面说的几项完全都可以忽略。

  • 单一职责原则(Single Responsibility Principle)
    每个对象只有一个职责,明确对象的边界,文章上面说的对象之间是协作关系,不是纠缠里就说过如何确定一个属性是否属于某个对象。
  • 开闭原则(Open Closed Principle)
    即可扩展(extension),不可修改(modification)原则,抽取出代码中不变的逻辑,封装可变的代码,
    策略模式就很好的表达这个原则的模式,可以查看之前的博客:
  • 里氏替换原则(Liskov Substitution Principle)
    继承必须确保超类所拥有的性质在子类中仍然成立,里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。关于里氏替换原则的例子,最有名的是“正方形不是长方形
  • 接口隔离原则(Interface Segregation Principle)
    尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含调用方感兴趣的方法,这也是我们把复杂功能分模块的应用法则。
    接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,但两者是不同的:
    单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
    单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
  • 依赖倒置原则(Dependence Inversion Principle)
    抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对抽象(接口)编程,而不是针对实现细节编程。上面在说改进架构的时候有说这个原则

具体的代码示例这篇帖子就不写了。

被bob大叔指到的你,一定能写出更完美的代码

重构代码

新功能的开发的同时要重构之前逻辑,坚持开闭原则,能达到事半功倍的效果。
工作闲暇时间去浏览现有的代码逻辑,我们每天都在成长,对系统的认知也在改变,思维方式也在不断的变化,用现在的眼光去审视旧的代码逻辑,大多数是能找可以优化的地方,或者隐藏的bug,重构他,不要以为这些只是一些挤牙膏式的调优,所有的事情都有一个从质变到量变的过程。

代码评审(code review)

代码评审在团队里还是很有必要的,代码评审不是口水战,也不是批斗大会,如果只是走形式code review的意义也就不存在了。
你写的代码是需要让团队的成员能看明白的,将来也是会有新的员工来维护你写的功能的,code review是一个能让团队内的其他成员快速了解新代码意图的办法。
大多数团队里程序员的水平参差不齐的,对业务和系统的理解深度也是不一样的,让团队内不同的人去code review能及时发现代码中的不足之处,哪些地方逻辑上有问题,哪里的业务没有考虑全面。

当一次提交的代码太多时,一下子是看不完,也可能理解不了,就要很评审整体思路,再review实现主干逻辑,最后才是实现细节。
需说明一下的是,code review 并不能完全发现代码中隐藏的bug,不要把找bug的任务和他混在一起。

学习多少构架或者框架知识,都不能阻止我们写烂代码。但当你沉下心来去打磨产品或者认真去实现一个功能时,你会在意你写的代码,会主动去写更清晰的逻辑,并改变和想办法去并处理糟糕的代码,希望这篇帖子有能帮助到你的地方。