跳转至

01 初识重构:重构的类型、收益和度量

你好,我是黄俊彬。这是我们专栏的第一课,我将系统给你分享重构的类型、时机、收益和度量。

在之前的很多咨询项目中,每当我和一些同学聊起重构时,都会被问到一个问题:到底什么是重构?一个开发同学说他在研发过程中,经常有产品、测试、项目等同学对他提代码重构的需求,比如后面这些情况。

  • 测试同学说:今天的版本测试又发现了内存泄露,你重构一下代码吧。
  • 产品同学说:这个界面的用户路径操作太深了,你重构一下代码吧。
  • 项目同学说:我们的线上Bug很多,质量太差了,你重构一下代码吧。

乍一听你可能会觉得这些同学说的好像也没错,但是你仔细一琢磨,就会发现这里面其实包含了性能优化、需求优化、缺陷优化等诸多内容,这些都算代码重构吗?另外,重命名一个方法、提取一个接口、单体架构组件化,这些又算是重构吗?它们之间有什么区别呢?

所以我觉得在课程的最开始,我们很有必要先理解清楚重构的概念,明白它能直接或间接为我们解决哪些痛点问题,以及如何来度量重构的收益。搞清楚这些定义后,上面的问题你都会有答案。

重构的类型和时机

首先,我们来看一下重构的定义。

《重构:改善既有代码的设计(第2版)》中写道:

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

注意,定义中提到“重构是不改变软件的可观察行为”,所以基于这一点,我们开头提到的需求优化、缺陷修复、性能优化等维度的代码调整都不属于重构。它们应该属于新需求、专项优化和软件缺陷修复等范围。

基于上述重构的定义,我们再来看看下面三种代码调整。

  • 重命名一个变量,使其具有更好的可读性。
  • 对A类和B类抽取公共接口C,将对A类和B类的依赖调整为对接口C的依赖,与具体的实现解耦。
  • 将大泥球的项目工程调整至组件化工程。

请你思考一下,这些调整是否属于重构?没错,基于前面的定义,它们都属于重构的范畴。但你可能会发现,这三种代码调整所影响的范围,或者说涉及的工作量又是不同的。其实,我们基于实际调整代码时所需的工作量和影响范围,可以将重构细分为三种类型:小型重构、中型重构和大型重构。

小型重构

小型重构是指对单个类内部的重构优化。通常包括对方法名称、方法参数数量、方法大小等内容的修改,下面我给你举几个常见的小型重构例子。

1.优化命名,使其更有含义。

//重构前
Category getCategory(long catID)  
//重构后
Category  getCategory(int categoryId);

我们通过对命名优化,可以避免缩写带来的混淆,让代码的可理解性更高。

2.引入一些解释性的变量。

//重构前
if ((platform.toUpperCase().indexOf("Android") > -1) &&
      (browser.toUpperCase().indexOf("Chrome") > -1))
{
  //... ...
}

//重构后
boolean isAndroid = platform.toUpperCase().indexOf("Android") > -1;
boolean isChromeBrowser = browser.toUpperCase().indexOf("Chrome")  > -1;
if (isAndroid &&isChromeBrowser)
{
  //... ...
}

我们通过提取解释性的变量名称,用变量名来解释表达式的用途,可以让代码的可理解性更高。

  1. 提取方法。
//重构前
public void onCreate(){
  ivAvater = findViewById(R.id.iv_avatar);
  ivBg = findViewById(R.id.iv_background);
  tvNick = findViewById(R.id.tv_nick);
  //... ...
  User user = momentsPresenter.getUserInfo();
  //... ...
  tvNick.setText(user.getNick());
  ImageUtils.loadAvatarBitmap(this, user.getAvatar(), ivAvater);
  ImageUtils.loadBitmap(this, user.getProFileImage(), ivBg, R.color.circle);
}

//重构后
public void onCreate(){
  initView();
  initData();
  bindView();
}

通过提取方法,将原有过大的方法拆分为更多职责更加单一的小方法,让代码的可理解性更高。

整体来说,小型的重构所需要的时间相对较少。在任何编码阶段,只要识别到代码存在方法命名含义不清、过大的方法、过多的方法参数等问题时,就可以及时重构。由于其影响范围小,并且可借助IDE进行自动化重构,因此比较安全。

中型重构

中型重构是对多个类间的重构优化,通常的一些修改包括提取接口、超类、委托等调整。给你举个例子,下面这段代码是一个文件的上传功能,需要根据用户的不同配置,将数据保存在不同的平台上。

//重构前
if(isBaiduYun){
  //调用百度服务上传文件
}else if(is aliYun){
  //调用阿里云服务上传文件
}else{
  //使用自己的服务器上传文件
}

//重构后
iFileUpload.upload(file);

如下图所示,我们可以通过提取抽象的上传接口,来简化判断的逻辑,提高代码的可维护性。

中型的重构所需要的时间比小型重构长,因此,建议你在添加新功能或者修复Bug时,找一个相对集中的时间段进行设计和修改。由于中型重构相对复杂,很难借助IDE完成所有的重构操作,所以在中型重构时,我们要充分做好测试。

大型重构

大型重构是对整个系统的架构进行重构优化,比如组件化、应用中台架构升级等,通常在做大型重构时也会伴随中小型的代码重构。以组件化为例,通过提取公用的基础组件和业务组件,来提高代码的可复用性,同时让业务能独立演进,就是一种大型重构。

对于大型重构,特别是对大型的遗留系统进行改造,需要的时间往往得按月计算。并且在此过程中,业务还需要不断地演进,所以建议对大型重构立专项执行。

另外,结合业务的迭代需求,我们可以把大型重构做一下拆分,在不同的研发迭代中重构。例如,重构的目标是将大泥球系统X拆分为组件A、B、C。由于组件B和C的业务在接下来的迭代中有较大的调整,那么就可以优先拆分出组件A。当然,实际的遗留系统,其耦合和依赖情况可能更复杂,需要具体分析后再制定对应的迭代策略。在后续的课程中,我会结合具体的项目带你详细拆解。

从前面的介绍可以看出,从小型重构到大型重构,虽然产生的价值越来越高,但时间周期和对代码的调整也越来越大。通常我们认为修改的代码越多,引起风险的可能性就越高,所以不同类型的重构所造成的影响和工作量是不同的,我们要选择合适的重构时机,否则重构可能无法顺利完成。

重构的收益

理解了重构之后,我们再来聊聊重构的收益。

这是一个比较有争议的话题,因为对于一个产品来说,重构不仅不会改变业务特性,还得团队另外投入时间,所以在国内一线交付压力如此巨大的情况下,重构往往都被排在业务迭代之后。而且,在遗留系统的问题上,研发人员和业务人员很容易直接形成“博弈的局面”:前者认为投入重构,提高代码的可维护性才能更好地支持业务;而后者觉得开发业务特性才是第一优先级。

其实,业务人员也知道重构的重要性,只是关键问题在于没办法评估重构的收益。所以,接下来我们就一起看看重构对团队能产生哪些收益,以及如何进行度量。

我们说,重构的目的是在不改变软件可观察行为的前提下,重点提高其可理解性,降低其修改成本。因此计算重构收益的方式很简单,从商业的角度来看,收益= 软件价值 - (研发+维护成本)。

如果我们只注重业务上的价值而忽略了软件的研发维护成本,那么长此以往就会来到拐点1。当研发维护成本超出业务价值,收益就开始负增长了。很多企业往往也是到这个拐点才意识到重构的重要性。

通常来说,重构需要一段时间的投入,来慢慢降低研发维护成本。在我过往的咨询项目中,有的需要几个月,有的甚至超过一年。但如果能坚持下来,就会来到拐点3,此时收益开始正向增长。

你可能好奇,重构是怎么有效降低研发维护成本的呢?对此,我们可以从效率和质量两个维度来分析。需要说明一下,因为很难有完美的公式来计算重构的收益,而且实际情况不同,重构的收益可能会有差异,所以我们主要是通过一些具体的场景来看这个问题。

以上只是部分场景,除了效率和质量外,我们在实际开发中还要考虑人员的变化、新人理解代码所需的时间等等。总而言之,重构虽然不直接影响业务的价值,但它会直接影响软件的研发维护成本,从而影响整体的收益。

重构的度量

那么又有新的问题来了,我们如何来度量这些收益呢?如果仅谈收益,没有客观的指标来反映结果,怎么证明最终的结果就是好的呢?

其实对于中小型重构,我们可以观察代码健康度相关的指标变化来度量重构的价值,比如代码的圈复杂度、平均函数行数、类行数等。如果我们能及时对代码中存在的坏味道和问题重构,那么这些指标应该呈现良好的变化趋势。

具体来讲,我们可以借助工具来实时地可视化检测这些指标,例如用Sonar来查看代码的质量情况。

对于大型的重构来说,我们可以通过工程效率上的指标变化来可视化重构的收益。

当然,这些指标的影响因素有很多,但重构最终的目的一定是为了更好地提升产品的质量和研发效率。只有以终为始,我们才能落地好重构。

总结

重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。我根据重构的影响范围,将重构分为小型重构、中型重构以及大型重构这三种类型。

从小型重构到大型重构,虽然产生的价值越来越高,但时间周期和对代码的调整也越来越大。所以我建议小型重构应该及时完成,在任何编码阶段都可以进行;中型重构在添加新功能或者修改Bug时进行设计和重构;大型重构则应该立专项推进。

在实际落地重构工作时,我们首先需要通过代码质量、工程效率让其价值更加可视化。另外还需要对重构进行合理拆分和优先级排序,别让重构变成最后的救命稻草,而应该将它持续纳入到日常的研发迭代中。如果产品的质量、团队的效率跟不上,那么只谈产品和业务无疑是空中楼阁。

对于中小型重构,我们可以通过代码的健康度相关指标变化来度量重构的收益。对于大型的重构,我们可以通过工程效率上的指标变化来度量重构的收益。关于度量,我在第22节课中会专门介绍关键的度量指标以及在实践中如何运用,敬请期待。

思考题

感谢你学完了今天的内容,今天的思考题是这样的:你在项目中落地过重构的工作吗?有遇到什么问题吗?你是如何解决的?

欢迎你在留言区与我交流讨论,也欢迎你把它分享给你的同事或朋友,我们一起来高效、高质量交付软件!

精选留言(8)
  • 郑峰 👍(1) 💬(1)

    实践中以中型重构为主。 小型重构在代码提交时已经完成。 大型重构则需要有计划进行,比如自动化重构工具的构建,和自动化保障规则的构建。

    2023-02-18

  • 白煮迁回 👍(1) 💬(1)

    之前重构项目都面临如何体现价值的问题? 文章中老师说的方重构前就需要对工程指标化,这部分相对容易些. 但对业务价值如何体现比较难? 比如: 1.承接需求开发效率指标无法准确获取.重构的同学一般与开发业务同学是2波人,需求评估工时无法明确重构带来提效价值. 2.重构后质量会有劣化期.复杂业务逻辑修改时引入新问题,反而多做多错. 3.重构专项推动比较难, 一般都是转化为做需求时顺便重构,反而劣化承接需求效率 解决方案 1.统计rd承接需求工时变化,但效果不明显 2.加载自动测试方案+多自测+多review,可以减少 但无法完全避免 3.在业务需求空白期推动/拆成小步迭代

    2023-02-14

  • dulp 👍(0) 💬(1)

    大型重构进行的较少,其次是中型重构。中型重构一般是代码质量不高和业务改动较大同时出现时会进行。之前一直没有思考如何度量重构的收益,这篇文章帮助很大。

    2023-03-11

  • Geek_6061ea 👍(0) 💬(1)

    请问老师,Sonar 是个线上的平台,可以扫描多仓吗?扫描代码有泄露风险,以及需要收费,难以说服公司接入。对于收益和度量的每个点,都有本地扫描工具替代吗?

    2023-02-14

  • zenk 👍(0) 💬(1)

    目前整个系统 1. 缺少分层不分核心业务和运维业务,各种逻辑都在一个函数里面 2. 缺少模块化,同样的逻辑分散 3. 导致理解业务逻辑,老担心是不是还有其他地方的代码没有看 这样的系统该如何一步一步的重构 目前的思路大致是: 1. 最多是组件级别重构 2. 识别那些坏味道,小步改进 3. 模块化的时候,梳理出依赖关系,按照依赖关系,先模块化前置依赖 请教老师 1. 这个步骤靠谱不,求其他建议 2. 另外执行的时候需要和领导沟通具体的工作和厂出,按照上面的度量,领导听了感觉太有说服力,不知这方面该怎么做

    2023-02-14

  • 小虎子11🐯 👍(0) 💬(1)

    干货满满,期待后面的内容

    2023-02-13

  • 蓝啼儿 👍(0) 💬(0)

    实践中以中型重构为主。

    2024-06-29

  • Agei 👍(0) 💬(0)

    大型重构的价值,其实看着都比较具象,但是其实在立项前去盘点价值其实难度很高,就是比如老师讲的bug数量如何减少这个在重构前去盘点有点伪命题,举例来说,之前开发A模块平均产生30个bug,重构完之后产生10个bug,这个其实是个结果,但是不一定重构能解决此问题,老师能直接给个大型重构价值推导的具体例子么?这样看着更有借鉴和学习思路,多谢老师

    2023-05-08