我们常说的“重构”有两个层面,一个是架构,一个是代码。架构上一旦成型去修改就很难了,重构起来就是伤筋动骨的事情。但是代码上的重构,更多是细节的修改,非常适合加入到我们日常的 coding 工作中。
那日常的 coding 中如何去重构代码呢?这里给大家推荐“捡垃圾”式重构法。
什么是“捡垃圾”式代码重构?
你在办公室发现一块垃圾在地上,作为一个现代文明人,肯定会第一时间捡起来扔进垃圾桶。
那当你在阅读某段代码后,发现功能实现了但是写得并不好,你会该怎么办?作为一个合格的开发者,我们可以以“捡垃圾”的方法进行重构。
当然估计有人会说:别人写的代码和我有什么关系?这里我没办法说服你,有的人对代码有很强的归属感,甚至不能容忍别人动他一行代码,各位只能根据实际情况自行判断。但是如果你没法推动别人修改,那从自己的老旧代码做起也无妨。
为什么“捡垃圾”式重构很重要呢?
作为大部分时候都在写业务代码的业务开发,业务方不一定会给你充分的时间去重构。
如果我们提出要停下手头的活,进行较长时间的重构,就要说服整个团队。毕竟,你的这个改动,需要推掉部分需求开发,需要进行全方面的回归。可以想象的是,你从提出想法的那一刻开始,就无法获得的支持。
所以把代码重构融入日常工作就非常重要。
“捡垃圾式”重构有意思的一点在于,看到一点就改一点,防微杜渐,能通过量变能引起质变,自下而上地改变整个代码。
在 Martin Fowler 的《重构》第一章给出了一个很好的例子。
文中讲了一个打印戏剧票的函数的重构过程,这个过程其实经历了两次需求的变化:
- 需求1:从支持输出打印文本,到输出 HTML
- 需求2:从固定的两种戏剧类型,到支持多戏剧类型
而作者做了如下的几次改动:
- 大函数拆分小函数:使增强可读性
- 拆分数据处理函数与格式打印函数:使数据层于呈现层分离,支持需求1
- 提取类,实现多态:增强代码重用性,支持需求2
但是这些大改动都是从改名字等这些影响可读性的小细节做起,一步步地改造程序,到最后提取了一个戏剧的类出来,这就是一个量变引起质变的过程。
如何实践?
最近一次,系统迁移过程中,我发现这个系统多个页面都是增删改差的页面,但是页面并没用做重用。于是想着能不能趁着这次迁移,做一个重构。
由于时间较充裕,我并没有征求小伙伴的意见,而选择了直接开干。
重构开始
当然直接这个开干,并不是真的直接推倒重来,不是从上到下,先设计一个增删改查的组件,然后再套用这个组件去重写所有页面。
我要用捡垃圾的方法,一点点地重构。
首要任务要保证,所有的页面能正常运行。这里采用了直接“复制-迁移”的方法。
- 复制第 1 个功能页面,修改配置,保证系统正常运行
- 细节修改,保证代码可读性,保证系统正常运行
- 复制第 2 个功能页面,修改配置,保证系统正常运行
- 细节修改,保证代码可读性,保证系统正常运行
- 如果有时间则对比第 1、2 个页面,提取公共方法、公共组件,保证系统正常运行
- 复制第 3 个功能页面,修改配置,保证系统正常运行
- 细节修改,保证代码可读性,保证系统正常运行
- 如果有时间则对比第 3 个页面与公共组件、方法,看看是否直接使用公共的组件、方法
- 如果无法直接使用,是否可以通过对公共组件、方法小量修改,使其适用新页面
- 重复步骤 6 - 9
捡垃圾式重构的一个精髓在于“保证系统正常运行”,每次仅进行小改动,每一次小改动,都需要想办法保证系统正常运行。
增强可读性
由于团队规范没做好、没执行到位,所以代码可读性是首要解决的问题。
1. 重命名文件名
按 Vue 官方指南,首先改掉了文件名:
更改之后,使用了更少的文件夹,但是整个层级更清晰了,如果前面 views 页面不多,我甚至可以将其提取到项目文件夹外。
注意: 改名之后,相关的文件引用路径需要改变,VSCode 的改名耗时较长,并且不是完全可靠,最好人工检验一次。
2. 重命名变量名
将常用的变量名做了一个统一,比如 create 做增加,delete 做删除等等,还有同样用 Vue 风格指南的动词后置方法,如 dialogOpen()、dialogClose(),将代码进行整齐划一的整改。
还有更重要的一点,如果一个函数名/变量名,你无法一眼就知道它的意思,就应该立即改名。
代码即注释,对于母语非英文的我们多少有点困难,但是能取一个简单易懂的名字,对我们阅读代码效率提升不是一星半点。一定要重视命名!
3. 将相关代码放一起
将对同一数据的处理逻辑放在一起。我们阅读代码的时候,不需要反复上下跳读,追溯变量的变化,整个过程更顺畅。
前端老旧时代的 var 放在大函数前面的做法,已经过时了。使用 const、let 来完成,我们可以做到更精确地控制变量,所以还是把它放在相关的代码块内更合适。
另外,相关性的代码放置在一起之后,是否可以提取出去,可以更好地做出判断。
4. 使用 map,reduce,filter 替代 for 循环
map、reduce、filter 的意义比 for 循环更清晰明了。
《重构》一书里,有更极致的例子,为了可读性,将一个 for 循环使用 map、reduce、filter 的方法拆成了 3 个循环,
这样带来的坏处是循环增多,对性能会产生一定的影响了。但是仔细想想,如果我们这个数据不是一个上千行的数据,是否会对前端的性能产生实质的影响?
基于此,更推荐去拆散 for 循环使用语义更明晰的 map、reduce、filter等循环方法。
拆分函数
经过上面的增强可读性之后,我们为重构提供了很好的基础。以上经过修改的代码,没有结构的大变化,应该是可以顺利运行的。于是就可以拆分函数了。
1. 单一原则
根据一个函数只做一件事,将大函数拆散。
想起来,之前定的代码规范,单个函数不能超过 50 行,真的太宽松。尤其是有 if else 的分支,常常里面有大量的处理逻辑。每次看到这些大块大块的代码,个头都嗡嗡声~
常常有前端的同学对为什么要执行“单一原则”有疑问,质疑将大函数拆成小函数的做法。但是如果你亲手实践一次重构,就会发现执行“单一原则”之后的代码,是进行后续步骤的充分条件,
如果没有一开始的拆解,后续所有的改造都举步维艰。
2,函数应该是有分级的
function 高级() {
中级1()
中级2()
}
function 中级1() {
低级1()
低级2()
}
function 低级1() {
doSomething;
}
拆分函数的过程中,我们应该对函数进行分级,比如说,对后端接口的操作,是我们前端最低级的函数;对数据的处理是中级函数;对样式处理的函数是高级的函数。
分好级别之后,我们不应该出现跨级调用,从低级跳到高级的情况等等。
// 不好的示例
function 高级() {
低级1()
中级2()
}
function 中级1() {
高级()
}
按这样来划分层级,梳理函数的调用关系,我们的逻辑会更加清晰。
拆分组件
相对面向对象类的写法,可能目前前端更多去做拆分组件的工作。我这里也没有去做类的写法,而选择了拆分组件。
拆分组件的工程量比拆分函数要大,所以也要一步步来:
- 按上面操作,将相关代码放置一起
- 复制一个功能点到新组件中
- 在页面中引入新组件
- 确定要内部的私有变量,与公有变量;
- 公有变量使用 props,接受父组件的值
- 私有变量使用 data, 不暴露出去
- 确定要调用的父组件的方法(如,业务数据处理的方法),再次将这部分函数迁回到父组件中。
- 如果组件有多个功能点,重复执行 2-5
不推荐使用父组件调用子组件的方法,子组件也不建议处理复杂的数据流程,类似于拆分函数时,越是底层的函数越是保持纯函数的写法。
重构告捷
经过这样的拆分,最后我成功将增删改查拆分除了 4 个小组件
- ListPage 对外公布的列表页组件
- ListPageSearcher 列表页的搜索输入框
- ListPageTable 列表页的展示表格
- ListPagePagination 列表页的翻页器
每个组件的行数在100行内,读起来心旷神怡。
接入组件
使用起来,只需要传入表单、表格配置,增删改查的方法,要绑定的 loading、data,就能完成一个简单的 CRUD 页面了。
<template>
<list-page
:data="data"
:loading="loading"
:tableConfig="tableConfig"
:formConfig="formConfig"
@getList="..."
@updateListItem="..."
@createListItem="..."
@deleteListItem="..."
></list-page
</template>
从此告别了无休止的 Ctrl + C、Ctrl + V 了。而且半天联调一个页面完全不成问题,不比 CV 工程师慢。
当然,因为我们的组件拆分得足够细,接入组件的过程我们也是可以拆分的。
- 接入孙子组件(如上的翻页器:ListPagePagination),修改代码使系统正常运行
- 如果有新功能,在孙子组件中实现,保证新旧页面正常运行
- 重复 1-2,直至全部接入孙子组件
- 对比页面与子组件,修改代码,分离与子组件不一致的部分
- 将与子组件一致的部分接入子组件
简单的页面可以使用直接接入的方法,而与子组件存在较多不同,逻辑较复杂的页面,则可以使用上面的方法先接入孙子组件,逐步完成接入子组件的目标。
总结
完成了一次自下而上的代码重构,最开心的,是做到了每次只改动一小点,每次改动都不影响系统的正常使用。
这样的想法,对编程思维有很好的锻炼。推翻了我以前常有的“删除旧代码,从头开始设计”的不切实际的想法。
同时,因为每一次都是小改动,平均下来,一次变动可能就是0.5-1小时的事情。修改完成之后,我可以根据实际情况选择继续开发新需求,或者继续优化。对项目进度而言影响非常小,我甚至都没和其他小伙伴商量就把这事情完成了。
“捡垃圾”式重构应该成为我们日常 coding 的必要工作。它也是我后续持续去做的事情!