本文转载自:
作者:黄子毅
链接:https://mp.weixin.qq.com/s/Fh72-rVU_I-ilCLQTCfgug
来源:前端精读评论
现在许多国内互联网公司的项目都持续了五年左右,美国老牌公司如 IBM 的项目甚至持续维护了十五年,然而这些项目却有着截然不同的维护成本,有的公司项目运作几年后维护成本依然与初创期不大,可以保持较为高效的迭代速度,但有的项目甚至改几个文案都会导致线上事故,研发效率变得越来越慢。
根据笔者的经验,尝试总结一些持续维护项目变得难以维护的原因,以及如何设计才能保持良好的可维护性。
精读
心态
如果不真心对待自己的项目,其实是很难做到良好可维护性的,所以第一点就是需要一个良好的心态。
作为项目管理者,一个项目一旦交给一位同学开发,那么就要完全信任这位同学的能力,因为实际上你已经不可能实质性的影响到开发细节了。有人可能觉得好的流程或者事后 CodeReview 能发现一些问题,但这永远是杯水车薪,比如下面这个例子:
小张接到任务研发透视表,要求这个透视表具有良好的开发体验并做好单测。
那怎么样做单测才算是有效的,如何同时保证开发体验呢?不同人会有不同的想法,也会有不同的结果。
有主人翁心态的小张
对于一个有一定经验,又对项目真正上心的小张来说,开发过程可能是这样的。
首先上来先写主要功能,比如考虑数据模型、绘图技术方案后,决定采用图形语法方式定义数据结构,在做了一系列高性能前置考虑后,快速做出来了一个原型,包含表格的渲染、操作、翻页、冻结等等功能。
但随着需求的深入,小张发现做到下钻、排序时,不知道为何影响到了列冻结的功能,而代码架构其实没什么大问题,抽象的也很好,主要就是一些细节的代码调用漏掉了,只要补上就立马打通了任督二脉,整套功能再度行云流水了起来。但不知道下次做树状展示结构时会不会又把之前的功能影响了,这始终是个隐患,于是小张开始思考先把单测加上再继续开发功能。
由于出问题的场景有很小部分是大量操作后偶然引发的,普通的函数式单测也无法保证覆盖的全面,因此小张决定做一个单测录制功能,他首先把对表格的所有操作 Action 化,让一套 json 可以描述所有用户操作,然后又在本地开发界面做了一个单测录制功能,即在页面上对表格功能拖拖拽拽时,就会实时生成这套用户操作 json,再把当时页面结构与内部状态记录下来作为对比依据,单测就还原这套 json 并与基准状态做对比就行了。
小张很快录制了很多原子操作的单测,比如表格的各种空数据状态、单行单列渲染、列冻结行冻结;然后又把一些功能混合的场景结合起来,比如列冻结时排序,翻页后进行下钻;最后又把一些随机复杂的功能组合在一起,形成一些日常容易出问题的特殊单测 case,比如表格单页后突然清空数据,再强制冻结第二列,再灌入3列数据并对第2行做排序,再取消列冻结并翻到第4页。以后每当遇到一个边界 case 时,小张都会把这个问题 case 记录到单测,验证确实运行失败,再进行修复,直到包含这个单测在内的所有单测都验证通过后,才算开发完成。
打工人小张
对于为了混口饭吃的小张来说,开发过程可能是这样的。
首先上来写主要功能,把各种表格功能做完后,也遇到了一样的边界 case 难题,此时小张本来想 case by case 修复,但又想到 leader 要求他写单测,觉得倒也不坏,就创建了单测目录。
怎么写单测呢?首先小张把遇到的问题修了,毕竟谁也不希望自己手里的 bug 太多,但至于录到单测就太麻烦了,反正大家也不知道这个 case,修掉了就再也不会出来了吧,那就只把 leader 要求的几个基本功能单测加上去,看下覆盖率也达到硬性指标就行了。
大团队代码总是容易走向混乱
假设你是 leader,你不知道自己团队的小张到底是主人翁小张还是打工人小张,企图通过 code review 来统一提升团队的代码质量,实际上可行吗?
如果不幸遇上了打工人小张,他在 code review 时展示的代码结构就不是能做整体单测的抽象,你只能看着单测文件硬提一些比如 “多加一些单测,多考虑一些情况” 的建议,实际上完全达不到主人翁小张做的效果。
这背后的原因是影响代码质量的因素太多,比如 Action 化,比如各种极端 case 的录入,比如全流程的单测形式,这些对代码来说都是质变,但 code review 时看到的代码就是不够抽象,不够 Action 化的,不可能把代码推翻重写一遍,只能在已有代码基础上提优化建议,而到这个时候,神仙也没法让打工人小张的代码优化为主人翁小张的,除非推翻重写。
这就是心态的影响力,能把项目做好的细节很多,而且细节之间还是环环相扣的,比如不把代码 Action 化就不方便做整体单测,但如果开发者打一开始就没想好好设计,code review 时又有多少人能想到这一点呢?想到了此时再提可能也为时太晚,一切都已成定局。
这些年笔者看过不少久经历史的代码,因为大公司有大量的开发者维护同一个项目,每个人开发时的心态都各有不同,会发现总能看到那些模块是用打工人心态做出来的,而你想彻底优化就只能彻底重写,但碍于项目体量太大时间上不允许,只能沿着打工人思路洋洋洒洒的继续写下去。
所以拥有一个良好,正面或者说积极的主人翁心态来写代码,一般来说都可以维护好复杂项目。
解耦
复杂项目的复杂指的是什么呢?是指功能多吗?其实不然。
如果仅从功能多就判定这个项目复杂,那我们身处的社会才是最复杂的系统,但社会中的每个玩家都没有觉得吃穿住行很难,核心原因就在于了解我们用到的场景只需要少量的知识,而做出一个行动要得到正确的结果,也不会造成太大的影响。比如出门买菜,只要做个公交车到菜市场,扫一下码就完成了交易,而不需要对背后的城市公交体系与菜市场背后的金融体系有任何深入的了解,你不需要理解公交车是哪儿来的,菜农手里的菜是从哪儿收购的。
但代码世界就很有趣了,在代码世界买个菜可能会导致世界毁灭。这就导致每一个项目开发人员,哪怕是去买个菜,也要受过总统级训练,对各种国家级大事做出正确的预案,为什么会这样呢?
因为代码世界的逻辑是不同开发者码出来的,在实现世界底层逻辑时可能就埋下了耦合的种子,导致你不知道为什么买菜会触发那么严重的事情。举个例子,改一个文案导致系统崩溃,原因可能是某处错误兜底逻辑用字面量判断了这个文案,而你把文案改了,这个判断就失效了。有的程序员挺难的,在这种项目环境下生存,每一步修改都要小心翼翼。
这个问题的解决办法就是解耦,在这里我们不细说具体怎么解耦,因为每个场景的解耦方式都不同。我们只需要理解几乎所有的业务逻辑都可以用解耦的方式做,就行了。只要你按照这样的大思路去设计系统,不论路径是怎样的,最终都能设计出一个漂亮的系统级方案。
比如做一个 BI 系统,看上去里面有各种复杂的模块可能会产生相互影响,比如数据处理、仪表盘搭建、大屏搭建、图表、GIS 地图等,在设计之初就要假装其他模块不存在,来考虑每个模块必要的输入是哪些。
比如布局,它仅仅用于对画布进行布局,为了保证布局系统是完全解耦的,必须让项目支持在无布局的环境下运行。为了做到这一点,就必须让布局真的 “只做布局”,而不存储当前画布结构,这样才不会因为布局系统被移除时,影响组件的联动,因为组件联动需要利用画布结构 API。
图层列表也可以和布局解耦,因为图层列表只关心画布的组件树结构,而不关心布局是如何实现的,所以画布的组件树结构就像生活中的金钱,大家都可以用它交易,而无需关心它流向了何方,被谁使用。
数据逻辑与画布结构无关,只需要关心表达式以及用户对维度度量的配置、聚合方式以及图表本身的特性进行查询 sql 拼接即可,唯一用到的通用资源是当前组件实例信息修改后,需要更新到画布的组件树上。
社会也是建立在这种底层认同上,才能这么解耦的,所以在复杂项目中一定要有一个大家都认可的底层概念,这个概念应该尽可能通用化(想想金钱什么都能买,如果只能买蔬菜就麻烦了)、贯穿整个业务逻辑(金钱是现代社会任何交易都必须的媒介)。
许多项目被诟病难改,往往是没有遵循这条逻辑,硬生生把可以不相关的概念耦合了。比如某个筛选器条件变化时,对某个组件做特殊操作,这个场景可以控制反转为,这个组件在接收到某些筛选条件时,自己做特定的操作。因为对 BI 系统来说,筛选器的输出要作为图表绘图的输入,在这个底层框架下,就不要再开辟一条筛选器关心到具体图表的逻辑了。
总结
维护好一个复杂项目很难,这次分享了两个实践中有用的方案,第一个抱有主人翁心态设计代码,要在设计之初就做好考量,不要寄希望于对没有好好设计的系统做缝缝补补。第二是深入理解为什么现代社会的运作巧妙之处,尽可能把代码架构组织一定程度映射到社会的运作机制上,目前来看,社会最适合代码借鉴的思路就是解耦,再利用庞大的分工协作网络完成单人无法完成的工作。