“捡垃圾”式代码重构实践

282 阅读10分钟

我们常说的“重构”有两个层面,一个是架构,一个是代码。架构上一旦成型去修改就很难了,重构起来就是伤筋动骨的事情。但是代码上的重构,更多是细节的修改,非常适合加入到我们日常的 coding 工作中。

那日常的 coding 中如何去重构代码呢?这里给大家推荐“捡垃圾”式重构法。

什么是“捡垃圾”式代码重构?

你在办公室发现一块垃圾在地上,作为一个现代文明人,肯定会第一时间捡起来扔进垃圾桶。

那当你在阅读某段代码后,发现功能实现了但是写得并不好,你会该怎么办?作为一个合格的开发者,我们可以以“捡垃圾”的方法进行重构。

当然估计有人会说:别人写的代码和我有什么关系?这里我没办法说服你,有的人对代码有很强的归属感,甚至不能容忍别人动他一行代码,各位只能根据实际情况自行判断。但是如果你没法推动别人修改,那从自己的老旧代码做起也无妨。

为什么“捡垃圾”式重构很重要呢?

作为大部分时候都在写业务代码的业务开发,业务方不一定会给你充分的时间去重构。

如果我们提出要停下手头的活,进行较长时间的重构,就要说服整个团队。毕竟,你的这个改动,需要推掉部分需求开发,需要进行全方面的回归。可以想象的是,你从提出想法的那一刻开始,就无法获得的支持。

所以把代码重构融入日常工作就非常重要。

“捡垃圾式”重构有意思的一点在于,看到一点就改一点,防微杜渐,能通过量变能引起质变,自下而上地改变整个代码。

在 Martin Fowler 的《重构》第一章给出了一个很好的例子。

文中讲了一个打印戏剧票的函数的重构过程,这个过程其实经历了两次需求的变化:

  • 需求1:从支持输出打印文本,到输出 HTML
  • 需求2:从固定的两种戏剧类型,到支持多戏剧类型

而作者做了如下的几次改动:

  1. 大函数拆分小函数:使增强可读性
  2. 拆分数据处理函数与格式打印函数:使数据层于呈现层分离,支持需求1
  3. 提取类,实现多态:增强代码重用性,支持需求2

但是这些大改动都是从改名字等这些影响可读性的小细节做起,一步步地改造程序,到最后提取了一个戏剧的类出来,这就是一个量变引起质变的过程。

如何实践?

最近一次,系统迁移过程中,我发现这个系统多个页面都是增删改差的页面,但是页面并没用做重用。于是想着能不能趁着这次迁移,做一个重构。

由于时间较充裕,我并没有征求小伙伴的意见,而选择了直接开干。

重构开始

当然直接这个开干,并不是真的直接推倒重来,不是从上到下,先设计一个增删改查的组件,然后再套用这个组件去重写所有页面。

我要用捡垃圾的方法,一点点地重构。

首要任务要保证,所有的页面能正常运行。这里采用了直接“复制-迁移”的方法。

  1. 复制第 1 个功能页面,修改配置,保证系统正常运行
  2. 细节修改,保证代码可读性,保证系统正常运行
  3. 复制第 2 个功能页面,修改配置,保证系统正常运行
  4. 细节修改,保证代码可读性,保证系统正常运行
  5. 如果有时间则对比第 1、2 个页面,提取公共方法、公共组件,保证系统正常运行
  6. 复制第 3 个功能页面,修改配置,保证系统正常运行
  7. 细节修改,保证代码可读性,保证系统正常运行
  8. 如果有时间则对比第 3 个页面与公共组件、方法,看看是否直接使用公共的组件、方法
  9. 如果无法直接使用,是否可以通过对公共组件、方法小量修改,使其适用新页面
  10. 重复步骤 6 - 9

捡垃圾式重构的一个精髓在于“保证系统正常运行”,每次仅进行小改动,每一次小改动,都需要想办法保证系统正常运行。

增强可读性

由于团队规范没做好、没执行到位,所以代码可读性是首要解决的问题。

1. 重命名文件名

按 Vue 官方指南,首先改掉了文件名:

截屏2022-06-26 22.55.29.png

更改之后,使用了更少的文件夹,但是整个层级更清晰了,如果前面 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() {
    高级()
}

按这样来划分层级,梳理函数的调用关系,我们的逻辑会更加清晰。

拆分组件

相对面向对象类的写法,可能目前前端更多去做拆分组件的工作。我这里也没有去做类的写法,而选择了拆分组件。

拆分组件的工程量比拆分函数要大,所以也要一步步来:

  1. 按上面操作,将相关代码放置一起
  2. 复制一个功能点到新组件中
  3. 在页面中引入新组件
  4. 确定要内部的私有变量,与公有变量;
    • 公有变量使用 props,接受父组件的值
    • 私有变量使用 data, 不暴露出去
  5. 确定要调用的父组件的方法(如,业务数据处理的方法),再次将这部分函数迁回到父组件中。
  6. 如果组件有多个功能点,重复执行 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 工程师慢。

当然,因为我们的组件拆分得足够细,接入组件的过程我们也是可以拆分的。

  1. 接入孙子组件(如上的翻页器:ListPagePagination),修改代码使系统正常运行
  2. 如果有新功能,在孙子组件中实现,保证新旧页面正常运行
  3. 重复 1-2,直至全部接入孙子组件
  4. 对比页面与子组件,修改代码,分离与子组件不一致的部分
  5. 将与子组件一致的部分接入子组件

简单的页面可以使用直接接入的方法,而与子组件存在较多不同,逻辑较复杂的页面,则可以使用上面的方法先接入孙子组件,逐步完成接入子组件的目标。

总结

完成了一次自下而上的代码重构,最开心的,是做到了每次只改动一小点,每次改动都不影响系统的正常使用。

这样的想法,对编程思维有很好的锻炼。推翻了我以前常有的“删除旧代码,从头开始设计”的不切实际的想法。

同时,因为每一次都是小改动,平均下来,一次变动可能就是0.5-1小时的事情。修改完成之后,我可以根据实际情况选择继续开发新需求,或者继续优化。对项目进度而言影响非常小,我甚至都没和其他小伙伴商量就把这事情完成了。

“捡垃圾”式重构应该成为我们日常 coding 的必要工作。它也是我后续持续去做的事情!