最近接手了一个低代码平台的项目,代码质量堪忧,用"烂代码"来形容一点都不为过。全局变量满天飞、函数嵌套十几层、复制粘贴到处都是、注释和代码对不上、一个文件几千行、改一个bug要翻半天代码、加一个功能要改十几个文件,改完还不知道会不会影响其他地方。
刚接手的时候,我是崩溃的,甚至想过推翻重写。但冷静下来想,推翻重写风险太大,业务逻辑都在旧代码里,重写很容易遗漏业务逻辑,而且时间也不允许。所以最终决定,在现有代码的基础上,进行一次系统性的重构。
花了一个多月时间,边做需求边重构,终于把这个项目的代码质量提升了一个档次,从"烂代码"变成了"优雅代码"。今天来聊聊这次重构的经历和经验,从发现问题、制定计划、分步实施,到重构后的效果和一些心得,分享给同样在和烂代码作斗争的朋友。
一、这个项目的代码有多烂
先说说这个项目的代码到底有多烂,让大家有个直观的感受。这个低代码平台是前端项目,用的是Vue,代码量大概十几万行,是好几拨人迭代出来的,每个人的编码风格都不一样,也没有统一的规范,积累了很多技术债。
具体的问题主要有这几个方面:
1. 全局变量满天飞
项目里定义了大量的全局变量,挂在window上,什么window.currentComponent、window.designerState、window.tempData,到处都是。这些全局变量,任何地方都能读能写,你根本不知道某个变量在哪里被修改了,出了问题排查起来非常困难。而且全局变量命名也不规范,什么temp、data、obj、a、b,看名字根本不知道是什么意思。
最夸张的是,有一个全局变量叫window.flag,在十几个地方被赋值,每个地方赋值的含义都不一样,有时候表示是否选中,有时候表示是否编辑,有时候表示是否加载,改一个地方的逻辑,可能影响好几个地方,非常容易出bug。
2. 函数又长又复杂,嵌套十几层
项目里有很多超级长的函数,一个函数几百行甚至上千行,嵌套了十几层if-else和for循环,看的时候要来回滚动屏幕,看到后面忘了前面。而且函数的职责不清晰,一个函数干了十几件事情,取名却叫handleClick或者onChange,根本不知道它到底干了什么。
有一个函数叫saveDesign,我数了一下,有八百多行,干了这些事情:校验数据、转换数据格式、调用保存接口、处理返回结果、更新本地状态、触发其他组件更新、记录操作日志、弹出成功提示、处理失败情况、重试机制。这么多事情塞在一个函数里,改任何一个环节都可能影响其他环节,非常脆弱。
3. 复制粘贴严重,代码重复率高
项目里的复制粘贴非常严重,同样的逻辑,在不同的文件里复制了好几份,只是改了几个变量名。比如,组件的拖拽逻辑,在设计器里写了一遍,在列表里又写了一遍,在模板里又写了一遍,三份代码几乎一样,但又有细微的差别,改bug的时候要改三个地方,很容易漏改。
我用工具检测了一下代码重复率,超过30%,也就是说,有三分之一的代码是重复的。这么高的重复率,不仅维护成本高,而且很容易出现不一致的bug,改了A地方忘了改B地方。
4. 组件设计不合理,耦合严重
Vue项目本来应该是组件化的,但这个项目的组件设计非常不合理。组件之间耦合严重,父组件直接修改子组件的data,子组件直接调用父组件的方法,甚至通过$parent.$parent去访问祖父组件,组件之间的依赖关系像一团乱麻,改一个组件可能影响好几个组件。
而且组件的粒度也不合理,有的组件太大了,一个组件几千行,干了十几件事情,应该拆分成多个小组件;有的组件又太小了,只是包了一层div,没有实际意义,增加了组件树的复杂度。
5. 状态管理混乱,数据流向不清晰
项目里的状态管理非常混乱,有的状态存在Vuex里,有的存在组件的data里,有的存在全局变量里,有的存在localStorage里,同一份数据可能在好几个地方都有副本,而且不同步。你根本不知道某个状态的"单一数据源"在哪里,改了一个地方,其他地方没更新,就出现了数据不一致的bug。
而且数据的流向也不清晰,有时候是父组件传子组件,有时候是子组件改父组件,有时候是通过事件总线,有时候是通过全局变量,数据到处流,出了问题根本不知道数据是从哪里来的、被谁改过。
6. 没有注释,或者注释和代码对不上
项目里的注释非常少,大部分函数和复杂逻辑都没有注释,看代码的时候要自己猜逻辑。而且仅有的一些注释,很多和代码对不上,可能是代码改了但注释没更新,看注释反而会被误导。
有一个函数,注释写的是"保存设计稿",但实际代码干的是"发布设计稿",因为后来需求变了,函数的逻辑改了,但注释没改。我刚开始看注释,以为是保存,结果改了半天发现不对,后来仔细看代码才发现是发布,被注释坑得不轻。
7. 没有测试,改代码全靠猜
项目里没有任何单元测试和集成测试,改代码全靠猜,改完了手动点一遍,觉得没问题就上线了。但手动测试覆盖不了所有场景,经常是改了一个bug,又引入了新的bug,线上问题不断。
而且因为没有测试,重构的时候风险很大,你根本不知道改了代码之后会不会影响其他地方,每改一处都提心吊胆的,要反复检查、反复测试,效率很低。
这些问题加在一起,导致这个项目的维护成本非常高,改一个简单的需求要花很长时间,而且很容易出bug。团队里的人都不愿意碰这个项目,一碰就头疼。所以,重构势在必行。
二、重构前的准备:不打无准备的仗
重构不是上来就改代码,那样很容易改出问题,而且越改越乱。重构之前,一定要做好充分的准备。
1. 通读代码,理解业务逻辑
重构的第一步,不是改代码,而是读代码,把整个项目的代码通读一遍,理解业务逻辑、数据流向、组件关系、模块划分。只有真正理解了代码,才能知道哪些地方该改、怎么改,改了之后会不会影响业务逻辑。
我花了大概一周时间,把项目的核心代码通读了一遍,画了几张图:组件关系图、数据流向图、模块依赖图。通过这些图,对整个项目的结构和逻辑有了清晰的认识,也找到了最核心、最需要重构的部分。
通读代码的时候,我还做了一件事情:把发现的问题都记录下来,比如哪个文件太长、哪个函数太复杂、哪里有重复代码、哪里耦合严重,列了一个问题清单。这个清单就是后面重构的依据。
2. 制定重构计划,分阶段进行
通读代码之后,根据问题清单,制定了一个详细的重构计划,分阶段进行,不追求一步到位。
我的重构计划分了四个阶段:
- 第一阶段:建立规范和基础设施。先统一编码规范,引入ESLint和Prettier,建立代码检查机制;搭建单元测试框架,给核心模块写测试;建立CI/CD流水线,保证代码质量。这个阶段不改业务逻辑,只是建立基础设施,为后面的重构保驾护航。
- 第二阶段:清理和整理。删除无用代码、重复代码、注释掉的代码;统一命名规范;整理文件结构,把文件放到合适的目录下;给复杂的函数和逻辑加注释。这个阶段也不改业务逻辑,只是清理和整理,让代码更干净、更易读。
- 第三阶段:模块化和组件化重构。这是最核心的阶段,把大函数拆成小函数,把大组件拆成小组件,提取公共逻辑,消除重复代码,优化组件之间的依赖关系,引入状态管理,统一数据流向。这个阶段会改业务逻辑的结构,但不改业务逻辑的行为,每改一处都要测试,保证行为不变。
- 第四阶段:性能优化和细节打磨。前面三个阶段完成之后,代码结构已经比较好了,这个阶段做一些性能优化,比如懒加载、缓存、减少重渲染,以及一些细节的打磨,让代码更优雅、更高效。
这个计划不是一成不变的,在重构的过程中,根据实际情况不断调整。但有了计划,就有了方向,不会盲目地改,也不会改到哪里算哪里。
3. 建立测试,为重构保驾护航
这是最重要的一步,也是很多人忽略的一步。重构之前,一定要有测试,否则你根本不知道改了代码之后行为有没有变,很容易改出bug。
这个项目原来没有任何测试,所以我在第一阶段就花时间搭建了单元测试框架,给核心模块和核心函数写了单元测试。虽然写测试花了一些时间,但这些测试在后面的重构中发挥了巨大的作用,每改一处代码,跑一遍测试,就能知道有没有改坏,心里很踏实。
除了单元测试,我还做了一件事情:把核心业务流程录了下来,作为手动回归测试的用例。每次重构完一个模块,按照这个用例手动点一遍,确保核心流程没问题。
有了自动化测试和手动测试用例,重构的风险就大大降低了,可以放心地改代码。
4. 和团队沟通,达成共识
重构不是一个人的事情,尤其是团队项目,一定要和团队成员沟通,达成共识。否则,你在这边重构,别人在那边加需求、写烂代码,重构的成果很快就会被破坏。
我在重构之前,和团队的所有成员开了一个会,把项目的代码问题、重构的必要性、重构的计划和目标,都跟大家讲清楚了,达成了共识。大家都同意,在重构期间,新写的代码必须遵循新的规范,不能再写烂代码;改旧代码的时候,要顺手重构,不能继续在烂代码上堆烂代码。
而且,我还制定了代码评审(Code Review)机制,所有提交的代码都要经过评审,不符合规范的代码不能合并。有了这个机制,就能保证新写的代码质量,也能保证重构的成果不被破坏。
三、重构的具体做法:分步实施,小步快跑
准备工作做好之后,就开始正式重构了。重构的原则是:分步实施,小步快跑,每一步都可测试、可回滚,不追求一步到位。
下面说说具体的做法。
1. 统一规范,让代码先"好看"起来
重构的第一步,是统一编码规范,让代码先"好看"起来。我引入了ESLint和Prettier,制定了团队的编码规范,包括命名规范、缩进、空格、引号、逗号、函数定义、import顺序等,然后用工具自动格式化和检查。
这一步看起来简单,但效果很明显。统一规范之后,代码的风格一致了,看起来舒服了很多,也减少了很多因为风格不一致导致的问题。而且,ESLint还能检查出一些潜在的bug,比如未使用的变量、未定义的变量、比较运算符的错误等,提前发现问题。
这一步不改业务逻辑,只是格式化代码和修复规范问题,风险很低,但收益很高,建议作为重构的第一步。
2. 删除无用代码,给代码"瘦身"
项目里积累了很多无用代码,比如注释掉的代码、从来没被调用过的函数、已经废弃的组件、重复的变量、调试用的console.log。这些无用代码不仅增加了代码量,也增加了理解成本,看代码的时候要花时间分辨哪些是有用的、哪些是没用的。
我花了几天时间,把这些无用代码都删掉了。删除的时候要小心,先用工具检查哪些代码没有被引用,再人工确认一遍,确保删的是真的无用代码,不要删错了。对于不确定的,可以先注释掉,观察一段时间没问题再删。
删除无用代码之后,项目的代码量减少了将近20%,代码更精简了,看起来也更清晰了。给代码"瘦身",是重构中最容易做、也最有成就感的一步。
3. 拆分大函数,让函数"单一职责"
这是重构中最核心的一步。项目里有很多又长又复杂的大函数,一个函数干了好几件事情,违反了"单一职责原则"。这些大函数很难理解、很难测试、很难维护,是很多bug的来源。
我的做法是,把大函数拆成小函数,每个小函数只干一件事情,函数名清晰地描述它的职责。比如,前面提到的那个八百多行的saveDesign函数,我拆成了十几个小函数:validateData、transformData、callSaveApi、handleSaveResult、updateLocalState、triggerComponentUpdate、saveOperationLog、showSuccessTip、handleSaveError、retrySave,每个函数只有几十行,职责清晰,一看名字就知道干什么。
拆分的时候,要注意几点:
- 先理解原函数的逻辑,画个流程图,搞清楚每一步在干什么。
- 按照逻辑步骤拆分,每一步拆成一个小函数。
- 拆分的时候,保证行为不变,只是把代码从一个函数搬到另一个函数,不改逻辑。
- 每拆完一个函数,跑一遍测试,确保没问题。
- 给小函数起一个好名字,名字要能描述函数的职责,不要叫handleClick1、doSomething这种没意义的名字。
拆分之后,原来的大函数变成了一个"编排函数",只负责调用各个小函数,逻辑清晰,一目了然。而且,小函数更容易测试,也更容易复用,其他地方需要同样的逻辑,可以直接调用这个小函数,不用复制粘贴。
4. 提取公共逻辑,消除重复代码
项目里的重复代码很多,同样的逻辑在好几个地方复制粘贴。重复代码是万恶之源,改bug的时候要改好几个地方,很容易漏改,而且不同地方的代码可能会慢慢出现差异,导致不一致的bug。
我的做法是,把重复的逻辑提取成公共函数或者公共组件,放在公共模块里,需要用的地方直接调用,不用复制粘贴。
比如,组件的拖拽逻辑,在设计器、列表、模板里都有,我把它提取成了一个useDrag的组合式函数(或者mixin),把拖拽的状态和逻辑都封装在里面,各个组件只要引入这个函数,就能拥有拖拽能力,不用再写一遍拖拽逻辑。这样,拖拽逻辑只有一份,改bug只需要改一个地方,而且各个组件的行为也一致了。
提取公共逻辑的时候,要注意:
- 先找到重复的代码,确认逻辑确实是一样的,或者只有细微的差别。
- 把公共逻辑提取出来,参数化那些有差别的部分,通过参数来控制不同的行为。
- 给公共函数起一个好名字,写清楚注释和用法。
- 替换原来的重复代码,改成调用公共函数,每替换一处,测试一处,确保行为不变。
消除重复代码之后,代码量减少了,维护成本降低了,bug也少了很多。
5. 优化组件设计,降低耦合
Vue项目的组件设计很重要,组件之间应该是低耦合、高内聚的,每个组件只负责自己的事情,通过props和events和外界通信,不要直接访问父组件或者子组件的内部状态。
这个项目的组件耦合很严重,我花了很多时间来优化组件设计:
- 把大组件拆成小组件,每个组件只干一件事情,职责清晰。
- 组件之间通过props传数据,通过events发事件,不要直接修改父组件的data,也不要通过$parent、$children访问其他组件的内部。
- 把共享的状态提到Vuex里,统一管理,组件通过getter获取状态,通过commit修改状态,保证数据的单一数据源。
- 用依赖注入(provide/inject)或者状态管理来代替深层的props传递,避免"prop drilling"。
- 给组件写清楚props的类型、默认值、是否必填,以及events的名称和参数,方便使用和维护。
优化组件设计之后,组件之间的依赖关系清晰了,改一个组件不会影响其他组件,维护成本大大降低。而且,组件的复用性也提高了,很多小组件可以在不同的地方复用。
6. 引入状态管理,统一数据流向
前面提到,这个项目的状态管理很混乱,数据到处都是,流向不清晰。我在重构中引入了Vuex,把共享的状态统一管理起来,建立清晰的数据流向。
具体的做法:
- 把全局共享的状态,比如当前用户、设计器状态、组件数据等,都放到Vuex的store里,作为单一数据源。
- 组件通过getter获取状态,不要直接访问store.state,也不要在组件里存一份副本。
- 修改状态只能通过commit mutation,或者dispatch action,不要在组件里直接修改store的状态。
- 异步操作放在action里,mutation只负责同步修改状态。
- 按照模块划分store,每个模块管理自己的状态,避免store太大太乱。
- 删掉原来的全局变量和事件总线,都改成用Vuex管理。
引入状态管理之后,数据的流向清晰了,状态的修改都有迹可循,出了问题很容易排查。而且,因为有了单一数据源,数据不一致的bug也大大减少了。
7. 给代码加注释,写好文档
重构的过程中,我还做了一件事情:给复杂的函数和逻辑加注释,写好文档。
注释不是越多越好,好的代码应该是自解释的,通过好的命名和清晰的结构,让人一看就懂。但对于一些复杂的业务逻辑、算法、边界条件处理,还是需要加注释,说明为什么这么做、有什么注意事项。
我加注释的原则是:
- 注释说明"为什么",而不是"做什么"。代码已经说明了做什么,注释应该说明为什么这么做,比如业务背景、设计考虑、注意事项。
- 给复杂的函数加注释,说明函数的用途、参数、返回值、副作用。
- 给复杂的逻辑块加注释,说明这一段在干什么、为什么这么处理。
- 给边界条件和特殊处理加注释,说明为什么要这么处理,不处理会有什么问题。
- 不要写废话注释,比如"i++ // i加1",这种注释毫无意义。
除了注释,我还写了一些文档,比如项目的架构说明、组件使用文档、状态管理说明、开发规范等,方便团队成员理解和使用。
有了注释和文档,代码的可维护性大大提高,新人接手项目也能快速上手。
四、重构后的效果
花了一个多月时间,重构基本完成了。重构之后,效果非常明显:
1. 代码量减少了30%。通过删除无用代码、消除重复代码、拆分函数,代码量从十几万行减少到了九万多行,精简了很多。
2. 代码质量大幅提升。函数平均长度从一百多行降到了三十多行,大部分函数都在五十行以内,职责清晰。组件的粒度更合理,耦合更低。状态管理统一,数据流向清晰。代码有了规范,风格一致。
3. bug率下降了。重构之后,因为代码结构更清晰、重复代码减少、状态管理统一,bug率明显下降,线上问题少了很多。而且,有了单元测试,很多bug在开发阶段就被发现了。
4. 开发效率提高了。重构之后,代码更容易理解,改需求和改bug的速度快了很多。以前改一个需求要翻半天代码、改好几个地方,现在因为组件化和模块化,只需要改对应的模块,效率提高了很多。
5. 团队士气提升了。以前大家都不愿意碰这个项目,觉得是个坑。重构之后,代码变好了,大家也愿意在这个项目上开发了,士气提升了不少。而且,新的规范和代码评审机制,也让团队的整体编码水平有了提升。
当然,重构不是一劳永逸的,代码质量的维护是一个长期的过程。重构完成之后,还要继续坚持规范、坚持代码评审、坚持写测试,不断优化代码,才能保持代码的质量,不会再次变成烂代码。
五、重构的心得和建议
最后,总结一些重构的心得和建议,给想要重构但不知道从何下手的朋友。
1. 重构不是重写,不要追求一步到位。很多人一提到重构,就想推翻重写,觉得旧代码太烂了,不如重新写一遍。但重写的风险很大,业务逻辑都在旧代码里,重写很容易遗漏,而且重写期间不能加新需求,时间成本很高。重构是在现有代码的基础上,逐步优化,不改行为,只改结构,风险低,而且可以边做需求边重构,不影响业务进度。不要追求一步到位,小步快跑,逐步优化,才是正确的重构方式。
2. 重构之前一定要有测试。这是最重要的一点。没有测试的重构,就是赌博,你根本不知道改了代码之后行为有没有变。重构之前,一定要先给核心模块写测试,有了测试保驾护航,才能放心地改代码。如果项目原来没有测试,那写测试本身就是重构的第一步,不要嫌麻烦,这是值得的。
3. 每一步都要可测试、可回滚。重构的时候,不要一次性改太多,要分步进行,每一步都要小,改完之后跑测试,确保没问题,再进行下一步。如果改出了问题,要能快速回滚。建议用Git,每改完一个小步骤就提交一次,出了问题可以回退到上一个提交。不要攒一大堆改动一起提交,那样出了问题都不知道是哪一步改坏的。
4. 重构要和团队达成共识,建立长效机制。重构不是一个人的事情,要和团队达成共识,大家一起维护代码质量。否则,你重构完了,别人继续写烂代码,很快又会变回烂代码。要建立长效机制,比如编码规范、代码评审、自动化测试、CI/CD,用机制来保证代码质量,而不是靠某个人的自觉。
5. 不要在重构的同时加新需求。重构的时候,最好不要同时加新需求,因为那样你分不清是重构改坏了,还是新需求的问题。重构就是重构,只改代码结构,不改业务行为,等重构完成、测试通过之后,再加新需求。如果实在要加新需求,也要先把当前的重构完成、提交了,再开始加新需求,不要混在一起。
6. 重构是持续的,不是一次性的。代码质量的维护是一个长期的过程,重构不是做完一次就完事了。每次改代码的时候,都要顺手重构,看到烂代码就改一点,看到重复代码就提取一下,不断优化,让代码越来越好。所谓"童子军规则":离开营地的时候,要比你来的时候更干净。写代码也是一样,每次提交代码,都要比你改之前更好一点。日积月累,代码质量就会越来越高。
六、写在最后
这次低代码平台的重构,是我做过的最大规模的一次重构,花了一个多月时间,踩了不少坑,也收获了很多。最大的感受是:烂代码不可怕,可怕的是不敢面对、不去改变。只要方法对、有耐心、分步进行,再烂的代码也能变成优雅代码。
当然,重构只是手段,不是目的。我们重构代码,是为了让代码更容易维护、更容易扩展、更少bug,最终是为了提高开发效率、提升产品质量。不要为了重构而重构,也不要追求代码的"完美",在业务价值和代码质量之间找到平衡,才是最重要的。
希望这篇文章能给正在和烂代码作斗争的朋友一些启发和帮助。记住,每一次重构,都是一次成长;每一次优化,都是一次进步。愿我们都能写出优雅的代码,也愿我们都能维护好代码的质量,不让它再次变成烂代码。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录