以下内容来自《重构》第二版第七章部分内容
1. 封装记录
organization = { name: "xp", country: "CH" };
// 封装
class Origanization {
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;
}
}
记录型结构是多树编程语言提供的一种常见特性。它们能直观地组织起存在关联的数据,让我们可以将数据作为有意义的单元传递,而不仅是一堆数据的拼凑。单简单的记录型结构也有缺陷,最恼人的一点是:它强迫我们清晰的区分 ”记录中存储的数据“ 和 ”通过计算得到的数据“。 假使我们要描述一个整数闭区间,我们可以使用 { start: 1, end: 5 }
描述,或者使用 { start: 1, length: 5 }
。
- 对于可变数据我们可以使用对象而非记录
- 对象可以隐藏结构的细节,直接为变量的值提供方法。用户不必追究存储的细节和计算的过程
示例:
首先从常量开始
organization = { name: "xp", country: "CH" };
下面是对这个常量读取和更新的地方
result += `<h1>${organization.name}</h1>`
organization.name = newName
重构的第一步很简单,先使用 封装变量
function getRawDataOfOranization(){ return organization; }
读取的例子
result += `<h1>${getRawDataOfOranization().name}</h1>`;
更新的例子
getRawDataOfOranization().name = newName
这里使用的不全是标准的 封装变量 手法。封装记录意味着,仅仅替换变量还不够,我们还需要控制它的使用方式。可以使用类来替换记录,从而达到这一目的
class Organization {
constructor(data) {
this._data = data
}
}
顶层作用域:
const organization = new Organization({ name: "xp", country: "CH" })
function getRawDataOfOranization() { return organization._data }
function getOrganization() { return organization }
创建完对象后,开始寻找该记录的使用点,并更新
// class Organization
set name(aString) { this._name = aString }
// 客户端
getOrganization().name = newName
同样的,将所有读取记录的地方用取值函数来替代
// class Organization
get name() { return this._name }
// 客户端
result += `<h1>${getOrganization().name}</h1>`
完成引用替换后
修改
function getRawDataOfOranization(){ return organization; }
为
function getOrganization() { return organization }
最后,将类改成
class Organization {
constructor(data) {
this._name = data.name
this._country = data.country
}
get name() { return this._name }
set name(aString) { return this._name = aString }
get country() { return this._country }
set country(aCountryCode) { return this._country = aCountryCode }
}
2. 以查询取代临时变量
const basePrice = this._quantity * this._itemPrice
if (basePrice > 1000)
return basePrice * 0.95
else
return basePrice * 0.98
// ======== 重构 ========
get basePrice() { this._quantity * this._itemPrice}
if (basePrice > 1000)
return this.basePrice * 0.95
else
return this.basePrice * 0.98
如果在不同的地方看到同一段变量的计算逻辑,可以把他们挪到同一个函数里
以查询取代临时变量 手法只适用于处理某些类型的临时变量:那些只被计算一次且之后不再被修改的变量。最简单的情况是这个临时变量只被赋值一次,但在更复杂的代码片段里,变量也可以被多次赋值—此时应该将这些计算代码一并提炼到查询函数中。并且待提炼
的逻辑多次计算同样的变量时,应该能得到相同的结果。因此,对于那些做快照用途的临时变量,就不能使用本手法
- 检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得到一样的值
- 如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它
- 测试
- 将变量赋值的代码提炼成函数
- 如果变量和函数不能使用同样的名字,那么先为函数取个临时的名字
- 确保待提炼函数没有副作用。若有,应先用 查询函数和修改函数分离 手法隔离副作用
- 测试
- 应用 内联变量 手法移除临时变量
示例:简单的订单类
class Order{
constructor(quantity, item) {
this._quantity = quantity
this._item = item
}
get price() {
var basePrice = this._quantity * this._item.price
var discountFactor = 0.98
if (basePrice > 1000) discountFactor -= 0.03
return basePrice * discountFactor
}
}
这里把 basePrice
和 discountFactor
两个临时变量提炼成函数
先从 basePrice
开始
class Order{
constructor(quantity, item) {
this._quantity = quantity
this._item = item
}
get price() {
const basePrice = this.basePrice
var discountFactor = 0.98
if (basePrice > 1000) discountFactor -= 0.03
return basePrice * discountFactor
}
// 提炼函数
get basePrice() {
return this._quantity * this._item.price
}
}
使用 内联变量
class Order{
constructor(quantity, item) {
this._quantity = quantity
this._item = item
}
get price() {
var discountFactor = 0.98
if (this.basePrice > 1000) discountFactor -= 0.03
return this.basePrice * discountFactor
}
get basePrice() {
return this._quantity * this._item.price
}
}
接下来对 discountFactor
进行同样的步骤
class Order{
constructor(quantity, item) {
this._quantity = quantity
this._item = item
}
get price() {
return this.basePrice * this.discountFactor
}
get basePrice() {
return this._quantity * this._item.price
}
get discountFactor() {
var discountFactor = 0.98
if (this.basePrice > 1000) discountFactor -= 0.03
return discountFactor
}
}
3. 提炼类
class Person {
get officeAreaCode() { return this._officeAreaCode }
get officeNumber() { return this.officeNumber }
}
=============== 重构 ===============
class Person {
get officeAreaCode() { return this._telephoneNumber.areaCode }
get officeNumber() { return this._telephoneNumber.number }
}
class TelephoneNumber {
get areaCode() { return this._areaCode }
get number() { return this._number }
}
一个类应该是一个清晰的抽象,只处理一些功能明确的责任。但是实际工作中,类会不断成长扩展,到了后面类就会变得过分复杂。如果某些数据和函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该将它们分离出去。
- 决定如何分解类所负责的责任
- 创建一个新的类,用以表现从旧类中分离出来的责任
- 如果旧类剩下的责任与旧类的名称不符合,为旧类改名
- 构造旧类时创建一个新类的实例,建立“从旧类访问新类”的连接关系
- 对于你想搬移的每一个字段,运用 搬移字段 。每次更改后运行测试
- 使用 搬移函数 将必要的函数搬移到新类。先搬移较低层函数(也就是“被其它函数调用” 多于 “调用其它函数”者)。每次更改之后运行测试
- 检查两个类的接口,去掉不再需要的函数,必要时为函数重新取一个适合新环境的名字
- 决定是否公开信的类。如果确实需要,考虑对新使用 将引用对象改为值对象 使其成为一值对象
首先从一个Person
类开始
class Person {
get name() { return this._name }
set name(arg) { this._name = arg }
get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}` }
get officeAreaCode() { return this._officeAreaCode }
set officeAreaCode(arg) { this._officeAreaCode = arg}
get officeNumber() { return this._officeNumber }
set officeNumber(arg) { this._officeNumber = arg}
}
这里可以将与电话号码相关的行为分离到一个独立的类中。
class TelephoneNumber{
}
然后在构造Person
类时创建 TelephoneNumber
类的一个实例
class Person{
constructor() {
this._telephoneNumber = new TelephoneNumber()
}
}
class TelephoneNumber {
get officeAreaCode() { return this._officeAreaCode }
set officeAreaCode(arg) { this._officeAreaCode = arg }
}
现在使用***搬移字段*** 搬移一个字段
class Person {
get officeAreaCode() { return this._telephoneNumber.officeAreaCode }
set officeAreaCode(arg) { this._telephoneNumber.officeAreaCode = arg }
}
再次运行测试,对下一个字段进行处理
class TelephoneNumber {
get officeNumber() { return this._officeNumber }
set officeNumber(arg) { this._officeNumber = arg }
}
class Person {
get officeNumber() { return this._telephoneNumber.officeNumber }
set officeNumber(arg) { this._telephoneNumber.officeNumber = arg}
}
再次测试,搬移对电话号码的取值函数
class Person {
get telephoneNumber() { return this._telephoneNumber.telephoneNumber }
}
class TelephoneNumber {
get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}` }
}
很显然“电话号码”不应该拥有“办公室”的概念,所以重命名一下变量
class Person {
constructor() {
this._telephoneNumber = new TelephoneNumber()
}
get name() { return this._name }
set name(arg) { this._name = arg }
get officeAreaCode() { return this._telephoneNumber.areaCode }
set officeAreaCode(arg) { this._telephoneNumber.areaCode = arg }
get officeNumber() { return this._telephoneNumber.number }
set officeNumber(arg) { this._telephoneNumber.number = arg }
}
class TelephoneNumber {
get areaCode() { return this._areaCode }
set areaCode(arg) { this._areaCode = arg }
get number() { return this._number }
set number(arg) { this._number = arg }
get toString() { return `(${this.officeAreaCode}) ${this.officeNumber}` }
}
同时 TelephoneNumber
类上有一个对自己 (telephone number)取值也没什么道理,这里使用***函数改名***
class TelephoneNumber {
get toString() { return `(${this.officeAreaCode}) ${this.officeNumber}` }
}
class Person {
get telePhoneNumber() { return this._telePhone.toString() }
}
4. 内联类
内联类与 提炼类 刚好相反。如果一个类不再承担足够的责任,不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),这时候挑选这一“萎缩类”的最频繁用户,以本手法将“萎缩类”塞进另一个类中。
- 对于待内联类(源类)中所有的public函数,在目标类上创建一个对应的函数,新创建的所有函数应该直接委托至源类
- 修改源类public方法所有的引用点,令它们调用目标类对应的委托方法。每次修改后运行测试
- 将源类中的函数与数据全部搬移到目标类,每次修改之后进行测试,直到源类变成空壳为止
- 删除源类
例子:下面这个类存储了一次物流运输(shipment)的若干信息(tracking information)
class TrackingInformation {
get shippingCompany() { return this._shippingCompany }
set shippingCompany(arg) { this._shippingCompany = arg }
get trackingNumber() { return this._trackingNumber }
set trackingNumber(arg) { this._trackingNumber = arg }
get display() {
return `${this.shippingCompany}: ${this.trackingNumber}`
}
}
它作为Shipment(物流)类的一部分被使用
class Shipment {
get trackingInfo() {
return this._trackingInformation.display
}
get trackingInformation() { return this._trackingInformation }
set trackingInformation(aTrackingInformation) {
this._trackingInformation = aTrackingInformation
}
}
TrackingInformation 类过去可能有很多的职责,但是现在已经不能肩负起它的责任,所以要把它内联到Shipment类中
首先需要寻找 TranckingInformation
类的方法有哪些调用点
调用方:
aShipment.trackingInformation.shippingCompany = request.vendor
首先在 Shipment
类中创建一个委托方法,并调整客户端代码,使其调用这个委托方法
class Shipment {
set shippingCompany(arg) { this._trackingInformation.shippingCompany = arg }
}
调用方:
aShipment.shippingCompany = request.vendor
对于 TrackingInformation
类中所有为客户端调用的方法,使用上面相同的手法。之后就可以将源类中的所有东西都搬移到 Shipment
中
先对 display
使用 内联函数
class Shipment {
get trackingInfo() {
return `${this._shippingCompany}: ${this._trackNumber}`
}
}
继续搬移 “收货公司” (shopping company)字段
get shippingCompany() { return this._shippingCompany }
set shippingCompany(arg) { this._shippingCompany = arg }
搬移完所有函数,就可以删除 TrackingInformation
类。
class Shipment {
get trackingInfo() {
return `${this.shippingCompany}: ${this.trackingNumber}`
}
get shippingCompany() { return this._shippingCompany }
set shippingCompany(arg) { this._shippingCompany = arg }
get trackingNumber() { return this._trackingNumber }
set trackingNumber(aTrackingNumber) {
this._trackingNumber = aTrackingNumber
}
}
5. 隐藏委托关系
manager = aPerson.department.manager
===== 重构 =====
manager = aPerson.manager
class Person {
get manager() { return this.department.manager }
}
一个好的模块设计,“封装”即使不是其最关键特征,也是关键特征之一。封装 意味着每个模块都应该尽可能少了解系统的其它部分。如此一来,一旦发生变化,需要了解这一变化的模块就会比较少----这会使变化比较容易进行。
当初我们学面向对象技术的时候就被教导,封装意味着应该隐藏自己的字段。随着经验日渐丰富,你会发现,有更多可以(而且值得)封装的东西。
如果某些客户端先通过服务对象的一个字段得到另一个对象(受托类),然后调用后者的函数,那么客户端就必须知晓这一层委托关系。万一受委托类修改了接口,变化会波及通过服务对象使用它的所有客户端。我们可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即使将来委托关系发生变化,变化也只会影响服务对象,而不会直接波及所有客户端。
- 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数
- 调整客户端,令它只调用服务对象提供的函数。每次调用后运行测试
- 如果将来不再有任何客户端需要取用
Delegate
受托类,便可以移除服务对象中的相关访问函数 - 测试
示例:本例从两个类开始,代表“人”的 Person
和代表 “部门” 的 Department
class Person {
constructor(name) {
this._name = name
}
get name() { return this._name }
get department() { return this._department }
set department(arg) { this._department = arg }
}
class Department {
get chargeCode() { return this.chargeCode }
set chargeCode(arg) { this.chargeCode = arg }
get manager() { return this._manager }
set manager(arg) { this._manager = arg }
}
有些客户端希望知道某人的经理是谁,为此,他必须先取得 Department
对象
客户端代码
mamager = aPerson.department.manage
这样的编码就对客户端揭露了 Department
的工作原理,于是客户知道: Department
负责追踪 “经理” 的信息。如果对客户隐藏 Department
,可以减少耦合。为了实现这一目的,在 Person
中建立一个简单的委托函数
class Person {
get manager() { return this._department.manager }
}
客户端代码:
manager = aPerson.manager
只要完成了对 Department
所有函数的修改,并相应修改了 Person
的所有客户端,我就可以移除 Person
中的 department
访问函数了