重构-改善既有代码的设计(三)
封装
封装记录(Encapsulate Record)
示例
1
organization = {name: "Acme Gooseberries",country:"GB"};重构为
1
2
3
4
5
6
7
8
9
10class Oraganization{
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; }
}动机
记录型结构是多数编程语言提供的一种常见特性,它们能直观地组织起存在关联的数据,让我可以将数据作为有意义的单元传递,而不仅是一堆数据的拼凑。
做法
- 对持有记录的变量使用封装变量,将其封装到一个函数中。
- 创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。然后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令其使用这个访问函数。
- 测试。
- 新建一个函数,让它返回该类的对象,而非那条原始的记录。
- 对于该记录的每处使用点,将原来返回记录的函数调用替换为那个返回实例对象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问函数还不存在,那就创建一个。每次更改之后运行测试。
- 移除类对原始记录的访问函数,那个容易搜索的返回原始数据的函数也要一并删除。
- 测试。
- 如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录或封装集合手法。
封装集合(Encapsulate Collection)
示例
1
2
3
4class Person{
get course() { return this._courses; }
set course(aList) { this._courses = aList; }
}重构为
1
2
3
4
5class Person{
get course() { return this._courses; }
addCourse(aCourse) {...}
removeCourse(aCourse) {...}
}动机
- 只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。这使得集合的成员可以直接被修改,而封装它的类则全然不知,无法介入。
- 一种避免直接修改集合的方法是,永远不返回集合的值。这种方法提倡,不要直接使用集合的手段,而是通过定义类上的方法来代替。(不推荐)
- 还有一种方法是,以某种形式限制集合的访问权,只允许对集合进行读操作。
- 最常见的做法是,为集合提供一个取值函数,但令其返回一个集合的副本。
做法
- 如果集合的引用尚未被封装起来,先用封装变量封装它。
- 在类上添加用于“添加集合元素”和“移除集合元素”的函数。
- 执行静态检查。
- 查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/移除元素的函数。每次修改后执行测试。
- 修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副本。
- 测试。
以对象取代基本类型(Replace Primitive with Object)
示例
1
orders.filter(o => "high" === o.priority || "rush" === o.priority);重构为
1
orders.filter(o => o.priority.higherThan(new Priority("normal")));动机
一旦我发现对某个数据的操作不仅仅局限于打印时,我就会为它创建一个新类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要类有了,日后添加的业务逻辑就有地可去了。
做法
- 如果变量尚未被封装起来,先使用封装变量封装它。
- 为这个数据值创造一个简单的类。类的构造函数应该保存这个数据值,并为它提供一个取值函数。
- 执行静态检查。
- 修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果有必要的话,同时修改字段的类型声明。
- 修改取值函数,令其调用新类的取值函数,并返回结果。
- 测试。
- 考虑对第一步得到的访问函数使用函数改名,以便更好反映其用途。
- 考虑应用将引用对象改为值对象或将值对象改为引用对象,明确指出新对象的角色是值对象还是引用对象。
以查询取代临时变量
示例
1
2
3
4
5const basePrice = this._quantity * this._itemPrice;
if(basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;重构为
1
2
3
4
5
6
7
8get basePrice() {this._quantity * this._itemPrice;}
···
if(this.basePrice > 1000)
return this.basePrice * 0.95;
else
return this.basePrice * 0.98;动机
- 如果我正在分解一个冗长的函数,那么将变量抽取到函数里能使函数的分解过程更简单,因为我就不再需要将变量作为参数传递给提炼出来的小函数。将变量的计算逻辑放到函数中,也有助于在提炼得到的函数与原函数之间设立清晰的边界,这能帮我发现并避免难缠的依赖和副作用。
- 改用函数还让我避免了在多个函数中重复编写计算逻辑。
- 这项重构手法在类中施展效果最好,因为类为待提炼函数提供了一个共同的上下文。
- 这项重构手法只适用于处理某些类型的临时变量:那些只被计算一次且之后不再被修改的变量。
做法
- 检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得到一样的值。
- 如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它。
- 测试。
- 将为变量赋值的代码段提炼成函数。
- 测试。
- 应用内联变量手法移除临时变量。
提炼类(Extract Class)
示例
1
2
3
4class Person{
get officeAreaCode() {return this._officeAreaCode;}
get officeNumber() {return this._officeNumber;}
}重构为
1
2
3
4
5
6
7
8class Person{
get officeAreaCode() {return this._telephonerNumber.areaCode;}
get officeNumber() {return this._telephonerNumber.number;}
}
class TelephoneNumber{
get areaCode() {return this._areaCode;}
get number() {return this._number;}
}动机
- 如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该将它们分离出去。
- 如果你发现子类化只影响类的部分特性,或如果你发现某些特性需要以一种方式来子类化,某些特性则需要以另一种方式子类化,这就意味着你需要分解原来的类。
做法
- 决定如何分解类所负的责任。
- 创建一个新的类,用以表现从旧类中分离出来的责任。
- 构造旧类时创建一个新类的实例,建立“从旧类访问新类”的连接关系。
- 对于你想搬移的每一个字段,运用搬移字段搬移之。每次更改后运行测试。
- 使用搬移函数将必要函数搬移到新类。先搬移较低层函数(也就是“被其他函数调用”多于“调用其他函数”者)。每次更改后运行测试。
- 检查两个类的接口,去掉不再需要的函数,必要时为函数重新取一个适合新环境的名字。
- 决定是否公开新的类。如果确实需要,考虑对新类应用将引用对象改为值对象使其变成一个值对象。
内联类(Inline Class)
示例
1
2
3
4
5
6
7
8class Person{
get officeAreaCode() {return this._telephonerNumber.areaCode;}
get officeNumber() {return this._telephonerNumber.number;}
}
class TelephoneNumber{
get areaCode() {return this._areaCode;}
get number() {return this._number;}
}重构为
1
2
3
4class Person{
get officeAreaCode() {return this._officeAreaCode;}
get officeNumber() {return this._officeNumber;}
}动机
- 如果一个类不再承担足够责任,不再有单独存在的理由,我就会挑选这一“萎缩类”的最频繁用户,以本手法将“萎缩类”塞进另一个类中。
- 另一个应用场景,我手头上有两个类,想重新安排它们肩负的职责,并让它们产生关联。这时我发现先用本手法将它们内联成一个类再用提炼类去分离其职责会更简单。
做法
- 对于待内联类(源类)中的所有 public 函数,在目标类上创建一个对应的函数,新创建的所有函数应该直接委托至源类。
- 修改源类 public 方法的所有引用点,令它们调用目标类对应的委托方法。每次更改后运行测试。
- 将源类中的函数与数据全部搬移至目标类,每次修改之后进行测试,直到源类变成空壳为止。
- 删除源类,为它举行一个简单的“丧礼”。
隐藏委托关系(Hide Delegate)
示例
1
manager = aPerson.department.manager;重构为
1
2
3
4
5manager = aPerson.manager;
class Person {
get manager() {return this.department.manager;}
}动机
“封装”意味着每个模块都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一变化的模块就会比较少————这会使变化比较容易进行。
做法
- 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数。
- 调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试。
- 如果将来不再有任何客户端需要取用Delegate(受托类),便可以移除服务对象中的相关访问函数。
- 测试。
移除中间人(Remove Middle Man)
示例
1
2
3
4
5manager = aPerson.manager;
class Person{
get manager() { retuen this.department.manager; }
}重构为
1
2
3
4
5manager = aPerson.manager;
class Person {
get manager() {return this.department.manager;}
}动机
随着受托类的特性越来越多,更多的转发函数就会使人烦躁。服务类完全变成了一个中间人,此时就应该让客户直接调用受托类。
做法
- 为受托对象创建一个取值函数。
- 对于每一个委托函数,让其客户端转为连续的访问函数调用。每次替换后运行测试。
替换算法(Substitute Algorithm)
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14function foundPerson(people){
for(let i = 0; i< people.lengyj; i++){
if(people[i] == "Don"){
return "Don";
}
if(people[i] == "John"){
return "John";
}
if(people[i] == "Kent"){
return "Kent";
}
}
return "";
}重构为
1
2
3
4function foundPerson(people){
const candidates = ["Don","John","Kent"];
return people.find(p => candidates.includes(p)) || '';
}动机
如果我发现做一件事可以有更清晰的方式,我就会用比较清晰的方式取代复杂的方式。“重构”可以把一些复杂的东西分解为较简单的小块,但有时候你就必须壮士断腕,删掉整个算法,代之以较简单的算法。
做法
- 整理一下待替换的算法,保证它已经被抽取到一个独立的函数中。
- 先只为这个函数准备测试,以便固定它的行为。
- 准备好另一个(替换)函数。
- 执行静态检查。
- 运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则,在后续测试和调试过程中,以旧算法为比较参照标准。
重构-改善既有代码的设计(三)
http://blog.chcaty.cn/2021/05/06/chong-gou-gai-shan-ji-you-dai-ma-de-she-ji-san/