重构速查表

重构的相关技能
帅旋
关注
充电
IT宅站长,技术博主,架构师,全网id:arthinking。

重新组织数据

发布于 2019-03-08 | 更新于 2024-05-16

自封装字段

你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙,这个时候可以通过 Self Encapsulate Field(自封装字段) 为这个字段建立取值/设值函数,并且只以这些函数来访问字段。

1
2
3
4
5
6
class Range {
private int low, high;
boolean includes(int arg) {
return arg >= low && arg <= high;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
class Range {
private int low, high;
boolean includes(int arg) {
return arg >= getLow() && arg <= getHigh();
}
int getLow() {
return low;
}
int getHigh() {
return high;
}
}

好处

自由访问类中的字段还是通过访问函数简介访问,两种观点争论比较大。

间接访问的好处是

  • 子类可以通过覆写一个函数而改变获取数据的途径;
  • 并且支持灵活的数据管理方式,例如延迟初始化。

坏处

虽然直接访问让字段缺少了灵活性,但是它的好处是代码比较容易阅读。

以对象取代数据值

一个类(或一组类)包含了一个数据项,这个数据项有它自己的行为和关联的数据,这个时候可以通过 Replace Data Value with Object(以对象取代数据值) 创建一个类,把这些数据项和行为版一到这个新类,然后在旧类中引用这个新类。

1551674628274

好处

加强类的内聚性。在做完这一步重构步骤之后,如果被抽离的对象是一个现实生活中的实物对象(用户,订单,文档),而不是类似数据,金钱,范围之类的数值对象,下一步很有必要进行 Change Value to Reference(将值对象改为引用对象)

将值对象改为引用对象

当你从一个类衍生出许多彼此相等的实例,希望将他们替换为同一个对象的时候,可以考虑使用 Change Value to Reference(将值对象改为引用对象) ,如下图的Customer对象:

1551674651419

关键的做法是使用 Replace Constructor with Factory Method(以工厂函数取代构造函数) ,让所有需要使用Customer对象的地方,都从工厂中获取同一个实例:

1
let customer = new Customer(customerData);
1
let customer = customerRepository.get(customerData.id);

好处

对引用对象的任何修改,都会影响到所有引用此对象的地方,保持对象的一致性。

要在引用对象和值对象之间做选择有时并不容易。有时候会从一个简单的值对象开始,在其中保存少量不可以修改的数据。接下来希望添加一些可修改的数据,并且期望修改能够影响到所有引用此对象的地方,这个时候就需要把这个对象编程一个引用对象了。

将引用对象改为值对象

如果引用对象开始变得难以使用,可以通过 Change Reference to Value(将引用对象改为值对象) 把它改为值对象了。

1551674667727

最关键的是要确保这个对象是不可变的,然后再尝试更改为值对象。可以通过 Remove Setting Method 移除设置函数,确认这个对象是不可变的。

1
2
class Product {
applyDiscount(arg) {this._price.amount -= arg;}
1
2
3
4
class Product {
applyDiscount(arg) {
this._price = new Money(this._price.amount - arg, this._price.currency);
}

好处

引用对象必须被某种方式控制,你总是必须向其控制着请求适当的引用对象。这可能会造成内存区域间错综复杂的关联。在分布式系统和并发系统中,不可变的值对象特别有用,因为你无需考虑它们的同步问题。

以对象取代数组

如果你有一个数组,其中的元素各自代表不同的东西,可以尝试通过 Replace Array with Object 一对象替换数组。对于数组中的每个元素,以一个字段来表示。

1
2
3
String[] row = new String[2];
row[0] = "Liverpool";
row[1] = "15";
1
2
3
Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");

重构原因

如果把数组当成一个快递收发柜,把用户名放到一号柜,把用户地址放到二号柜…这种方式下加入有一天把一些东西放错了信箱,那么会带来灾难性的后果。并且你需要时间来确定哪个柜子是放什么东西的。

好处

  • 你可以把字段关联的行为也放到类中;
  • 类的字段比起数组的元素能够更好的进行文档化。

复制“被监视数据”

如果领域数据存放于负责UI的类中,那么应该通过 Duplicate Observed Data 将该数据复制到一个领域对象中。建立一个Observer模式,用于同步领域对象和GUI对象内的重复数据。

1551674760904

好处

  • 把业务逻辑层和视图层的代码区分开来,让程序更易懂,可读性更高;
  • 如果需要新增展示接口,直接创建一个展示类,而无需修改业务逻辑代码;
  • 更好的做前后端分离;

将单向关联改为双向关联

假设两个类需要互相使用对方的特性,但是其间只有一条单向连接,那么可以使用 Change Unidirectional Association to Bidirectional 添加一个反向指针,并使修改函数能够同时更新两条连接。

如下图,原来只有Order保存了Customer的关联关系:

1551675060100

1
2
3
4
5
6
7
8
9
10
11
class Order...
Customer getCustomer() {
return _customer;
}

voi setCustomer(Customer arg) {
_customer = arg;
}

Customer _customer;

重构步骤

1、在被引用类中增加一个字段用于保存反向指针

接下来,在Customer中也添加Order的关联关系:

1551675077541

1
2
3
class Customer {
private Set _orders = new HashSet();
}

2、选择一方来控制关联关系

选择一方来控制关联关系,选择方法:

1、多对一,则多的一方控制关联关系,如果一个客户可以拥有多分订单,那么就由Order类来控制关联关系;

2、如果一个对象是另一个对象的部件,那么由整体那方负责控制关联关系;

3、如果两者都是引用对象,关联关系是多对多,则随便选取一方来控制关联关系。

本例中,我们选择Order来控制关联关系,那么需要为Customer需要一个可以直接访问 _orders 集合的方法,提供给Order来维护反向关系:

3、在被控制端建立一个辅助函数

1
2
3
4
class Customer...
Set friendOrders() {
return _orders;
}

现在可以修改函数,令它同时更新反向指针:

4、在修改函数更反向指针

1
2
3
4
5
6
7
8
9
10
11
class Order...
void setCustomer(Customer arg) {
if (_customer != null)
_customer.friendOrders().remove(this);

_customer = arg;

if (_customer != null)
_customer.friendOrders().add(this);

}

将双向关联改为单向关联

如果两个类之间有双向关联,但是其中一个类如今不再需要另一个类的特性了,那么可以使用 Change Bidirectional Association to Unidirectional 去除不必要的关联。

如下图,把OrderCustomer的关联关系去除。

1551674838072

需要注意的是,在去除关联的时候,需要确保原先的关联关系没有在该类中使用,或者可以通过其他方式替换该关联关系,如方法参数列表中传入原本关联的对象:

1
2
3
4
class Order...
double getDiscountedPrice() {
return getGrossPrice() * (1 - _customer.getDiscount());
}

改为:

1
2
3
4
class Order...
double getDiscountedPrice(Customer customer) {
return getGrossPrice() * (1 - customer.getDiscount());
}

以字面常量取代魔法数

如果有一个字面值,带有特别的含义,那么应该使用 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) 创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。

1
2
3
double potentialEnergy(double mass, double height) {
return mass * height * 9.81;
}
1
2
3
4
5
static final double GRAVITATIONAL_CONSTANT = 9.81;

double potentialEnergy(double mass, double height) {
return mass * height * GRAVITATIONAL_CONSTANT;
}

封装字段

如果类中存在一个public字段,可以通过 Encapsulate Field(封装字段) 将它声明为private,并提供相应的访问函数:

1
2
3
class Person {
public String name;
}
1
2
3
4
5
6
7
8
9
10
class Person {
private String name;

public String getName() {
return name;
}
public void setName(String arg) {
name = arg;
}
}

好处

  • 如果一个组件的数据和行为紧密相关,并且在代码中的同一个地方,那么就可以很好的进行维护和开发;
  • 你也可以在访问字段的方法里嵌入一些复杂的操作。

封装集合

如果有一个函数返回一个集合,可以使用 Encapsulate Collection(封装集合) 让这个函数返回该集合的只读副本,并在这个类中提供添加/移除集合元素的函数。

另外提供用于为集合添加/移除元素的函数,这样集合的拥有者就可以控制集合元素的添加和移除了。

1
2
3
class Person {
public Set getCourses() {return this._courses;}
public void Setcourses(Set arg) {this._courses = arg;}
1
2
3
4
5
6
7
8
9
class Person {
public Set getCourses() {
return Collections.unmodifiableSet(_courses);
}

addCourse(aCourse) { ... }

removeCourse(aCourse) { ... }

好处

  • 集合字段封装在类中,如果取值函数返回集合副本,就不会因为在集合所在类不知情的情况下,意外的修改或者覆盖集合元素,产生不期望的结果;
  • 如果集合元素存放于基本的容器(ArrayList…),那么你可以封装提供一些更加方便的方法来操作元素;
  • 如果集合元素存放于经过自己封装的容器,那么就可以控制容器的行为了,如禁止添加元素等。

以数据类取代记录

假如需要面对传统编程环境中的记录结构,如xml,json格式,可以通过 Replace Record with Data Class(以数据类取代记录) 为该记录创建一个“哑”数据对象。

1
organization = {name: "Acme Gooseberries", country: "GB"};
1
2
3
4
5
6
7
8
9
10
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() {return this._name;}
set name(arg) {this._name = arg;}
get country() {return this._country;}
set country(arg) {this._country = arg;}
}

以类取代类型码

类中有一个数值类型码,而它不影响类的行为,那么可以使用 Replace Type Code with Class(以类取代类型码) 以一个新的类替换该数值类型码:

1551674865383

1
2
orders.filter(o => "high" === o.priority
|| "rush" === o.priority);
1
orders.filter(o => o.priority.higherThan(new Priority("normal")))

好处

  • 我们希望将原始类型(编码类型)转换为成熟的对象,从而具有面向对象编程必须提供的所有好处;
  • 通过用类替换类型代码,我们允许对传递给编程语言级别的方法和字段的值进行类型提示;
  • 我们可以将代码移动到类型的类中。如果您需要在整个程序中使用类型值执行复杂的操作,这样此代码可以“存活”在一个或多个类型类中。

以子类取代类型码

如果你有一个不可变的类型码,它会影响类的行为,那么可以通过 Replace Type Code with Subclasses(以子类取代类型码) 以子类取代类型码。

1551674885130

1
2
3
function createEmployee(name, type) {
return new Employee(name, type);
}
1
2
3
4
5
6
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name);
case "salesman": return new Salesman(name);
case "manager": return new Manager (name);
}

好处

  • 可以删除控制流代码块,以多态取代switch语句,这体现了单一职责原则,让代码更具有可读性;
  • 如果你需要新增一个类型码,直接增加一个子类即可;
  • 通过用类替换类型代码,我们为编程语言级别的方法和字段的类型提示铺平了道路。在使用编码类型的时候包含的简单数字或字符串值是不可能的。

注意

以Strate/Strategy取代类型码

如果有一个类型码,会影响类的行为,但是无法通过继承手法消除(已有继承体系),那么可以通过 Replace Type Code with State/Strategy(以State/Strategy取代类型码) 以状态对象取代类型码。

1551674911793

好处

  • 当具有编码类型的字段在对象的生命周期改变其值时,这种重构技术是一种不错方法。在这种情况下,通过替换原始类所引用的状态对象来替换值;
  • 如果需要添加编码类型的新值,您需要做的就是添加新的状态子类而不改变现有代码(参见开放/封闭原则)。

以字段取代子类

假设你的各个子类的唯一差别只在“返回常量数据”的函数身上,那么请使用 Replace Subclass with Fields(以字段取代子类)

1551675305408

1
2
3
4
5
6
7
8
9
class Person {
get genderCode() {return "X";}
}
class Male extends Person {
get genderCode() {return "M";}
}
class Female extends Person {
get genderCode() {return "F";}
}
1
2
3
class Person {
get genderCode() {return this._genderCode;}
}

好处

  • 将大量功能从类层次结构移动到另一个位置后,可能需要进行类继承体系的更改。如果当前的层次结构不再那么有价值,那么久进行删减,可以根据情况将此层次结构压缩为包含一个或多个具有必要值的字段的单个类;
  • 简化系统架构。 如果你想要做的就是在不同的方法中返回不同的值,那么创建子类就太过分了。

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/refactoring/reorganize-data.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。

请帅旋喝一杯咖啡

咖啡=电量,给帅旋充杯咖啡,他会满电写代码!

IT宅

关注公众号及时获取网站内容更新。