重构



2021年04月03日    Author:Guofei

文章归类: 0xd0_设计模式    文章编号: 1040

版权声明:本文作者是郭飞。转载随意,但需要标明原文链接,并通知本人
原文链接:https://www.guofei.site/2021/04/03/code_refactoring.html


前言

面对重构的两难

  • 面对多年的大型遗留系统,越来越多的需求变更让维护成本越来越高,面对越来越多的竞争,有被市场淘汰的风险
  • 凑合用一下还可以坚持几年,如果不小心改出问题,企业可能立即歇菜

系统重构的前提是“不改变软件的外部行为”,这保证了不会引入bug. 例如,把显示的日期表示从 ‘2021-12-12’ 变成 ‘2021-12-12 00:00:00’,在开发看来不是bug,但在客户看来就是bug

改代码的原因有4种:

  1. 满足客户的新需求
  2. 改bug
  3. 优化性能
  4. 优化内部结构

其中1和2源于客户的功能需求,3源于客户的非功能需求。只有4的价值是隐形的,体现在日后长期维护上。

似乎重构与需求无关,但其实不是这样的

  • 传统是如何添加新功能的:尽量不改变原代码,这样就不会影响以往的功能。后果是源源不断往里面添加程序,时间一长,代码越来越多、越来越乱。
  • “糟糕设计零容忍”策略:接到需求后,分为两步。先重构系统,使其适应要添加的功能;然后添加需求。

等价变换重构。书上给个例子,把一段代码分为多个步骤重构

  • 增加注释(原程序没有注释)
  • 调整顺序
  • 重命名变量(原程序变量名很多事单个字母)
  • 分段(用空行把程序分成不同单元)
  • 把分段后的代码提取成函数

重构时,最好快速迭代,快速发布一个版本。否则,如果重构深入时发现bug,不得不回滚到原始代码,整个重构就失败了。

第一步:分解大函数

具体步骤还是上面的增加注释、调整顺序、重命名变量、分段、提取

例如:

  • 调整顺序(很多大函数在开始定义了一大堆变量,你需要边读边调整,把变量的定义移动到用它们的代码附近)
  • 分段和提取函数:
    • 你不必把一个大函数读完才提取函数。
    • 有些天然的分块,例如条件语句、循环语句,有时候一个分块很多行,就可以抽取成函数
  • 有时候,抽取出来的函数,也很大,那么可以继续分解这个函数。
  • 经过一系列分解,一个大函数变成很多小函数,可读性强了,但是产生了几十个小函数,这些小函数凌乱的堆砌、没有层级,整个代码依然很大。我们在后面的步骤继续重构

常见问题

  • 原代码中的变量可以毫无顾忌的交互数据,但是抽取之后就只能用参数来定义。
  • 一个糟糕的设计:把所有有交互的变量都写到参数里面。一方面,可读性很差也很傻;另一方面,如果代码变更需要增加参数,就是灾难。
  • 一般可以用值对象(Value Object)来传递参数,重构初期可以把变脸都塞进一个值对象。不过后面需要把值对象变成有实际意义的几个。最终的参数就是几个值对象。函数传入的值对象数量不超过6个,最好1-4个

第二步:拆分大对象

操作其实很简单,就是把原对象中的某些方法移动到其他对象中,这个操作叫做 Extract Class,问题的关键是移动到哪个对象。

  • 传统的重构思路是“按照职责做拆分”,每个类单一职责。但这种方法并不奏效。因为开发人员原本不熟悉整个业务的所有细节,而是在开发之后才熟悉。因此一下子想好整个设计是不可能的。
  • 更推荐“小步快跑”,一次想不清楚,就分多次,每次实现一部分。
    • 合久必分:把大对象中不相关的拆分成多个方法类
    • 分久必合:随着开发人员对整个业务更加了解,系统性的审视全局,把分散的方法类合并到业务类中

第三步:提高代码复用

  • 找到重复的代码并不难:
    • 当你开发新功能时,复制第二次就要注意了。
    • 同一个流程的某个环节,例如付款时的付款方式不同,但流程中的大部分应该是可以复用的。
    • 不同业务的相似功能。例如,填写付款单、发票。它们虽然是不同的业务,但都需要效验输入是否合法、检查余额等。
    • 本身相似的功能。例如收款单和付款单、同行评审和非同行评审。
  • 提高代码复用,这才是考验优秀程序员的地方。
    • 如果重复代码在同一个对象中:抽取成方法
    • 如果重复代码在不同对象中:抽取成工具类。例如,获取时间的代码散布在十几个地方,但功能有不完全相同。就应当做一个工具类。
    • 重复代码在不同对象中:另一种方法是抽取成实体类。工具类仅仅是一堆方法的集合,而实体类有一定的业务逻辑。
    • 如果代码所在的类有并列关系:抽取父类。例如,正常开票与非正常开票这两个类,都有 valid, save 方法,因此抽取出一个开票类
    • 其他。提高复用的方法还有很多,各有使用场景。体现了对优秀程序员的考验。

第四步:发现程序可扩展点

是预先做可扩展性,还是需求变更时增加扩展性,这是个两难的难题,有一些一般原则

  • 预先的可扩展性设计不要太多
  • 更常见的可扩展性来自需求变更。

第五步:降低程序的依赖

  • 接口、实现:工厂模式
  • 与外部系统解耦:外部接口和适配器模式.我们想调用一个服务的3.0版本,而不是原先的2.0版本,发现类名、方法名,甚至包名都变了,代码里到处都是对2.0版本的各种引用,像老树一样盘根错节。根本原因是耦合太强。这就适合用适配器模式改造。
  • 继承泛滥:桥接模式
  • 方法的解耦:策略模式
  • 过程的解耦:命令模式

(这里只提一下,详细见于另一篇文章 【Python】设计模式

第六步:分层

参考文献

《大话重构》,范钢,人民邮电出版社。


您的支持将鼓励我继续创作!