08 数据重构

数据重构

数据应该符合封装的思想
是直接读取还是间接读取?
是用字段还是用对象?
是值对象还是引用对象?
是单向关联还是双向关联?
如何更好的表示类型码?常量?类?

Self Encapsulate Field(自封装值域)

problem:你直接访问一个私有值域(field),但与值域之间的耦合关系逐渐无法满足需求。
solution:为这个值域建立取值/设值函数(getting and setting methods ),并且只以这些函数来访问值域。

1
2
3
4
5
6
7
8
9
10
11
12
13
 private int _low, _high;
boolean includes (int arg) {
return arg >= _low && arg <= _high;
}

//======================after refactoring=========================

private int _low, _high;
boolean includes (int arg) {
return arg >= getLow() && arg <= getHigh();
}
int getLow() {return _low;}
int getHigh() {return _high;}

重构原因?

「直接访问变量」的好处则是:代码比较容易阅读,不需要再多此一举。

「间接访问变量」更符合封装,好处是:

  1. 我可以在getter方式,或者setter方法中统一增加查询和修改的逻辑,减少耦合。
  2. 支持更灵活的数据管理方式:在字段getter和setter中可以轻松实现字段值的延迟初始化和验证。
  3. 可以在子类中覆盖getter和setter中和方法,实现自己的需求。

    直接访问变量也并无坏处,一般可以根据公司的要求来,或者根据需要变化。

Replace Data Value with Object(以对象取代数据值)

problem:多个类中包含相同的一个数据字段,这个数据字段有相同的行为和关联数据。
solution:创建一个新类,把这个字段和他的行为放进去;original class保存这个新类。

重构原因?

该重构是Extract Class的特例。Extract Class是分离类的职责,而Replace Data Value with Object是解决“这个字段和行为系列可以同时出现在几个类中,从而创建重复的代码”的问题的。

如何重构?

  • 使用Self Encapsulate Field隐藏original class中对于这个字段的访问
  • 创建一个新类,copy字段和getter方法。创建一个构造器来初始化值。暂时不需要setter字段,因为目前只是重构成一个值对象。
  • 在original class中,把字段的类型改成new class类型。
  • 在original class的构造器中初始化这个字段。
  • original class的getter调用associated object的getter
  • original class的setter创建一个new class。

什么是值对象和引用对象?

值对象:一个现实对象对应多个对象。比如地址/颜色/喜好。用对象的关系来体现,引用对象就是组合关系。

引用对象:一个现实对象对应一个对象。比如客户/产品。用对象的关系来体现,引用对象就是聚合关系。

对于对象关系,请参考【UML】一文

Change Value to Reference(将实值对象改为引用对象)

把值对象变为引用对象

重构原因?

值对象一般不应该具有setter方法,如果希望修改数据,并且希望所有引用此对象的地方都能看到这个修改。

Change Reference to Value(把引用对象变成值对象)

把引用对象变成值对象

重构原因?

引用对象不好用了:

  1. 可能造成内存区域之间错综复杂的关联。
  2. 在分布系统和并发系统中,不可变的value object特别有用,因为你无须考虑它们的同步问题。
  3. 我希望简单的访问一些不会被改变的对象

Replace Array with Object(以对象取代数组)

以对象替换数组。对于数组中的每个元素,以一个值域表示之。

1
2
3
4
5
6
7
8
9
String[] row = new String[3];
row [0] = "Liverpool";
row [1] = "15";

//======================after refactoring=========================

Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");

重构原因?

数组应该只用于「以某种顺序容纳一组相似对象」
不要用数组来记录数种不同对象,因为很难记住第一个元素表示什么含义。

Change Unidirectional Association to Bidirectional(将单向关联改为双向)

problem:两个classes都需要使用对方特性,但其间只有一条单向连接(one-way link)。
solution:添加一个反向指针,并使修改函数(modifiers)能够同时更新两条连接。(译注:这里的指针等同于句柄(handle),修改函数(modifier)指的是改变双方关系者)
这个修改函数所在类就是维护方。

重构原因?

随着时间推移,可能发现referred class需要得到其引用者(某个object)以便进行某些处理。
在获取反向引用的过程比较麻烦的时候需要进行重构

如何重构?

  • 在class中增加一个值域,用以保存「反向指针」。
  • 决定由哪个class (引用端或被引用端)控制关联性(association)。
  • 在「被控端」建立一个辅助函数,其命名应该清楚指出它的有限用途。
  • 如果既有的修改函数(modifier)在「控制端」,让它负责更新反向指针。
  • 如果既有的修改函数(modifier)在「被控端」,就在「控制端」建立一个控制函数,并让既有的修改函数调用这个新建的控制函数。

哪一段比较适合作为控制端?

【一对多】:让多的一方作为控制端
【一对一和多对多】:任意一方

Change Bidirectional Association to Unidirectional(将双向关联改为单向)

problem:两个鄉之间有双向关联,但其中一个class如今不再需要另一个class的特性。
solution:去除不必要的关联(association)。

重构原因?

  1. 双向关联(bidirectional associations)很有用,但你也必须为它付出代价,那就是「维护双向连接、确保对象被正确创建和删除」而增加的复杂度。
  2. 大量的双向连接(two-way links)也很容易引发「僵尸对象」:某个对象本来已经该死亡了,却仍然保留在系统中,因为对它的各项引用还没有完全清除。
  3. 过多的双向关联会导致紧耦合过多,造成一个类的变化对另外的类造成影响。
    当不再需要另一个class的特性的时候,可以进行重构。

Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数)

problem:使用具有特定含义的数字。
solution:将此数字替换为具有解释数字含义的可读名称的常量。

1
2
3
4
5
6
7
8
9
10
double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}

//======================after refactoring=========================

double potentialEnergy(double mass, double height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;

重构原因?

一旦这些具有特殊含义的数值不断发生改变,将会是一场噩梦。

Encapsulate Field(封装值域)

problem:你的class中存在一个public值域。
solution:将它声明为private,并提供相应的访问函数(accessors)。

和Self Encapsulate Field不同,这里的字段是public的。

1
2
3
4
5
6
7
public String _name

//======================after refactoring=========================

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

重构原因?

符合封装的思想
间接管理更灵活

Encapsulate Collection(封装群集)

有个函数(method)返回一个群集(collection)。
让这个函数返回该群集的一个只读映件(read-only view),并在这个class中提供「添加/移除」(add/remove)群集元素的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 class Person{
private Set _courses;
public Set getCourses() {
return _courses;
}
public void setCourses(Set arg) {
_courses = arg;
}
}

//======================after refactoring=========================

class Person{
public void addCourse (Course arg) {
_courses.add(arg);
}
public void removeCourse (Course arg) {
_courses.remove(arg);
}
public Set getCourses() {
return Collections.unmodifiableSet(_courses);
}
}

重构原因?

也是封装的思想
不应该让其他类直接获取和修改数据

Replace Type Code with Class(以类取代型别码)

class之中有一个数值型别码(numeric type code),但它并不影响class的行为。

针对JAVA,使用枚举类也行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Person {
public static final int O = 0;
public static final int A = 1;
public static final int B = 2;
public static final int AB = 3;
private int _bloodGroup;
public Person (int bloodGroup) {
_bloodGroup = bloodGroup;
}
public void setBloodGroup(int arg) {
_bloodGroup = arg;
}
public int getBloodGroup() {
return _bloodGroup;
}
}

//======================after refactoring=========================

class Person {
private BloodGroup _bloodGroup;
public Person (BloodGroup bloodGroup) {
this._bloodGroup = bloodGroup;
}
public int getBloodGroup() {
return _bloodGroup.getCode();
}
public void setBloodGroup(BloodGroup bloodGroup) {
this._bloodGroup = bloodGroup;
}
}

class BloodGroup {
public static final BloodGroup O = new BloodGroup(0);
public static final BloodGroup A = new BloodGroup(1);
public static final BloodGroup B = new BloodGroup(2);
public static final BloodGroup AB = new BloodGroup(3);
private static final BloodGroup[] _values = {O, A, B, AB};
private final int _code;
private BloodGroup (int code ) {
_code = code;
}
public int getCode() {
return _code;
}
public static BloodGroup code(int arg) {
return _values[arg];
}
}

type code是什么?

表示一系列实体的类型常量值集合

1
2
3
4
public static final int O = 0;
public static final int A = 1;
public static final int B = 2;
public static final int AB = 3;

不影响class的行为是什么意思?

type code只是纯粹数据
表示class不会因为type code的不同而执行不同的方法,表现不同的行为(比较常见的是type code会在switch语句中引起行为变化时)。

重构原因?

  1. 以type code为参数的函数实际上,只是接受一个int类型的数据,大大的降低代码的可读性。
  2. 常量无法进行类型检查,无法避免输入错误带来的影响。

Replace Type Code with Subclasses(以子类取代型别码)

problem:你有一个不可变的(immutable)type code,它会影响class的行为。
solution:以一个subclass 取代这个type code。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//这是一个使用Replace Type Code with Class重构过的类,其实没有必要重构
enum EmployeeTypeEnum{
ENGINEER(0),SALESMAN(1),MANAGER(2);
private _type;
EmployeeType (int type) {
_type = type;
}
int getType() {
return _type;
}
}

class Employee{
EmployeeTypeEnum employeeType;
Employee(EmployeeTypeEnum e){
employeeType = e;
}
EmployeeTypeEnum getType(){
return employeeType;
}
void doSomething(EmployeeTypeEnum type) {
switch (employeeType) {
case EmployeeTypeEnum.ENGINEER:
doSomething1();
case EmployeeTypeEnum.SALESMAN:
doSomething2();
case EmployeeTypeEnum.MANAGER:
doSomething3();
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}

//很多处会用到switch语句
}

//======================after refactoring=========================

class Engineer extends Employee {
int doSomething(){
...;
}
EmployeeTypeEnum getType(){
return EmployeeType.ENGINEER;
}
//..
}

class Salesman extends Employee {
int doSomething(){
...;
}
EmployeeTypeEnum getType(){
return EmployeeType.SALESMAN;
}
//..
}

class Manager extends Employee {
int doSomething() {
...;
}
EmployeeTypeEnum getType(){
return EmployeeType.MANAGER;
}
//..
}

abstract class Employee{

//静态工厂方法
static Employee create(EmployeeTypeEnum type) {
switch (type) {
case EmployeeTypeEnum.ENGINEER:
return new Engineer();
case EmployeeTypeEnum.SALESMAN:
return new Salesman();
case EmployeeTypeEnum.MANAGER:
return new Manager();
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}

abstract doSomething();
}

这里只有一处用到switch语句,并且只用于决定创建何种对象,这样的switch语句是可以接受的。

重构原因?

  1. 每一个方法,都需要根据类型来表现不同的行为,会导致代码中具有很多的switch语句
  2. 不符合开闭原则,如果需要增加一个type code,就需要在每一个方法的switch中都增加响应的逻辑,这样的工作也是十分头疼的。

Replace Type Code with State/Strategy(以State/strategy 取代型别码)

problem:你有一个type code,它会影响class的行为,但你无法使用subclassing。
solution:以state object(专门用来描述状态的对象)取代type code 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//这是一个使用Replace Type Code with Class重构过的类,其实没有必要重构
enum EmployeeTypeEnum{
ENGINEER(0),SALESMAN(1),MANAGER(2);
private _type;
EmployeeType (int type) {
_type = type;
}
int getType() {
return _type;
}
}

class Employee{
EmployeeTypeEnum employeeType;
Employee(EmployeeTypeEnum e){
employeeType = e;
}
EmployeeTypeEnum getType(){
return employeeType;
}
void doSomething(EmployeeTypeEnum type) {
switch (employeeType) {
case EmployeeTypeEnum.ENGINEER:
doSomething1();
case EmployeeTypeEnum.SALESMAN:
doSomething2();
case EmployeeTypeEnum.MANAGER:
doSomething3();
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}

//很多处会用到switch语句
}

//======================after refactoring=========================

abstract class EmployeeType{
//静态工厂方法
static EmployeeType create(EmployeeTypeEnum type) {
switch (type) {
case EmployeeTypeEnum.ENGINEER:
return new Engineer();
case EmployeeTypeEnum.SALESMAN:
return new Salesman();
case EmployeeTypeEnum.MANAGER:
return new Manager();
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}

abstract int doSomething(){
...;
}
abstract int getTypeCode();
//..
}

class Engineer extends EmployeeType {
int doSomething(){
...;
}
EmployeeTypeEnum getTypeCode () {
return EmployeeTypeEnum.ENGINEER;
}
//..
}

class Salesman extends EmployeeType {
int doSomething(){
...;
}
EmployeeTypeEnum getTypeCode () {
return EmployeeTypeEnum.MANAGER;
}
//..
}

class Manager extends EmployeeType {
int doSomething() {
...;
}
EmployeeTypeEnum getTypeCode () {
return EmployeeTypeEnum.SALESMAN;
}
//..
}

class Employee{

EmployeeType employee;

void setType(EmployeeTypeEnum e) {
employee = EmployeeType.create(e);
}

public doSomething(){
employee.doSomething();
};
}

Replace Type Code with Subclasses和Replace Type Code with State/Strategy重构后,很自然的会使用Replace Conditional with Polymorphism进行重构,上面都是使用Replace Conditional with Polymorphism重构后的结果

重构原因?

无法使用Replace Type Code with Subclasses:

  1. 原类已经具有其他子类。
  2. type code可能在运行的过程中变化(一个不变对象所属类型发生变化),使用子类很难做到这一点。原因是使用Replace Type Code with Subclasses,每一个对象都是一个类型,两者是一体的关系,无法分离开。而Replace Type Code with State/Strategy,对象是对象,类型是类型,分离后就可以实时改变类型。

Replace Subclass with Fields(以值域取代子类)

你的各个subclasses 的惟一差别只在「返回常量数据」的函数身上。
修改这些函数,使它们返回superclass 中的某个(新增)值域,然后销毁subclasses 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

abstract class Person {
abstract boolean isMale();

abstract char getCode();
}

class Male extends Person {
boolean isMale() {
return true;
}

char getCode() {
return 'M';
}
}

class Female extends Person {
boolean isMale() {
return false;
}

char getCode() {
return 'F';
}
}

//======================after refactoring=========================

class Person {
private final boolean _isMale;
private final char _code;

Person(boolean isMale, char code) {
_isMale = isMale;
_code = code;
}

static Person createMale() {
return new Male(true, 'M');
}

static Person createFemale() {
return new Female(false, 'F');
}

boolean isMale() {
return _isMale;
}

char getCode() {
return _code;
}
}

重构原因?

子类没有什么额外的新特性,也没有明显的变化行为。只有一个返回常量的变化行为,这个常量的用途是:不同的子类会返回不同的常量。
除此之外,子类实在没有足够的存在价值

本文标题:08 数据重构

文章作者:Sun

发布时间:2019年01月11日 - 17:01

最后更新:2019年01月17日 - 20:01

原始链接:https://sunyi720.github.io/2019/01/11/refactoring/08 数据重构/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。