《重构-改善既有代码的设计》学习笔记 (一)
关于《重构-改善既有代码的设计》第一版的学习笔记。虽然是二十年前的老书了,但是由于我不想看js案例,所以还是选择了第一版进行阅读。本节主要是重构的简介与代码的bad smell介绍。
重构,第一个案例
重构即为不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
本章节以一个影片出租店用程序作为案例示范了重构的过程。主要部分没有什么特别的,值得注意的有以下两处
- 某处重构中不使用临时变量储存函数值而改用两次调用外部函数。代码看起来更可读不过似乎影响效率,之后大概会解释重构与效率的取舍吧。
- 更改代码结构加入状态模式。不同影片的价格计算方式不同,但是由于类型可修改,不能简单使用继承。于是采用状态模式。此处影片的类型为可修改的状态,获取影片价格为状态对应的行为,虽然我看着更像策略模式…
重构之前,首先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力
重构技术就是以微小的步伐修改程序,如果你犯下错误,很容易便可发现它
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
重构原则
重构: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 或是使用一系列重构手法,在不改变软件可观察行为的前提下调整其结构。
为何重构
- 重构改进软件设计
- 重构使软件更容易理解
- 重构帮助找到bug
- 重构提高编程速度
何时重构
事不过三,三则重构
- 添加功能时重构
- 修补错误时重构
- 复审代码时重构
重构的难题
数据库
- 大多数商用程序都与数据库高度耦合
- 容易导致数据迁移
- 对于非对象数据库,可以通过加入分隔层隔离两个模型各自的变化。这会增加系统复杂度但是可以带来很大的灵活度。
- 无需一开始就插入分隔层,可以等对象模型不稳定时再产生。
修改接口
对于已发布接口,可能会出现比公开接口更严重的问题。
这时需要同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应。让旧接口调用新接口,而非复制函数实现。对于java,可以使用 deprecation (意为不建议使用) 标记旧接口。
另一方面,若无必要,不要发布接口。可能意味着需要改变代码所有权观念,让每个人都可以修改别人的代码,以适应接口的改动。
不要过早发布接口,请修改你的代码所有权政策,使重构更顺畅。
难以通过重构手法完成的设计改动
很难将不考虑安全性需求时构造起来的系统重构为具备良好安全性系统
何时不该重构
- 当代码实在太混乱,重构它还不如重新写一个来得简单。
- 当代码满是错误。
- 如果项目已近最后期限
重构与性能
重构可能使软件运行更慢,但它也使软件的优化更容易。除了对性能有要求的实时系统,其他任何情况下都应写出可调的软件,然后调整它以求获得足够速度。
编写快速软件的方法
- 时间预算法
从分解设计开始做好时间预算,适用于心律调节器等高度重视性能的系统 - 持续关注法
要求程序员做任何事时设法保持高性能 - 性能提升法
不关注性能直到性能优化阶段(通常在开发后期)
代码的坏味道
Duplicated Code (重复代码)
同一个类的两个函数含有相同的表达式
采用 Extract Method 提炼出重复代码,然后让这两个地点都调用被提炼出来的那一段代码
同一个互为兄弟的子类内出现
- 如果含有相同表达式。对两个类使用 Extract Method ,再对被提炼出来的代码使用 Pull Up Method ,将它推入超类中。
- 如果两个代码只是相似,运用 Extract Method 将相似部分和差异部分割开,构成单独一个函数。然后可能可以运用 Form Template Method 获得模版方法设计模式。
- 如果以不同的算法做相同的事,选择较清晰的一个,用 Substitute Algorithm 将其他函数的算法替换掉
两个毫不相关的类
对其中一个使用 Extra Class,将重复代码提炼到一个独立类中。注意考虑原有类与新类的关系。
Long Method (过长函数)
- 每当需要使用注释说明时,便将需要说明的东西写进独立函数中,并以其用途命名。关键不在于函数长度,而在于函数 “What” 和 “How” 之间的距离。
- 大多情况只需要 Extract Method。
- 如果有大量参数和临时变量。运用 Replace Temp with Query 来消除临时元素。Introduce Parameter Object 和 Preserve Whole Object 则可以简化参数列表
- 如果仍有太多临时变量和参数,使用 Replace Method with Method Object
- 注意条件表达式和循环。可以使用 Decompose Conditional 处理条件表达式。循环和其内代码则应提炼到独立函数中
Large Class (过大的类)
- 如果类中有太多实例变量,通常可以使用 Extract Class 或 Extract Subclass
- 如果类中有太多代码,还可以使用 Extract Interface 为客户端对于类的每一种使用方式提炼出一个接口
- 如果是一个GUI类,可能需要把数据和行为移到一个独立的领域对象去。可能需要各保留一些重复数据,并保持两边同步。可以使用 Duplicate Observed Data
Long Parameter List (过长参数列)
- 如果向已有的对象发出一条请求就可以取代一个参数,那么你应该激活重构手法 Replace Parameter with Method 。
- 还可以用 Preserve Whole Object 将来自同一对象的一堆数据收集起来,并以该对象替换他们。
- 如果某些数据缺乏合理的对象归属,可使用 Introduce Parameter Object为它们制造出一个参数对象。
- 例外情况,如果明显不希望产生某种依赖关系,将数据单独拆分成参数也合情合理。
Divergent Change (发散式变化)
如果某个类经常因为不同的原因在不同的方向上变化,应该找出某种特定原因而造成的所有变化,然后运用 Extract Class 将它们提炼到另一个类中
Shotgun Surgery (霰弹式修改)
与上一条相反,如果每遇到一种变化,你都必须在许多不同类中作出小修改,应该使用 Move Method 和 Move Field 把需要修改的代码放进同一个类。通常可以运用 Inline Class,这可能会导致少量的上一条中的发散式变化,但可以轻易地被处理
Feature Envy (依恋情节)
函数对某个类的兴趣高过自己所处的类。
- 通常关注的焦点是某个其他的类中的某些数据。通常使用 Move Method 即可
- 有时函数会用到几个类的功能。这时可以判断哪个类拥有最多被此函数使用的数据,也可以先以 Extract Method 将这个函数分解并放在不同地点
- 也有几个复杂的设计模式破坏了这一条。例如Strategy策略模式和Visitor访问者模式和自委托(啥玩意)。这些模式用于对抗前面的发散式变化。
Data Clumps (数据泥团)
在很多地方看到相同的三四项数据,例如两个类中相同的字段或是许多函数签名中相同的参数。
- 首先找出数据以字段形式出现的地方,运用 Extract Class 将它们提炼到独立的对象中,然后运用 Introduce Prarameter Object 或 Preserve Whole Object 缩减参数列表。
- 一个评判方法 删掉众多数据中的一项,如果有数据失去类意义,意味着需要产生新对象。
Primitive Obsession (基本类型偏执)
新手通常不愿意在小任务上运用小对象。
- 可以运用 Repalce Data Value with Object 将原本单独存在的数据值替换为对象
- 如果想要替换的数据值是类型码而它不影响行为。可以用 Replace Type Code with Class 将它替换掉。
- 如果有与类型码相关的条件表达式。可以用Replace Type Code with Subclass 或 Replace Type Code with State/Strategy 加以处理
- 如果有一组总被放在一起的字段,可运用 Extract Class
- 如果在参数列表中看到基本类型数据,可运用 Introduce Parameter Object
- 如果正在从数组中挑选数据,可运用 Replace Array with Object
Switch Statements ( switch 惊悚现身 )
- 一般考虑使用多态替换它
- 如果只是在单一函数中有些选择事例,可以用Replace Parameter with Explicit Methods
- 如果选择条件之一是 null ,可以使用 Introduce Null Object
Parallel Inheritance Hierachies (平行继承体系)
每当你为某个类新增一个子类时,如果你必须为另一个类也增加一个类。
一般让一个继承体系的实例引用另一个继承体系的实例。也可以使用 Move Methods 和 Move Field
Lazy Class (冗赘类)
去除无用的类。
- 如果某些子类没有做足够的工作,使用 Collapse Hierachy
- 对于几乎没用的组件,使用 Inline Class
Speculative Generality (夸夸其谈未来性)
过度设想未来以企图处理一些非必要的事情。例如函数或类的唯一用户是测试用例。
- 无用的抽象类,请运用 Collapse Hierarchy
- 不必要的委托,运用 Inline Class
- 函数有无用参数,实施 Remove Parameter
- 函数名带有多余的抽象,实施 Rename Method
Temporary Field (令人迷惑的暂时字段)
- 有时会看到某个实例变量仅为某种特定情况而设。
一般可以使用 Extract class,也许可以使用 Introduce Null Object 在变量不合法的情况下创建一个空对象,从而避免条件式代码 - 如果类中有一个复杂算法,需要好几个变量,也有可能出现这种情况。
可以利用 Extract Class 提炼到独立类中,成为新的函数对象
Message Chains (过度耦合的消息链)
对象之间相互请求的链过长。意味着客户代码将与查找过程中的导航紧密耦合。
可以使用 Hide Delegate 。或是先观察消息链的最终对象,用 Extract Method 提炼到独立函数中,再使用 Move Method 推入消息链。
Middle Man (中间人)
过度使用委托。
- 如果只有少数几个不干实事的函数,使用 InlineMethod 把它们放进调用端
- 如果还有其他行为,运用 Replace Delegation with Inheritance 变为实责对象的子类
Inappropriate Intimacy (狎昵关系)
两个类过多关注彼此的私有部分。
- 可以采用 Move Method 和 Move Field 划清界限
- 可以运用 Extra Class 将共同点提炼到一个新的类
- 可以运用 Hide Delegate 传递这些信息
- 继承也会导致这一问题。可以使用 Replace Inheritance with Delegation 使其离开继承体系
Alternative Classes with Different Interfaces (异曲同工的类)
两个类做同一件事却有不同的签名。
- 可以运用 Rename Method 重命名。
- 还需要反复运用 Move Method 将某些行为移入类,直到两者的协议一致为止
- 如果移动方法过于重复累赘,可以运用 Extract Superclass
Incomplete Library Class (不完美的库类)
关于程序库的一些问题。
- 如果你只想修改库类的一两个函数,可以运用 Introduce Foreign Method
- 如果想要添加大量额外行为,运用 Introduce Local Extension
Data Class (纯稚的数据类)
一些纯粹的数据类,需要考虑是否得到了恰当的封装。尝试将其他地方对该类的调用搬移过来。
Refused Bequest (被拒绝的馈赠)
子类可能不需要继承超类所有的东西。
一般而言意味着继承体系设计错误。传统说法中,所有的超类都应该是抽象的
当然这一问题并不经常引起困惑和问题,大多数情况无需在意
Comments (过多的注释)
当感觉需要撰写注释时,先尝试重构,试着让所有注释变得多余
构筑测试体系
确保所有测试都完全自动化,让它们检查自己的测试结果
需要添加特性时,先写相应测试代码。作者的喜好是在实践测试时先通过在断言中填写错误期望值或通过修改代码让测试不通过,以验证测试代码能正常工作。
每当你收到bug报告,请先写一个单元测试来暴露bug
本章的案例使用了20年前的JUnit版本,不适用于现今的测试体系,不多赘述。