《重构 改善既有代码的设计》(第七章)【封装】

封装记录(Encapsulate Record)

封装集合(Encapsulate Collection)

以对象取代基本类型(Replace Primitive with Object)

以查询取代临时变量(Replace Temp with Query)

提炼类(Extract Class)

内联类(Inline Class)

隐藏委托关系(Hide Delegate)

移除中间人(Remove Middle Man)

替换算法(Substitute Algorithm)

封装记录

具体展现

// 重构前
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) || '');
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值