烂代码,升维来看是什么问题导致的呢?
烂代码是个有意思的话题,Martin Fowler 的著作《重构:改善既有代码的设计》里面就专门提到了代码异味(code smells),特别认同他说的:“代码异味是一种表象,它通常对应于系统中更深层次的问题。”
如果你的系统中到处都是烂代码,就要思考深层次的原因了,需求太奇葩?进度太紧?水平不行?没有设计?或者多个因素一起?
Martin给出的解决方案是重构,这绝对是很好的建议,只是执行起来有难度!
- 我想重构代码,但是我没有工期怎么办?
- 我好不容易重构完,会不会没多久又老样子了?
- 我自己没有能力重构!
归根结底不光是个技术问题,还涉及软件工程和项目管理。
刚写代码的时候,都是烂代码,后来见过好代码,也开始学着提升代码质量,后来自己代码质量上来了发现别人写烂代码,等到带团队了就要去解决团队中烂代码。
理想态是团队的代码质量不错,即使有几个刚毕业的新手程序员也不让烂代码混进去,这样产出也高,一个功能实现速度是别的团队的1.5倍到2倍甚至更快。
在解决团队烂代码问题上的尝试。
最先尝试的是代码审查,所有更新的代码都先在分支开发,合并到主干前要代码审查。顺便说一下:对于直接在主干开发,代码合并后再审查那种效果是会大打折扣的,事后改起来很困难,而且优先级会很低。
代码审查严格执行的话,效果是很明显的。但也遇到很多问题:
1. 代码命名、格式不符合规范,肉眼很难看的过来怎么办?
2. PR(Pull Request)太大,要审查的代码太多,看不过来怎么办?
3. 急着要上线的功能,代码质量一般甚至很烂怎么办?
4. 自动化测试覆盖不全怎么办?
5. 代码能实现功能,执行也没问题,但是结构混乱或者没有遵循最佳实践,批准还是不批准?
6. 原本的项目就是个“屎山”代码,新代码只能修修补补怎么办?
代码审查的一部分工作要工具化自动化。
很多代码审查,靠肉眼是查不过来的,需要靠工具配合:
- 源代码管理工具不可少,可以清楚看到代码改动的代码审查工具不可少
- CI(持续集成)必不可少,每次提交代码要借助CI运行一些自动化测试
- lint必不可少,借助lint检查命名、格式等问题
- 自动化测试(单元测试和集成测试)不可少,代码的更新不能破坏现有功能,新增了功能,相应的也要新增自动化测试代码
PR不能太大,大PR要尽可能拆成小PR
越小的PR越好Review,反之太难,尽可能拆小
对于工期紧,急于合并的PR,质量差的不要轻易合并,需要后续改进的要有Ticket跟踪
生产环境的紧急补丁、deadline将近的新功能,这些PR很难说NO,只能先合并,但大部分时候就是合并了再无后续,最终一点点污染了整个代码库。
所以总的经验就是:质量差的,宁可推迟发布;质量没问题,但是测试不全的或者可以有优化空间的,都要求创建一个或多个Ticket去跟踪,并且这些Ticket的优先级和其他正式功能的Ticket优先级是一样的,必须在后续的Sprint里面去尽快完成。
对于稍微复杂一点的功能,写代码之前要先做系统设计
在代码审查时,最初经常遇到的一个问题就是,一个功能安排下去,开发很快就实现,结果你一看代码,质量也不算太差,但是没有遵循好的实践,单个代码没问题,合在一起就很难阅读和维护。
要说PR不批准推翻重写吧,人家都花了几天甚至几周时间在上面了,说要重写肯定会很抵触,要是小修改也意义不啊。这种情况很难处理。
后来就找到一个好的解决方案,就是先做设计,因为设计阶段,只需要写简单的文档,画几张结构图,通过设计Review会议,一发现方向不对,马上就可以调整,很容易就对齐思路,等到最终实现代码,提交PR的时候,基本上和当初讨论的设计不会有太大出入,也不会再出现前面说到的要推翻重写的情况。
写代码之前先做设计绝对是磨刀不误砍柴工的好事,写设计文档倒逼着开发人员在实现前先想清楚,设计Review也是一个很好的机会让团队其他成员学习怎么做设计,怎么遵循最佳实践。
唯一的问题就是要求团队里面有资深的开发人员,能在设计Review期间发现问题,指出问题,提出改进意见,否则就意义不大。
对于什么样的功能需要做系统设计,我们团队是用T恤尺码来对应任务大小的,比如S、M、L、XL等,我们的要求是M及以上的都要求做系统设计,S的可以不做。
强制代码审查后才能合并,以及强制先设计再写代码,很好的帮助了我的团队控制代码质量。
但这还不够,还经常遇到的问题就是:一些旧的屎山项目怎么办?技术债务怎么办?
老项目的策略是这样的:
1. 不值得维护的,隔离起来,保证它能运行就够了,只打补丁,不增加新功能,也不会再更新
2. 需要长期维护的,需要经常新增功能的,那么就要通过重构去改进完善。
老项目重构的常用策略是:
1. 先补自动化测试代码,尤其是集成测试,这些自动化测试保证以后我做任何修改,都不会出大的问题
2. 逐个模块替换,而不是一下子推翻重写然后迁移。
比如要用NextJS重构一个重要的前端项目,不是先写一个新项目,然后整个替换掉,而是先写创建一个组件库,让这个组件库在新旧项目中可以共用,每次新的组件写好了,替换老项目中的相应模块,在老项目中充分测试后,再应用到新项目,这样等到老项目的模块都替换的差不多了,新项目也已经充分测试过了,迁移的风险就很小。
再说技术债务,借债不是坏事,但是债务是有利息的,最好的还债方式就是像房贷一样,每个月定期还一部分,而不是攒最后一起还,那太难了。
首先会用任务管理系统跟踪所有的技术债务,每个技术债务相关的任务都会创建成ticket,对于复杂的,会进一步拆分成小的ticket。
然后在每一个Sprint,我技术债务相关的任务在20%左右,这样的任务和产品需求任务优先级是一样的。这样基本上我的代码库的技术债务一直维持在一个很良性的水平。
有的团队就是另一种情况,去年花了一个季度的时间去偿还技术债务,因为到了实在非还不可的地步,那个季度基本上无法响应来自产品的需求。我个人是不太推荐这种方式的,对开发团队和产品团队都是巨大的压力。
最近还探索出一个比较好的开发模式:
就是两周的Sprint中,我们第一周只做产品的需求,然后第一周开发完成后部署到测试环境测试,第二周就是修复各种新功能的Bug,但是相应的bug修复工作量没多少,所以我们在第二周就可以有时间去做纯技术相关的任务,比如偿还技术债务、测试新的技术栈、开发公共组件。因为持续有产品功能交付,所以产品经理也接受了这种模式。
除了老项目的债务问题,新项目也有一个问题,就是架构设计出来了,代码实现的时候,程序员的水平是参差不齐的,有新手有老手,就是是老手,还有各自的个人喜好在里面,这也给代码质量带来很大挑战。
所以如果是我自己设计的架构或者我带的项目,我会在架构设计完成后,自己或者要求其他人基于架构设计,先实现一些基本的功能模块,把基本的场景都覆盖,在这些模块实现的时候,形成一个好的开发实践,比如前端项目,可以遵守Redux的状态管理实践。这个阶段也一样要有代码审查,团队里每个人都可以提出自己的意见反馈,根据反馈还可以对最佳实践做出调整。
有了最佳实践,其他人在实现时,就可以照葫芦画瓢,按照最佳实践就可以写出质量不错的代码,对于不符合最佳实践的代码,在代码审查阶段就应该果断拒绝。
简单总结一下:
1.代码审查很重要,而且一定要先审查再合并
2.系统设计也很重要,坚持先系统设计再对3.设计评审再实现代码
4.代码要有自动化测试覆盖
5.技术债务要定期还
6.老项目重构可以一点点替换
7.新项目或已有项目要有最佳实践供新手参考,并且大家一起遵守
所以烂代码这个问题,升维来看本质上需求管理问题,需求本身影响着实现质量。缺乏专业的需求管理能力,盲目扩大需求范围,力求多和快,忘记了好和省了。