重构改善既有代码设计

 

目录

一、什么是重构

二、重构的目的和时机

2.1 目的

2.1.1 改进软件的设计

2.2.2 使软件更容易理解

2.2.3 帮助找到 BUG

2.2.4 提高编程速度

2.2 重构的时机

2.3 重构的难题

2.3.1 数据库

2.3.2 修改接口

2.3.3 难以通过重构首发完成的设计改动

2.3.4 何时不该重构

三、“坏”代码

四、重构列表

五、重新组织函数

六、对象之间搬移特性

七、重新组织数据源

八、简化条件表达式

九、简化函数调用

十、处理概括关系

十一、大型重构

阿里开发手册


一、什么是重构

如书的序言所说,重构是这样一个过程:再不改变外部代码的前提下,对代码做出修改,改进程序的内部结构,重构是一种经过千锤百炼形成的有条不紊的程序整理方法,可以最大程度减少整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。

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

二、重构的目的和时机

2.1 目的

重构不是包治百病的灵丹妙药,也绝对不是所谓的“银弹”。重构只是一种工具,能够帮助你始终良好的控制代码而已。使用它,可能基于下面的几个目的。

这里有一个有意思的科普(引用自百度百科: 没有银弹
):在民俗传说里,所有能让我们充满梦靥的怪物之中,没有比狼人更可怕的了,因为它们会突然地从一般人变身为恐怖的怪兽,因此人们尝试着查找能够奇迹似地将狼人一枪毙命的银弹。我们熟悉的软件项目也有类似的特质(以一个不懂技术的管理者角度来看),平常看似单纯而率直,但很可能一转眼就变成一只时程延误、预算超支、产品充满瑕疵的怪兽,所以,我们听到了绝望的呼唤,渴望有一种银弹,能够有效降低软件开发的成本,就跟电脑硬件成本能快速下降一样。

2.1.1 改进软件的设计

当人们只为短期目的而修改代码时,他们经常没有完全理解架构的整体设计。于是代码逐渐失去了自己的结构。程序员越来越难以通过阅读代码来理解原来的设计。代码结构的流失有累积效应。越难看出代码所代表的设计企图,就越难以保护其设计,于是设计就腐败得越快。

完成同样一件事,设计欠佳的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事情,因此改进设计的一个重要方向就是消除重复代码。消除重复代码,我就可以确定所有事物和行为在代码中只表述一次,这正是优秀设计的根本。

2.2.2 使软件更容易理解

所谓程序设计,很大程度上就是与计算机对话:我编写代码告诉计算机做什么,而它的响应是按照我的指示精确行动。一言以蔽之,我所做的就是填补“我想要它做什么”和“我告诉它做什么”之间的缝隙。编程的核心就在于“准确说出我想要的”。

然而别忘了,除计算机之外,源码还有其他读者,并且很大概率还是几个月后的自己,如何更清晰地表达我想要做的,这可能就需要一些重构的手法。

这里我联想到了软件设计的 KISS 原则:KISS 原则, Keep It Simple and Stupid ,简单的理解这句话就是,要把一个系统做的连白痴都会用。

2.2.3 帮助找到 BUG

对代码的理解,可以帮助找到系统中存在的一些 BUG。搞清楚程序结构的同时,也可以对自己的假设做一些验证,这样一来 BUG 想不发现都难。

Kent Beck 经常形容自己的一句话是:“我不是一个特别好的程序员,我只是一个有着一些特别好的习惯的还不错的的程序员。”重构能够帮助我们更有效地写出健壮的代码。

2.2.4 提高编程速度

听起来可能有些反直觉,因为重构可能会花大量的时间改善设计、提高阅读性、修改 BUG,难道不是在降低开发速度嘛?

软件开发者交谈时的故事:一开始他们进展很快,但如今想要添加一个新功能需要的时间就要长得多。他们需要花越来越多的时间去考虑如何把新功能塞进现有的代码库,不断蹦出来的bug修复起来也越来越慢。代码库看起来就像补丁摞补丁,需要细致的考古工作才能弄明白整个系统是如何工作的。这份负担不断拖慢新增功能的速度,到最后程序员恨不得从头开始重写整个系统。

下面这幅图可以描绘他们经历的困境。

重构改善既有代码设计_第1张图片

但有些团队的境遇则截然不同。他们添加新功能的速度越来越快,因为他们能利用已有的功能,基于已有的功能快速构建新功能。

两种团队的区别就在于软件的内部质量。需要添加新功能时,内部质量良好的软件让我可以很容易找到在哪里修改、如何修改。良好的模块划分使我只需要理解代码库的一小部分,就可以做出修改。如果代码很清晰,我引入 BUG 的可能性就会变小,即使引入了 BUG,调试也会容易得多。理想情况下,代码库会逐步演化成一个平台,在其上可以很容易地构造与其领域相关的新功能。

这种现象被作者称为“设计耐久性假说”:通过投入精力改善内部设计,我们增加了软件的耐久性,从而可以更长时间地保持开发的快速。目前还无法科学地证明这个理论,所以说它是一个“假说”。

20年前,行业的陈规认为:良好的设计必须在开始编程之前完成,因为一旦开始编写代码,设计就只会逐渐腐败。重构改变了这个图景。现在我们可以改善已有代码的设计,因此我们可以先做一个设计,然后不断改善它,哪怕程序本身的功能也在不断发生着变化。由于预先做出良好的设计非常困难,想要既体面又快速地开发功能,重构必不可少。

2.2 重构的时机

  • 添加功能时重构
  • 修补错误时重构
  • 复审代码时重构

2.3 重构的难题

2.3.1 数据库

原因一:程序和数据库关联过于紧密耦合

原因二:数据迁移

解决:再对象模型和数据库模型之间加入一个分隔层,可以隔离两个模型各自的变化。

2.3.2 修改接口

当你要修改某个函数名时,留下旧函数,让他调用新函数,而不是赋值函数实现。

2.3.3 难以通过重构首发完成的设计改动

2.3.4 何时不该重构

重构并不是必要,当然也有一些不那么需要重构的情况:

  • 不需要修改,那些丑陋的代码能隐藏在一个 API 之下。 只有当我需要理解其工作原理时,对其进行重构才会有价值;
  • 重写比重构容易。 这可能就需要良好的判断力和丰富的经验才能够进行抉择了。

三、“坏”代码

可以好好看下《阿里巴巴开发手册》

  • 神秘命名
  • 重复代码
  • 过长函数
  • 过长参数列表
  • 全局数据: 全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。一次又一次,全局数据造成了一些诡异的 BUG,而问题的根源却在遥远的别处。
  • 可变数据: 对数据的修改经常导致出乎意料的结果和难以发现的 BUG。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据。
  • 发散式变化: 模块经常因为不同的原因在不同的方向上发生变化。
  • 散弹式修改: 每遇到某种变化,你都必须在许多不同的类内做出许多小修改。
  • 依恋情结: 所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流。
  • 数据泥团: 你经常在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。
  • 基本类型偏执: 很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。
  • 重复的 switch: 在不同的地方反复使用相同的 switch 逻辑。问题在于:每当你想增加一个选择分支时,必须找到所有的 switch,并逐一更新。考虑利用多态替换他。
  • 循环语句: 我们发现,管道操作(如 filter 和 map)可以帮助我们更快地看清被处理的元素一级处理它们的动作。
  • 冗余的元素
  • 夸夸其谈通用性: 函数或类的唯一用户是测试用例。
  • 临时字段: 有时你会看到这样的类:其内部某个字段仅为某种特定情况而定。这样的代码让人不理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。
  • 过长的消息链
  • 中间人: 过度运用委托。
  • 内幕交易: 软件开发者喜欢在模块之间筑起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。
  • 过大的类
  • 异曲同工的类
  • 纯数据类: 所谓纯数据类是指:他们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。
  • 被拒绝的遗赠: 拒绝继承超类的实现,我们不介意:但如果拒绝支持超类的接口,这就难以接受了。
  • 注释: 当你感觉需要纂写注释时,请先尝试重构,试着让所有注释都变得多余。

四、重构列表

  1. 重构的记录格式
  2. 寻找应用点
  3. 重构手法:小步前进、频繁测试

五、重新组织函数

  • 提炼函数重构改善既有代码设计_第2张图片
  • 内联函数重构改善既有代码设计_第3张图片
  • 内联临时变量重构改善既有代码设计_第4张图片
  • 以查询取代临时变量重构改善既有代码设计_第5张图片
  • 引入解释性变量重构改善既有代码设计_第6张图片
  • 分解临时变量重构改善既有代码设计_第7张图片
  • 移除对参数的赋值重构改善既有代码设计_第8张图片
  • 以函数对象取代函数重构改善既有代码设计_第9张图片
  • 替换算法重构改善既有代码设计_第10张图片

六、对象之间搬移特性

  • 搬移函数:再函数最常应用的类建立一个有着类似行为的新函数,将旧函数变成一个单纯的委托函数或是将旧函数完全移除。
  • 搬移字段:再目标类新建一个字段,修改字段的所有用户,令他们改用新字段
    重构改善既有代码设计_第11张图片
  • 提炼类:建立一个新类,将相关的字段和函数从旧类搬移到新类。
  • 类内联化:将这个类的所有特性搬移到另一个类中,然后移除原类。
  • 隐藏“委托关系”:再服务类上建立客户所需的所有函数,用以隐藏委托关系。
    重构改善既有代码设计_第12张图片
  • 移除中间人:让客户直接调用受托类。
    重构改善既有代码设计_第13张图片
  • 引入外加函数:在客户类建立一个函数,并以第一参数形式传入一个服务类实例。
    重构改善既有代码设计_第14张图片
  • 引入本地扩展:简历一个新类,使它包含这些额外函数。让这个扩展屏成为源类的子类或包装类。
    重构改善既有代码设计_第15张图片

七、重新组织数据源

  • 自封装字段:为这个字段简历取值/设置函数,并且只以这些函数来访问字段。
    重构改善既有代码设计_第16张图片
  • 以对象取代数据值:将数据变成对象。
  • 将值对象改为引用对象。
    重构改善既有代码设计_第17张图片
  • 将应用对象改为值对象。
    重构改善既有代码设计_第18张图片
  • 以对象取代数组:以对象取代数组,对于数组中的每个元素,以一个字段来表示。
  • 复制“被监视数据”:将数据复制到一个领域对象中,简历一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
  • 单向关联改为双向关联:添加一个反向指针,并使修改函数能够同时更新两条连接。
    重构改善既有代码设计_第19张图片
  • 字面常量取代魔法数:创造一个常量,根据其意义为他命名,并将上述的字面数值替换为这个常量。
    重构改善既有代码设计_第20张图片
  • 封装字段:私有化,private,并且提供相应的访问函数。

     
  • 封装集合:让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
    重构改善既有代码设计_第21张图片
  • 以数据类型取代记录:为该记录创建一个数据对象
  • 以类取代类型码。
  • 以子类取代类型码。
    重构改善既有代码设计_第22张图片

八、简化条件表达式

  • 分解条件表达式:从if、then。else三个段落中分别提炼出独立函数
    重构改善既有代码设计_第23张图片
  • 合并条件表达式:将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
    重构改善既有代码设计_第24张图片
  • 合并重复的条件片段:将这些重复代码搬移到条件表达式之外。
  • 移除控制标记:以break或return取代控制标记。
  • 以卫语句取代嵌套条件表达式。
  • 以多态取代条件表达式:将这些条件表达式的每个分支放进一个子类内的腹泻函数中,将原始函数沈明伟抽象函数。
    重构改善既有代码设计_第25张图片
  • 引入断言:
    重构改善既有代码设计_第26张图片

九、简化函数调用

  • 函数改名:修改函数名称
  • 添加参数:为函数添加一个对象参数,让该对象带进函数所需信息。
    重构改善既有代码设计_第27张图片
  • 移除参数:移除函数参数。
  • 将查询函数和修改函数分离:建立两个不同函数,一个负责查询,一个负责修改。
    重构改善既有代码设计_第28张图片
  • 函数携带参数:建立单一函数,以参数表达不同的值、
  • 明确函数取代参数:针对参数每一个可能值,简历一个独立函数。
  • 保持对象完整:传递整个对象
    重构改善既有代码设计_第29张图片
  • 引入参数对象:以对象取代参数。
  • 工厂函数取代构造函数。
  • 以异常取代错误码。
    重构改善既有代码设计_第30张图片
  • 以测试取代异常:修改调用者,使它在调用函数之前先做检查。
    重构改善既有代码设计_第31张图片

十、处理概括关系

  • 字段上移
    重构改善既有代码设计_第32张图片
  • 函数上移
    重构改善既有代码设计_第33张图片
  • 构造函数本体上移:在超类中新建一个构造函数,并在子类构造函数中调用它。
    重构改善既有代码设计_第34张图片
  • 函数下移
    重构改善既有代码设计_第35张图片
  • 字段下移
    重构改善既有代码设计_第36张图片
  • 提炼子类:新建一个子类,将上面所说的一部分特性移到子类中。
    重构改善既有代码设计_第37张图片
  • 提炼超类
  • 提炼接口:相同的子集提炼到一个独立的接口中
    重构改善既有代码设计_第38张图片
  • 折叠继承体系
    重构改善既有代码设计_第39张图片
  • 塑造模板函数:将这些操作分别放进独立函数中,并且保持他们都有相同的签名,于是原函数也就变得相同了,然后将原函数上移至超类。
  • 委托取代继承:
    重构改善既有代码设计_第40张图片
  • 继承取代委托:
    重构改善既有代码设计_第41张图片

十一、大型重构

  • 梳理并分解继承体系:建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
  • 讲过程化设计转换为对象设计:将数据变成对象,将大块的行为分成小块,并将行为移入相关对象之中。
  • 将领域和表述/显示分离:将领域逻辑分离出来,为他们简历独立的领域类。
  • 提炼继承体系:简历继承体系,易一阁子类标识一个特殊情况

阿里开发手册

  • 各层命名规约
    • 获取单个对象的方法用 get 做前缀。
    • 获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects。 
    • 获取统计值的方法用 count 做前缀。 
    • 插入的方法用 save/insert 做前缀。
    • 删除的方法用 remove/delete 做前缀。
    • 修改的方法用 update 做前缀
  • 领域模型命名规约
    • 数据对象:xxxDO,xxx 即为数据表名。
    • 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
    • 展示对象:xxxVO,xxx 一般为网页名称。
    • POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。
  • 方法参数在定义和传入时,多个参数逗号后面必须加空格。正例:下例中实参的 args1,后边必须要有一个空格。
    method(args1, args2, args3);
  • 所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
    说明:对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生, 会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
  • 任何货币金额,均以最小货币单位且整型类型来进行存储。
  • 如上所示 BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法。 说明: equals()方法会比较值和精度 (1.0 与 1.00 返回结果为 false) ,而 compareTo()则会忽略精度。
  • 定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性 默认值
  • 序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
  • 关于 hashCode 和 equals 的处理,遵循如下规则:
    1) 只要覆写 equals,就必须覆写 hashCode。
    2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。
    3) 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
    说明:String 因为覆写了 hashCode 和 equals 方法,所以可以愉快地将 String 对象作为 key 来使用。
  • 判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式。
    说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。
  • 使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
    说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法
  • 错误码设计规范:
    错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。
    说明:错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100
  • 日志打印时禁止直接用 JSON 工具将对象转换成 String。用toString()
  • 数据库表设计规范:

    数据库名、表名、字段名,都不允许出现任何大写字母。
    数据库名成:tps***(业务名),例:tps_tiku。
    数据库表名:业务名_类型名_实体名,例:tk_td_paper
    数据库表类型
    tb:基础信息表,数据基本不增长
    td:业务产生数据表,数据会随着用户操作增长很快
    tr :业务关系表,实体建的关联关系表
    vb:基础表之间的视图
    vd:数据表之间的视图
    vr:关系表之间的视图
    表和字段一定要有注释
    提供表间逻辑关系。
    提供表结构设计文档。
    普通索引命名 idx_字段名_字段名,唯一索引 uniq_字段名_字段名

  • 利用延迟关联或者子查询优化超多分页场景。
    说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当
    offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL
    改写。
  • 分层领域模型规约:
    DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。 
    DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。 
    BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。 
    Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。 
    VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
  • 高并发服务器建议调小 TCP 协议的 time_wait 超时时间
    操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服务器端会因为处于 time_wait 的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值。
  • 调大服务器所支持的最大文件句柄数(File Descriptor,简写为 fd)
    主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd。主流的linux服务器默认所支持最大fd数量为1024,当并发连接数很大时很容易因为fd不足而出现“open too many files”错误,导致新的连接无法建立。建议将linux 服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)。
  • 在线上生产环境,JVM 的 Xms 和 Xmx 设置一样大小的内存容量,避免在 GC 后调整堆大小带来的压力

 

 

 

 

 

 

你可能感兴趣的:(java)