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

第七章:封装。

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

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

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

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

记录型结构可以有两种类型:一种需要声明合法的字段名字,另一种可以随便用任何字段名字。后者语言自身提供,如散列(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) || '');
}

《重构》读后感(第五章,第六章)

第五章:介绍重构名录。

本章没有过多内容,主要是对本书后续重构内容的结构的说明。
后边介绍的重构手法,每个都有如下五部分:
1、名称:要建造一个重构词汇表,名称是很重要的。
2、速写:这部分可以帮助你更快找到你所需要的重构手法。
3、动机:为你介绍“为什么要做这个重构”和“什么情况下不该做这个重构”
4、做法:简明扼要的一步一步介绍如何进行此重构。
5、范例:以一个十分简单的例子来说明此重构手法如何运作。

这份重构名录只是记录那些作者认为最值得记录的重构手法。因为它们都是最常用的,值得命名和介绍,有些重构没有收录是因为太小,太简单,没必要多加赘述,还有一些是因为用的很少,或者于其他的重构手法相似。
对于后文将要讲到的每个重构,逻辑上来说,都有一个反向重构。

第六章:第一组重构(最有用的重构)。

一、提炼函数
反向重构:内联函数
动机:【将意图与实现分开】。如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,一眼就能看出这个函数的用途,而不需要关心内部如何实现。

具体展现:

// 重构前
function printOwing(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();

    //print details
    console.log('name: ${invoice.customer}');
    console.log('amount: ${outstanding}');
}

// 重构后
function printOwing(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();
    printDetails(outstanding);

    function printDetails(outstanding) {
        console.log('name: ${invoice.customer}');
        console.log('amount: ${outstanding}');
    }
}

二、内联函数
反向重构:提炼函数
动机:1、函数内部代码和函数名称一样清晰易读。2、一群组织不甚合理的函数,可以将它们都内联到一个大型函数中,然后再提炼。3、代码有太多的间接层(委托),不是所有间接层都有价值。

具体展现:

// 重构前
function getRating(driver) {
    return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver) {
    return driver.numberOfLateDeliveries > 5;
}

// 重构后
function getRating(driver) {
    return (driver.numberOfLateDeliveries > 5) ? 2 : 1;
}

三、提炼变量
反向重构:内联变量
动机:表达式可能非常复杂而难以阅读,这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。

具体展现:

// 重构前
return order.quantity * order.itemPrice - 
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + 
Math.min(order.quantity * order.itemPrice * 0.1, 100);

// 重构后
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100)
return basePrice - quantityDiscount + shipping;

四、内联变量
反向重构:提炼变量
动机:有时候变量名并不比表达式本身更具表现力,还有些时候,变量可能会严重妨碍重构附近的代码。

具体展现:

// 重构前
let basePrice = anOrder.basePrice;
return (basePrice > 1000);

// 重构后
return anOrder.basePrice > 1000;

五、改变函数声明
别名:函数改名
动机:函数是我们将程序拆成小块的主要方式。函数声明则展示了如何讲这些小块组合在一起工作,因此函数的名字和其参数就非常重要。

具体展现:

// 重构前
function circum(radius) { ... }

// 重构后
function circumference(radius) { ... }

六、封装变量
动机:函数只有一种用法,就是调用。数据就要麻烦很多,因为没有转发机制。把数据移走就必须修改所有对其的引用代码。当数据的可访问范围很小时,还不成问题。但如果可访问范围变大,重构难度就随之增大,这也就是为什么说全局数据是大麻烦的原因。
所以,要搬移一处被广泛使用的数据,最好的办法就是先以函数的形式封装所有对该数据的访问,这样就把“重新组织数据”的困难任务转化为了“重新组织函数”这个相对简单的任务。另外:封装数据能提供一个清晰的观测点,可由此监控数据的变化和使用情况。

具体展现:

// 重构前
let defaultOwner = {firstName: "Martin", lastName: "Fowler"};

// 重构后
let defaultOwnerData = {firstName: "Martin", lastName: "Fowler"};
export function defaultOwner()       { return defaultOwnerData; }
export function setDefaultOwner(arg) { defaultOwnerData = arg; }

七、变量改名
动机:好的命名是整洁代码的核心。变量可以很好的解释一段程序在干什么。适用范围越广,名字的好坏就越重要。

具体展现:

// 重构前
let a = height * width;

// 重构后
let area = height * width;

八、引入参数对象
动机:一组数据项总是结伴同行,出没于一个又一个函数,这就是所谓的数据泥团,将数据组织成结构就是一件有价值的事情,它会催生代码中更深层次的改变,改变代码的概念图景,将这些数据结构提升为新的抽象概念,可以帮助我们更好的理解问题域。

具体展现:

// 重构前
function amountInvoiced(startDate, endDate) { ... }
function amountReceived(startDate, endDate) { ... }
function amountOverdue(startDate, endDate) { ... }

// 重构后
function amountInvoiced(aDateRange) { ... }
function amountReceived(aDateRange) { ... }
function amountOverdue(aDateRange) { ... }

九、 函数组合成类
动机:如果发现一组函数形影不离的操作同一块数据,就可以组建一个类了。类能明确的给这些函数提供一个共用的环境,在对象内部调用就可以少传很多参数,简化函数调用,同时也可以更方便的传递给系统的其他部分。这个重构还给我们一个机会,去发现其他的计算逻辑,将它们也重构到这个类当中。

具体展现:

// 重构前
function base(aReading) { ... }
function texableCharge(aReading) { ... }
function calculateBaseCharge(aReading) { ... }

// 重构后
class Reading {
    base() { ... }
    texableCharge() { ... }
    calculateBaseCharge() { ... }
}

十、函数组合变换
动机:在软件中,经常需要把数据“喂”给一个程序,让它再计算出各种派生信息。这些派生数值可能会在不同的地方用到,因此这些计算逻辑经常在用到派生数据的地方重复。这样,我们就可以把所有计算派生数据的逻辑收拢到一处,以后可以在固定的地方找到和更新这些逻辑,避免重复。
函数组合变换的替代方案是上一条的内容:函数组合成类。不过两者有一个重要的区别:如果代码中会对源数据做更新,那么使用类更好一些;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,就会遭遇数据不一致。

具体展现:

// 重构前
function base(aReading) { ... }
function texableCharge(aReading) { ... }

// 重构后
function enrichReading(argReading) {
    const aReading = _.cloneDeep(argReading);
    aReading.baseCharge = base(aReading);
    aReading.texableCharge = texableCharge(aReading)
    return aReading;
}

十一、拆分阶段
动机:一段代码在同时处理两件不同的事。

具体展现:

// 重构前
const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;

// 重构后
const orderRecord = priceOrder(order);
const orderPrice = price(orderRecord, priceList);

function parseOrder(aString) {
    const values = aString.split(/\s+/);
    return ({
        productID: values[0].split("-")[1]],
        quantity: parseInt(values[1]),
    });
}

function price(order, priceList) {
    return order.quantity * priceList[orderData.productID];
}