《重构》读后感(第七章)

第七章:封装。

分解模块时最重要的标准,也许就是识别出那些模块应该对外界隐藏的小秘密了。

一、封装记录
动机:
记录型结构能直观地组织起存在关联的数据,让我可以将数据作为有意义的单元传递,而不仅是一堆数据的拼凑。

对于可变数据,作者更倾向于使用类对象而非记录。对象可以隐藏结构的细节。该对象的用户不必追究存储的细节和计算的过程。同时,这种封装还有助于字段的改名:我可以重新命名字段,但同时提供新老字段名的访问方法,同样就可以渐进地修改调用方,直到替换全部完成。

对于不可变数据,直接将值保存在记录里,需要做数据变换时增加一个填充步骤即可。

记录型结构可以有两种类型:一种需要声明合法的字段名字,另一种可以随便用任何字段名字。后者语言自身提供,如散列(hash)、映射(map)、散列映射(hashmap)、字典(dictionary)或关联数组(associative array)等。但使用这类结构也有缺陷,那就是一条记录上持有什么字段往往不够直观。如果这种记录只在程序的一个小范围里使用,那问题不大,但若其使用范围变宽,”数据结构不直观”这个问题就会造成更多困扰。

具体展现:

// 重构前
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; }
}

二、封装集合
动机:
使用面向对象技术的开发者对封装尤为重视。但在封装集合时,往往会有一个错误:只对集合变量的访问进行了封装,但仍然让取值函数返回集合本身。使得集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。通常避免这种情况,会提供一些修改集合的方法:”添加”和”移除”方法,这样就可使对集合的修改必须经过类。

避免直接修改集合的方法,可以以某种形式限制集合的访问权,只允许对集合进行读操作。最常见的做法是:为集合提供一个取值函数,但令其返回一个集合的副本。这样即使有人修改了副本,被封装的集合也不会受到影响。

具体展现:

// 重构前
class Person {
    get courses() { return this._courses; }
    set courses(aList) { this._courses = aList; }
 }

// 重构后
class Person {
    get courses() { return this._courses.slice(); }
    addCourse(aCourse)       { ... }
    removeCourse(aCourse)    { ... }
}

三、以对象取代基本类型
动机:
举例说明:开发初期使用一个字符串来表示“电话号码”,但随着不断开发,它又需要“格式化”和“抽取区号”等特殊行为,这类逻辑很快就制造出许多重复代码,增加使用成本。

如果发现某个数据的操作不仅仅局限于打印时,就为它创建一个新类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要有类了,日后添加的业务逻辑就有地可去了。

具体展现:

// 重构前
orders.filter(o => "high" === o.priority || "rush" === o.priority );

// 重构后
orders.filter(o => o.priority.higherThan(new Priority("normal")));

四、以查询取代临时变量
动机:
临时变量的一个作用是保存某段代码的返回值,以便在函数的后面部分使用它。临时变量允许引用之前的值,既能解释它的含义,还能避免对代码进行重复计算。

该手法只适用于处理某些类型的临时变量:那些只被计算一次且之后不再被修改的变量。

具体展现:

// 重构前
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 {
  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 {
  get officeAreaCode() { return this._officeAreaCode; }
  get officeNumber() { return this._officeNumber; }
}

七、隐藏委托关系
动机:
如果某些客户端先通过对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户端就必须知晓这一层委托关系。如果受托类修改了接口,就将波及到整个客户端的修改,很容易引发错误。这时可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除了这种依赖。这么一来,即使将来委托关系发生变化,变化也只会影响服务对象,而不会直接波及所有客户端。

具体展现:

// 重构前
manager = aPerson.department.manager;

// 重构后
manager = aPerson.manager;

class Person {
  get manager() { return this.department.manager; }
}

八、移除中间人
动机:
前面讲到了隐藏委托关系带来的好处,但同样会有代价。每当客户端要使用受托类的新特性时,你就必须在服务端添加一个简单的委托函数,随着受托类的特性越来越多,转发函数也就越来越多。服务类完全变成了一个中间人,此时让客户直接调用受托类是更好地选择。

很难说什么程度的隐藏才是合适的。对于隐藏委托关系和移除中间人,是可以混合使用的。有些委托关系非常常用,就可以保留下来,这样可以使客户端代码调用更友好。

具体展现:

// 重构前
manager = aPerson.manager;

class Person {
  get manager() { return this.department.manager; }
}

// 重构后
manager = aPerson.department.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) || '');
}