一、重构的定义
n. 重构 : 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
v. 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
二、重构的前提?如何理解?如何保障?重构的两顶帽子是什么?
前提:不改变软件可观测行为
理解:可观测行为是指使用软件所产生的结果。也就是说重构不能改变原代码的逻辑结果。
保障:增量式重构=自动化测试+持续集成+TDD驱动重构
两顶帽子:添加新功能和重构
二、需要重构的情况
- 神秘命名
- 重复代码
- 过长函数
- 过长参数列表
- 全局数据
- 可变数据
- 发散式变化
- 雾弹式修改
- 依恋情结
- 数据泥团
- 基本类型偏执
- 重复的switch
- 循环语句
- 冗赘的元素
- 夸夸其谈通用性
- 临时字段
- 过长的消息链
- 中间人
- 内幕交易
- 过大的类
- 异曲同工的类
- 纯数据类
- 被拒绝的遗赠
- 注释
三、如何重构
第一:第一组重构
- 提炼函数
- 内联函数
- 提炼变量
- 内联变量
- 改变函数声明
- 封装变量
- 变量改名
- 引入参数对象
- 函数组合成类
- 函数组合变换
- 拆分阶段
第二:封装
- 封装记录
- 封装集合
- 以对象取代基本类型
- 以查询取代临时变量
- 提炼类
- 内联类
- 隐藏委托关系
- 移除中间人
- 替换算法
第三:搬移特性
- 搬移函数
- 搬移字段
- 搬移语句到函数
- 搬移语句到调用者
- 以函数调用取代内联代码
- 移动语句
- 拆分循环
- 以管道取代循环
- 移除死代码
第四:重新组织数据
- 拆分变量
- 字段改名
- 以查询取代派生变量
- 以引用对象改为值对象
- 将值对象改为引用对象
第五:简化条件逻辑
- 分解条件表达式
- 合并条件表达式
- 以卫语句嵌套条件表达式
- 以多态取代条件表达式
- 引入特列
- 引入断言
第六:重构Api
- 将查询函数和修改函数分离
- 函数参数化
- 移除标记函数
- 保持对象完整性
- 以查询取代参数
- 以参数取代查询
- 移除设值函数
- 以工厂函数取代构造函数
- 以命令取代函数
- 以函数取代命令
第七:处理继承关系
- 函数上移
- 将一个方法从子类移到父类中,以便多个子类可以共享该方法的实现
class ParentComponent {
// 父类中定义共享的方法
sharedMethod() {
// 共享的方法实现
}
}
class ChildComponent extends ParentComponent {
// 子类不再需要实现 sharedMethod
// ...
}
上述代码中,我们定义了一个父类 ParentComponent,其中包含了一个共享的方法 sharedMethod()。然后我们创建了一个子类 ChildComponent,它继承自 ParentComponent,并且不需要再实现 sharedMethod(),因为该方法已经在父类中定义了。
通过函数上移,sharedMethod() 方法从子类中移到了父类中,这样其他继承自 ParentComponent 的子类也能够共享该方法的实现。这样可以避免在每个子类中重复实现相同的方法,提高代码的可维护性和重用性。
- 字段下移
- 一种处理继承关系的重构手法,用于将一个方法从父类移动到子类中,以避免让不需要使用该方法的子类也继承该方法
class ParentComponent {
// 父类包含的一些共享的方法和属性
}
class ChildComponent extends ParentComponent {
specificMethod() {
// 子类特定的方法实现
}
// 将不再需要的方法下移到子类中
obsoleteMethod() {
// 不再需要的方法实现
}
}
上述代码中,我们有一个父类 ParentComponent,其中包含了一些共享的方法和属性。然后我们创建了一个子类 ChildComponent,它继承自 ParentComponent。在子类中,我们定义了一个特定的方法 specificMethod(),它是子类独有的方法。
接着,我们将不再需要的方法 obsoleteMethod() 从父类中下移到了子类中。由于某些子类不需要使用这个方法,将其下移到子类中可以避免不必要的继承和冗余。这样,在使用 ChildComponent 的实例时,只能访问到子类中定义的方法,而无法访问到父类中下移的方法。
通过函数下移,我们可以根据具体需求将不再需要的方法下移到子类中,避免让不需要使用的子类也继承这些方法。这样可以提高代码的可读性和可维护性,减少继承层次和冗余代码。
- 构造函数本体上移
- 是一种处理继承关系的重构手法,用于将子类中的构造函数本体移到父类中。这样做可以避免子类中重复的初始化逻辑,并且确保所有子类都遵循同样的初始化流程
class ParentComponent {
constructor() {
// 父类的构造函数本体逻辑
}
}
class ChildComponent extends ParentComponent {
constructor() {
super(); // 调用父类的构造函数
// 子类特有的构造函数本体逻辑
}
}
上述代码中,我们定义了一个父类 ParentComponent,它有一个构造函数,在构造函数本体中包含父类特有的初始化逻辑。然后我们创建了一个子类 ChildComponent,它继承自 ParentComponent。在子类的构造函数中,我们首先通过 super() 调用父类的构造函数,以确保父类的初始化逻辑得到执行。然后在子类的构造函数本体中,可以添加子类特有的初始化逻辑。
通过构造函数本体上移,可以避免子类中重复的构造函数逻辑,并让所有子类都遵循相同的初始化流程。这样可以提高代码的重用性、可维护性和一致性。
- 函数下移
- 是一种处理继承关系的重构手法,用于将一个方法从父类移动到子类中。这样做可以确保子类具备特定的行为,并避免不需要使用该方法的子类也继承了该方法
- 字段下移
class ParentComponent {
sharedMethod() {
// 父类共享的方法实现
}
}
class ChildComponent extends ParentComponent {
specificMethod() {
// 子类特定的方法实现
}
}
上述代码中,我们有一个父类 ParentComponent,其中包含了父类共享的方法 sharedMethod()。然后我们创建了一个子类 ChildComponent,它继承自 ParentComponent。在子类中,我们定义了一个特定的方法 specificMethod(),它是子类独有的方法。
然后,如果我们发现 sharedMethod() 这个方法只有在子类中会被使用,而其他子类不需要使用它,就可以进行函数下移,将该方法从父类中移到子类中:
class ParentComponent {
// 父类包含的一些共享的方法和属性
}
class ChildComponent extends ParentComponent {
specificMethod() {
// 子类特定的方法实现
}
// 将不再需要的方法下移到子类中
sharedMethod() {
// 不再需要的方法实现
}
}
通过函数下移,我们可以根据具体需求将不再需要的方法下移到子类中,避免让不需要使用的子类也继承这些方法。这样可以提高代码的可读性和可维护性,减少继承层次和冗余代码。
- 以子类取代类型码
- 是一种处理对象类型区分的重构手法,通过将一个类型码作为类的属性替换为具体的子类,从而提高代码的可扩展性和可维护性。
class Shape {
constructor(type) {
this.type = type;
}
draw() {
switch (this.type) {
case 'circle':
// 绘制圆形的逻辑
break;
case 'rectangle':
// 绘制矩形的逻辑
break;
case 'triangle':
// 绘制三角形的逻辑
break;
// 更多类型的逻辑...
}
}
}
上述代码中,我们有一个 Shape 类,它带有一个 type 属性用于表示不同的形状。然后,在 draw() 方法中,使用 switch 语句根据 type 属性执行相应的绘制逻辑。
现在,如果我们发现代码中频繁使用了 switch 语句来根据类型码进行逻辑分支,这就是一个反模式,可以使用以子类取代类型码来重构代码:
class Shape {
draw() {
// 抽象方法,每个子类必须实现自己的绘制逻辑
}
}
class Circle extends Shape {
draw() {
// 绘制圆形的逻辑
}
}
class Rectangle extends Shape {
draw() {
// 绘制矩形的逻辑
}
}
class Triangle extends Shape {
draw() {
// 绘制三角形的逻辑
}
}
// 在使用时,创建具体的子类实例,而不是通过类型码区分
const circle = new Circle();
circle.draw();
const rectangle = new Rectangle();
rectangle.draw();
const triangle = new Triangle();
triangle.draw();
上述代码中,我们将 Shape 类修改为抽象基类,并创建了具体的子类 Circle、Rectangle 和 Triangle 来表示不同的形状。每个子类都必须实现自己的绘制逻辑。这样,我们就可以根据具体的子类来调用 draw() 方法,而不再需要使用类型码进行逻辑分支判断。
通过以子类取代类型码重构,可以提高代码的可扩展性和可维护性。当需要添加新的形状时,只需要创建一个新的子类即可,不需要修改原有的代码逻辑。同时,代码也更加清晰和易懂,减少了条件分支的复杂度。
- 移除子类
- 有时候我们可能会发现某些子类不再需要存在,或者某些子类的行为与父类完全一致,这时可以考虑移除这些子类
class Shape {
draw() {
// 绘制形状的逻辑
}
}
class Circle extends Shape {
draw() {
// 绘制圆形的逻辑
}
}
class Rectangle extends Shape {
draw() {
// 绘制矩形的逻辑
}
}
class Triangle extends Shape {
draw() {
// 绘制三角形的逻辑
}
}
上述代码中,我们有一个基类 Shape 表示形状,以及三个子类 Circle、Rectangle 和 Triangle 分别表示圆形、矩形和三角形。每个子类都有自己特定的 draw() 方法实现。
现在假设我们发现 Rectangle 类的绘制逻辑与基类 Shape 完全相同,可以考虑将 Rectangle 类移除,直接在基类中处理矩形的绘制逻辑。可以这样修改代码:
class Shape {
draw() {
// 绘制形状的逻辑
}
drawRectangle() {
// 绘制矩形的逻辑
}
}
class Circle extends Shape {
draw() {
// 绘制圆形的逻辑
}
}
class Triangle extends Shape {
draw() {
// 绘制三角形的逻辑
}
}
通过将 Rectangle 类移除,我们直接在基类 Shape 中添加了一个新的方法 drawRectangle(),用于绘制矩形。这样,在使用的时候,只需要根据具体的形状调用相应的方法即可。
移除子类可以简化代码结构,减少不必要的继承层次和重复代码。但需要谨慎判断是否真的可以移除子类,确保不会对代码的其他部分产生负面影响,并且在移除之前,最好通过测试来验证代码的行为是否符合预期。
- 提炼超类
- 有时候我们会发现多个子类之间存在相似的行为或属性,这时可以考虑将这些相似之处提取到一个超类中,以减少重复的代码
class Shape {
constructor(type) {
this.type = type;
}
draw() {
console.log(`Drawing ${this.type}`);
}
}
class Circle extends Shape {
constructor(radius) {
super('Circle');
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super('Rectangle');
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
上述代码中,我们有一个基类 Shape 表示形状,以及两个子类 Circle 和 Rectangle 分别表示圆形和矩形。每个子类都有自己特定的属性和方法。
现在我们发现,无论是圆形还是矩形,它们都具有一个共同的属性 type 和一个共同的方法 draw()。可以考虑将这部分相似的代码提炼到一个超类中。可以这样修改代码:
class Shape {
constructor(type) {
this.type = type;
}
draw() {
console.log(`Drawing ${this.type}`);
}
}
class Circle extends Shape {
constructor(radius) {
super('Circle');
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super('Rectangle');
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
通过创建超类 Shape,我们将共同的属性 type 和方法 draw() 提取出来,然后让子类继承超类。这样,子类就不再需要重复定义这些相同的行为或属性。
提炼超类可以减少代码冗余,并且使代码更加清晰和易于维护。同时,当需要对共同行为或属性进行修改时,只需要修改超类即可,避免了逐个修改子类的麻烦。但需要注意的是,在提炼超类之前,需要仔细考虑子类之间是否真的存在共同的行为或属性,确保提炼出的超类具有一定的通用性和合理性
- 折叠继承体系
- 折叠继承体系是指将多层继承关系简化为单层或更少层的继承结构
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}
class Mammal extends Animal {
constructor(name) {
super(name);
}
sleep() {
console.log(`${this.name} is sleeping.`);
}
}
class Dog extends Mammal {
constructor(name) {
super(name);
}
bark() {
console.log(`${this.name} is barking.`);
}
}
上述代码中,我们有一个基类 Animal 表示动物,以及两个子类 Mammal 和 Dog 分别表示哺乳动物和狗。每个子类都有自己特定的方法。
现在假设我们发现 Mammal 类只是简单地继承了 Animal 类,并没有添加新的属性或方法。可以考虑折叠继承体系,将 Mammal 类移除,直接在 Dog 类中集成 Animal 类的功能。可以这样修改代码:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
sleep() {
console.log(`${this.name} is sleeping.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
bark() {
console.log(`${this.name} is barking.`);
}
}
通过折叠继承体系,我们移除了中间的 Mammal 类,并将 Animal 类的功能直接集成到 Dog 类中。这样,Dog 类不再需要经过多层继承才能获得 Animal 类的功能。
折叠继承体系可以简化代码结构,减少继承的层次和复杂性。但需要注意的是,在进行折叠继承体系之前,需要确保移除的类没有额外的功能或属性,并且在折叠继承体系之后,最好通过测试来验证代码的行为是否符合预期。同时,还需要考虑代码的可读性和可维护性,确保折叠后的继承关系更加清晰和易于理解。
- 以委托取代子类
- 有时候我们可能会发现子类的继承关系过于复杂,或者存在多个子类只是为了覆盖父类的部分行为。这时可以考虑使用委托模式来取代子类,通过将特定的行为委托给其他对象来达到相同的效果
class Shape {
constructor(type) {
this.type = type;
}
draw() {
console.log(`Drawing ${this.type}`);
}
}
class Circle {
constructor(radius) {
this.shape = new Shape('Circle');
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
draw() {
this.shape.draw();
console.log(`Radius: ${this.radius}`);
}
}
class Rectangle {
constructor(width, height) {
this.shape = new Shape('Rectangle');
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
draw() {
this.shape.draw();
console.log(`Width: ${this.width}, Height: ${this.height}`);
}
}
上述代码中,我们有一个基类 Shape 表示形状,以及两个子类 Circle 和 Rectangle 分别表示圆形和矩形。每个子类都有自己特定的属性和方法。
现在假设我们发现 Circle 和 Rectangle 类中的 draw() 方法只是简单地调用了基类 Shape 的 draw() 方法,并且输出了一些特定的信息。可以考虑以委托取代子类,将这部分特定行为委托给另一个对象,而不是通过继承来实现。可以这样修改代码:
class Shape {
constructor(type) {
this.type = type;
}
draw() {
console.log(`Drawing ${this.type}`);
}
}
class DrawingService {
constructor(shape) {
this.shape = shape;
}
draw() {
this.shape.draw();
}
}
class Circle {
constructor(radius) {
this.shape = new Shape('Circle');
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
draw() {
const drawingService = new DrawingService(this.shape);
drawingService.draw();
console.log(`Radius: ${this.radius}`);
}
}
class Rectangle {
constructor(width, height) {
this.shape = new Shape('Rectangle');
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
draw() {
const drawingService = new DrawingService(this.shape);
drawingService.draw();
console.log(`Width: ${this.width}, Height: ${this.height}`);
}
}
通过以委托取代子类,我们将特定的绘制行为封装在 DrawingService 对象中,并在子类中使用该对象来执行绘制操作。这样既避免了多层继承的复杂性,也将特定行为委托给其他对象处理。
以委托取代子类可以简化继承关系,减少子类的数量,并且使代码更加灵活和可扩展。但需要注意的是,在应用委托模式之前,需要仔细考虑具体的委托关系和实现方式,确保委托对象拥有足够的功能和合理的接口设计。同时,还需要权衡使用继承或委托的优缺点,选择最合适的方案来解决问题。
- 以委托取代超类
- 有时候我们可能会发现一个超类(父类)的功能过于庞大,包含了多个不同的子功能,或者超类的职责不够清晰。这时可以考虑使用以委托取代超类的方式进行重构,将超类的部分功能委托给其他对象处理,以达到简化超类、解耦和提高可维护性的目的。
class SuperClass {
constructor() {
this.subFunctionA();
this.subFunctionB();
this.subFunctionC();
}
subFunctionA() {
console.log('SuperClass: subFunctionA');
}
subFunctionB() {
console.log('SuperClass: subFunctionB');
}
subFunctionC() {
console.log('SuperClass: subFunctionC');
}
}
class DelegateClass {
subFunctionA() {
console.log('DelegateClass: subFunctionA');
}
subFunctionB() {
console.log('DelegateClass: subFunctionB');
}
}
// 在需要使用的地方
class ClientClass {
constructor() {
this.delegate = new DelegateClass();
this.delegate.subFunctionA();
this.delegate.subFunctionB();
}
}
在上述示例中,我们有一个超类 SuperClass,其中包含了三个子功能函数 subFunctionA、subFunctionB 和 subFunctionC。我们希望将 subFunctionA 和 subFunctionB 两个子功能委托给其他对象处理。
于是,我们创建了一个新的类 DelegateClass,其中包含了与超类中需要委托的函数对应的实现。
在需要使用的地方,比如 ClientClass 中,我们引入了委托类 DelegateClass 的实例,并在构造函数中调用委托对象的相应函数,从而实现了将部分功能委托给其他对象处理的效果。这样一来,超类 SuperClass 就变得更加简洁和职责明确,子功能的具体实现由委托对象负责。
通过以委托取代超类,我们可以将超类的功能拆分到不同的对象中,使代码结构更清晰、职责更明确,并且在需要扩展或修改功能时更加灵活。同时,这种重构方式也遵循了面向对象设计中的单一职责原则和依赖倒置原则。
需要注意的是,在应用以委托取代超类的重构时,需要仔细考虑委托关系,确保委托对象能够正确处理被委托的功能,并且具有合理的接口设计。此外,为了保证代码的可读性和可维护性,建议给委托方法添加适当的注释或文档说明。