以对象取代基本类型(Replace Primitive with Object)
以查询取代临时变量(Replace Temp with Query)
具体展现
// 重构前
organization = { name: "Acme Gooseberries", country: "GB" };
// 重构后
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() { return this._name; }
get country() { return this._country; }
set name(arg) { this._name = arg; }
set country(arg) { this_country = arg; }
}
记录型结构能直观地组织起存在关联的数据,让我可以将数据作为有意义的单元传递,而不仅是一堆数据的拼凑。
对于可变数据,作者更倾向于使用类对象而非记录。对象可以隐藏结构的细节。该对象的用户不必追究存储的细节和计算的过程。同时,这种封装还有助于字段的改名:我可以重新命名字段,但同时提供新老字段名的访问方法,同样就可以渐进地修改调用方,直到替换全部完成。
对于不可变数据,直接将3个值保存在记录里,需要做数据变换时增加一个填充步骤即可。
记录型结构可以有两种类型:一种需要声明合法的字段名字,另一种可以随便用任何字段名字。后者语言自身提供,如散列(hash)、映射(map)、散列映射(hashmap)、字典(dictionary)或关联数组(associative array)等。但使用这类结构也有缺陷,那就是一条记录上持有什么字段往往不够直观。如果这种记录只在程序的一个小范围里使用,那问题不大,但若其使用范围变宽,"数据结构不直观"这个问题就会造成更多困扰。
为了变得更直观,可以使用类更加直接。
在封装集合时,往往会有一个错误:只对集合变量的访问进行了封装,但仍然让取值函数返回集合本身。使得集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。通常避免这种情况,会提供一些修改集合的方法--"添加"和"移除"方法。
避免直接修改集合的方法,可以以某种形式限制集合的访问权,只允许对集合进行读操作。最常见的做法是:为集合提供一个取值函数,但令其返回一个集合的副本。这样即使有人修改了副本,被封装的集合也不会受到影响。
做法
先封装集合的引用,在勒种添加用于"添加集合元素"和"移除集合元素"的函数。查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/移除元素的函数。修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副本。
例子
// 重构前,也许在之前,会认为以下的封装已经做到了重构
class Person {
constructor(name) {
this._name = name;
this._courses = [];
}
get name() { return this._name; }
get courses() { return this._courses; }
set courses(aList) { this._courses = aList; }
}
class Course {
constructor(name, isAdvanced) {
this._name = name;
this._isAdvanced = isAdvanced;
}
get name() { return this._name; }
get isAdvanced() { return this._isAdvanced; }
}
// Person可以使用Course获取课程的相关信息
numAdvancedCourses = aPerson.courses.filter(c => c.isAdvanced).length;
更新列表可以通过
// 方式一:对整个列表更新
const basicCourseNames = readBasicCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name, false));
// 方式二:直接更新课程列表
for (const name of readBasicCourseNames(filename)) {
aPerson.courses.push(new Course(name, false));
}
这样做就破坏了封装性了,更新列表后,Person类无法得知。仅仅封装了字段引用,而未真正封装字段的内容。
为了解决上面说的问题,需要继续重构,为Person类增加两个方法,"添加课程"和"移除课程"的接口。
addCourse(aCourse) {
this._courses.push(aCourse);
}
removeCourse(aCourse, fnIfAbsent = () => { throw new RangeError(); }) {
const index = this._courses.indexOf(aCourse);
if (index === -1) fnIfAbsent();
else this._sourses.splice(index, 1);
}
有了添加和删除方法后,就可以把set courses设置函数删除了。若不能删除,要保证用一个副本给字段赋值,这样不会修改通过参数传入的集合。
set courses(aList) {
this._courses = aList.slice();
}
为了让修改都是通过addCourse和removeCourse来完成,get取值函数返回一个副本,这样获取到courses值时直接push将无效。
get courses() {
return this._courses.splice();
}
如果发现某个数据的操作不仅仅局限于打印时,就会为它创建一个新类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要有类了,日后添加的业务逻辑就有地可去了。
临时变量的一个作用是保存某段代码的返回值,以便在函数的后面部分使用它。临时变量允许引用之前的值,既能解释它的含义,还能避免对代码进行重复计算。
该手法只适用于处理某些类型的临时变量:那些只被计算一次且之后不再被修改的变量。
例子
// 重构前
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
// 重构后
get basePrice() {
return this._quantity * this._itemPrice;
}
// ...
if (this.basePrice > 1000) {
return this.basePrice * 0.95;
} else {
return this.basePrice * 0.98;
}
在实际工作中,类扮演的职责是职责单一的功能抽象。随着功能的扩大,类也会不断地扩大,最后这个类就变得难以维护,不易理解。
如果某些参数和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示应该将它们分离出去。
例子
// 重构前
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; }
}
提炼类的反向操作
使用内联类的原因是如果一个类不再承担足够责任,不再有单独存在的理由。还有一个场景是两个类,想重新安排它们的职责,并让它们产生关联,可以先将它们内联在一个类再用提炼类去分离其职责会更加简单。
模块化设计离不开封装。封装意味着每个模块都应该尽可能少了解系统的其他部分,如对别的对象的方法的使用。
如果某些客户端先通过对象的字段得到另一个对象,然后调用后者的函数,那么客户端就必须知晓这一层委托关系。带来的影响就是,如果后者的函数的接口发生了修改,就将波及到整个客户端的修改,很容易引发错误。这时可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除了这种依赖。随后的修改就只需要对象里修改,不用到整个客户端中修改。
例子
// 重构前
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对象,然后再调用对象中的manager,即
manager = aPerson.department.manager;
如果在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; }
get manager() { return this._department.manager; }
}
那么接下里获取经理的信息,就是
manager = aPerson.manager;
移除中间人是隐藏委托关系的反向重构。
前面讲到了隐藏委托关系带来的好处,但同样会有代价。每当客户端要使用受托类的新特性(即上面提到的后者的函数)时,就必须在服务端添加一个简单的委托函数,随着受托类的特性越来越多,转发函数也就越来越多。服务类完全变成了一个中间人,此时让客户直接调用受托类是更好地选择。
其实无论是隐藏还是移除,是很难评定标准的。标准可以在系统运行过程中不断进行调整。随着代码的变化,"合适的隐藏程度"尺度也相应改变。
对于隐藏委托关系和移除中间人,是可以混合使用的。有些委托关系非常常用,就可以保留下来,这样可以使客户端代码调用更友好。
"重构"可以把一些复杂的东西分解为较简单的小块,但有时就必须删掉整个算法,代之以较简单的算法。
在修改算法之前,可能这个算法是一个巨大且复杂的代码。先尽可能分解原先的函数,只有先将它分解为较简单的小型函数,才能很有把握地进行算法替换工作。
例子
// 重构前
function foundPerson(people) {
for (let i = 0; i < people.length; i++) {
if (people[i] === 'Don') {
return 'Don';
}
if (people[i] === 'John') {
return 'John';
}
if (people[i] === 'Rent') {
return 'Rent';
}
}
return '';
}
// 重构后
function foundPerson(people) {
const candidates = ['Don', 'John', 'Rent'];
return people.find(p => candidates.includes(p) || '');
}