重构·改善既有代码的设计

文章目录

前言

计算机系统是完全人造系统,并不是“本来无一物”,需要“时时勤拂拭”。代码就是需要程序员时时进行整理的。

本文的章节目录和《重构·改善既有代码的设计(第二版)》的目录保持一致。书中所讲内容比较容易理解,并且可操作性比较强,值得一看。

3 代码坏味道

3.1 神秘命名

Mysterious Name

3.2 重复代码

Duplicated Code

如果重复代码只是相似,而不是完全相同。 请先移动语句,调整代码顺序,把相似部分放到一起以便提炼

重复代码在不同子类,函数上移到超类。

3.3 过长函数

提炼函数的时候会出现很多的参数或者临时变量,此种情形,应该使用查询取代临时变量。

提炼函数的信号:注释、条件表达式、switch和循环

3.4 过长参数列表

Long Parameter List

函数组合成类。如果多个函数有同样的几个参数,引入一个类就极为有意义。将参数编程类的字段。

3.5 全局变量

Global Data

封装变量

3.6 可变数据

拆分变量
查询函数和修改函数分离
移除设值函数
以查询取代派生变量
函数组合成变换
将引用对象改为值对象

3.7 发散式变化

Divergent Change

某个模块经常因为不同的原因在不同的方向上发生变化。
重要:“每次只关心一个上下文”

拆分阶段、搬移函数、提炼函数和提炼类

3.8 霰弹式修改

Shotgun Surgey

每当遇到某种变化,都要在许多不同的类内做出许多小修改。需要修改的代码散布四处,不但很难找到它们,也容易错过某个重要的修改。

内联函数、内联类

3.9 依恋情节

Feature Envy

一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于自己所处模块内部的交流。这就是依恋情节的典型情况。

原则:将总是一起变化的东西放在一块儿。

3.10 数据泥团

Data Clumps

常常在很多地方看到相同的三四项数据。两个类中相同的字段、许多函数签名中相同的参数。

3.11 基本类型偏执

Primitive Obsession

很多程序员不愿意创建对自己问题域有用的基本类型,如钱、坐标、范围等。

以对象取代基本类型:将原本单独存在的数据值替换为对象。
以子类取代类型码

3.12 重复的switch

Repeated Switches

以多态取代条件表达式

3.13 循环语句

Loops

以管道取代循环:管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作。

3.14 冗赘的元素

Lazy Element

如果函数的实现和名字一样易读,那么就通过内联,直接干掉这个函数。
如果一个类里面就一个函数,那么这个类也可以英勇就义,通过内联干掉它。
如果一个多余的类处于一个继承体系之中,可以使用折叠继承关系

3.15 夸夸其谈通用性

Speculative Generality

如果抽象类没有太大的作用,请运用折叠继承体系
不必要的委托和函数,就内联,消除掉。
如果函数的某些参数未被用到,改变函数声明去掉这些参数;

如果函数或者类只会被测试用例调用到,这就有了夸夸其谈通用性的坏味道,如果发现了这种函数或者类,可以先删除测试用例,然后移除死代码

3.16 临时字段

Temporary Field

临时字段要想办法挪走。可以提炼类或者搬移函数。
可以使用引入特例,在“变量”不合法的情况下创建一个替代对象,从而避免写出条件式代码。

3.17 过长的消息链

Message Chains

用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象。。。。。。这就是消息链。在实际代码中可能是一长串取值函数或者是一长串临时变量。

隐藏委托关系

3.18 中间人

Middle Man

人们可能会过度使用委托。有时候会看到某个类的接口有一半的函数都委托给其他类。这就是过度应用。

移除中间人:直接和真正负责的对象打交道;
如果不干实事的函数有少数几个,通过内联函数,放入调用端;
如果中间人还有其他行为,可以使用以委托取代超类或者以委托取代子类把它变成真正的对象,如此既可以扩展原对象的行为,又不必负担很多的委托动作。

3.19 内幕交易

Insider Trading

如果两个模块有共同的兴趣,可以用隐藏委托关系,把另一个模块变成两者的中介。

继承常会造成密谋,因为子类对超类的了解总是超过后者的主观愿望。如果你觉得应该让这个孩子独立生活了。 请运用以委托取代子类或者以委托取代超类,让它离开继承体系。

3.20 过大的类

Large Class

提炼类:将几个变量一起提炼至新类内。通常,类内数个变量有着相同的前缀或者后缀,这就意味着有机会把它们提炼到某个组件内。如果此组件适合作为一个子类,提炼超类以子类型取代类型码比较简单。

观察一个大类的使用者,会找到拆分类的线索。看使用者是否只用到了这个类的功能的一个子集,这样,每个子集都能拆分成一个独立的类。

3.21 异曲同工的类

Alternative Classes with Different Interfaces

看看能不能提炼成一样的接口。

3.22 纯数据类

这些类早期有public字段,应该使用封装记录将它们封装起来。

将调用行为搬移到纯数据类里面来。

纯数据类往往意味着行为被放在了错误的地方。只要把处理数据的行为从客户端搬移到纯数据类里面来,就能使情况大为改观。

不可修改的字段无需封装,使用者可以直接通过字段取得数据,无需通过取值函数。

3.23 被拒绝的遗赠

Refused Bequest

如果父类有些数据或者函数,子类不想继承。一般会新建一个兄弟类,将不要的成员下移推给兄弟类。

如果子类复用了超类的行为,却又不愿意支持超类的接口。此时坏味道浓烈了。 此时需要以委托取代子类或者以委托取代超类

3.24 注释

常常会有这样的情况:你看到一段代码有长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕。

当你感觉需要编写注释时,请先尝试重构,试着让所有注释都变得多余

6 第一组重构

6.1 函数组合成类

Combine functions into class

function base(){}
function fun1(){}
function fun2(){}

====>
class class1{
  founction base(){}
  function fun1(){}
  function fun2(){}
}

6.2 内联函数

如果函数代码和函数的名称同样清晰易读,此时,应该去掉这个函数,直接使用其内部代码。通过内联手法,找出有用的中间层,将无用的中间层去除。

function getRating(driver) {
return moreThanFiveLateDelivers(driver)? 2 : 1;
}
function moreThanFiveLateDelivers(driver){
return driver.numberOfLateDelivers > 5;
}

===== >
function getRating(driver) {
return (driver.numberOfLateDelivers > 5) ? 2 : 1;
}

6.3 提炼变量

Extract Variable

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(order.quantity * order.itemPrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;

表达式有可能非常复杂,引入局部变量可以有助于复杂逻辑的理解。

如果变量名在更宽的上下文中也有意义,可以考虑将其暴露出来,这通常是以函数的方式。

6.4 内联变量

如果表达式本身就很简单了,就没有必要定义一个变量了,直接干掉它。

6.5 改变函数声明

function circum(radius){}
====>
function circumference(radius){}

发现了更好的函数名,要马上改名。(函数改名的好办法:先写一句注释来描述这个函数的用途,再把这句注释变成函数的名字)。

6.6 封装变量

let defaultOwner = {firstName:"Martin", lastName:"Fowler"}

====>

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

封装数据价值很大。对于所有可变的数据,只要它的作用域超过单个函数,就将其封装起来。

成员private也是这种思路。 通过get set方法对数据进行修改。

6.7 变量改名

let a = height * width;
====>
let area = height * width

好的命名是clean code的核心。

6.8 引入参数对象

Introduce Parameter Object

function amountInvoiced(startDate, endDate){...}
function amountReceived(startDate, endDate){...}
function amountOverdue(startDate, endDate){...}
====>
function amountInvoiced(aDateRange){...}
function amountReceived(aDateRange){...}
function amountOverdue(aDateRange){...}

用一个数据结构取代数据泥团。

6.9 函数组合成类

function base(aReading){}
function taxableCharge(aReading){}
function calculateBaseCharge(aReading){}
====>
class Reading {
  base(){...}
  taxableCharge(){...}
  calculateBaseCharge(){...}
}

如果发现一组函数形影不离地操作者同一块数据(通常是将这块数据作为参数传递给函数),那就是时候组建一个类了。

使用类有一大好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会自动与核心数据保持一致。

6.10 函数组合成变换

Combine functions into Transform

所有计算派生数据的逻辑收拢到一处

function base(aReading){}
function taxableCharge(aReading){}

====>

function enrichReading(argReading) {
const aReading = _.cloneDeep(argReading);
aReading.baseCharge = base(aReading);
aReading.taxableCharge = taxableCharge(aReading);
}

6.11 拆分阶段

Split Phase

const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
====>
const orderRecord = parseOrder(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[order.productId];
}

看见一段代码在处理不同的事情,就要拆分成各自独立的模块。

7 封装

7.1 封装记录

organization = {name:"Acme", country:"GB"}
====>
class Organization {
	constructor(data){
    this._name = data.name;
    this._country = data.country;
   }
   get name(){}
   set name(){}
   get country() {}
   set country() {}
}

7.2 封装集合

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){...}
}

封装程序中所有可变数据,这样可以看清数据的修改点和修改方式,这样在更改数据结构时会非常方便。

不要让集合的取值函数返回原始集合,这就避免了客户端的意外修改。

以某种形式限制集合的访问权,只允许对集合进行读操作。Java中可以直接返回一个集合的只读代理,这种代理允许用户读取集合,但会阻止所有更改操作——Java代理会抛出一个异常。

7.3 以对象取代基本类型

oders.filter(o => "high" === o.priority 
    || "rush" === o.priority)
========>
oders.filter(o => o.priority.higherThan(new Priority("normal"));

开发初期,会以简单的数据项表示简单的情况,后面会发现,简单的数据项不再简单。

一旦发现对某个数据的操作不仅仅局限于打印时,此时可以创建一个新类。

7.4 以查询取代临时变量

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 (this.basePrice > 1000) {
    return this.basePrice * 0.95;
} else {
	return this.basePrice * 0.98;
}

将变量抽取到函数之中,能使函数的分解过程更加简单。有助于划清各个函数之间的边界。

此项重构手法在类内施展效果最好,因为类为待提炼函数提供了一个共同的上下文。

7.5 提炼类

某些数据和函数总是一起出现,某些数据同时变化甚至彼此相依,此时就应该分离出去。

如果你发现子类化只影响类的部分特性,或者你发现某些特性需要以一种方式来子类化,某些特性需要以另一种方式子类化,这就意味着需要分解原来的类。

7.6 内联类

Inline Class

就是一个类不足以成为一个类的时候。 就应该把它的相关内容放到其他的类里面去。

class Person{
get officeAreaCode() {return this._telephoneNumber.areaCode}
get officeNumber() {return this._telephoneNumver.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};
}

7.7 隐藏委托关系

Hide Delegate

本质上就是增加中间人。

manager = aPerson.department.manager

======>
manager = aPerson.manager

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

隐藏字段扩展到隐藏委托关系。

7.8 移除中间人

隐藏委托关系反过来。

尺度自己把握,到底是需要中间人还是移除中间人。

7.9 替换算法

Substitute Algorithm

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] === "Kent") {
    return "Kent";
   }
 }
 return "";
}

==============> 
function foundPerson(people) {
	const candidates = ["Don", "John", "Kent"];
   return people.find(p => candidate.include(p)) || '';
}

发现做一件事情有更清晰的方式,就可以毫不犹豫地替换掉原来的复杂的做法。

使用这项重构手法之前,需要确保已经分解了原来的函数。替换一个巨大复杂的算法是非常困难的。只有分解小了,才能有把握地进行替换工作。

8 搬移特性

8.1 搬移函数

Move function

任何函数都需要具备上下文环境才能存活。

搬移函数的一个直接动因:频繁引用其他上下文中的元素,而对自身上下文中的元素关心甚少。此时,就应该让它去和更亲密的那些元素去相会。

抽取common的util函数。

8.2 搬移字段

Move Field

数据结构是一个健壮的程序的根基。坏的数据结构会掩藏程序的真实意图。

8.3 搬移语句到函数

Move Statements info Function

如果我发现调用某个函数时,总有一些相同的代码也需要每次执行,name我会考虑将此段代码合并到函数里面。

如果将来代码对不同的调用者需要有不同的行为,那时可以搬移语句到调用者

8.4 搬移语句到调用者

函数边界发生偏移的一个征兆是:以往在多个地方共用的行为,如今需要在某些调用点面前表现出不同的行为。 此时,我们需要将表现不同的行为从函数里面挪出,搬移到其调用处。

8.5 以函数调用取代内联代码

Replace Inline Code with Function Call

let appliesToMass = false;
for (const s of states) {
	if (s == "MA") appliesToMass = true;
}

======>
appliesToMass = states.include("MA");

就是要合理地封装函数

8.6 移动语句

Slide Statements

让存在关联的东西一起出现,可以使代码更容易理解。

有人喜欢在函数顶部一口气声明函数用到的所有变量,我个人则喜欢在第一次需要使用变量的地方声明它

8.7 拆分循环

Split Loop

let avarageAge = 0;
let totalSalary = 0;
for (const p of people) {
    avarageAge += p.age;
    totalSalary += p.salary;
}
avarageAge = avarageAge / people.length

=====>

let totalSalary = 0;
for (const p of people) {
    totalSalary += p.salary;
}

let avarageAge = 0;
for (const p of people) {
    avarageAge += p.age;
}
avarageAge = avarageAge / people.length

身兼多职的循环非常常见。

如果拆分循环之后影响了性能,可以再将循环进行合并。但是实际情况是,即时处理的列表数据更多一些,循环本身也很少成为性能瓶颈,更何况拆分出来的循环通常还使一些更强大的优化手段变得可能。

8.8 以管道取代循环

Replace Loop with Pipeline

集合管道(collection pipeline)是这样一种技术,它允许我们使用一组运算来描述集合的迭代过程,每种运算的入参和返回值都是一个集合。

常见的运算就是filter和map。filter运算是指用一个函数从输入集合中筛选出符合条件的元素子集的过程。运算得到的集合可以供管道的后续流程使用;map运算是指用一个函数作用于输入集合的每一个元素上,将集合变换成另外一个集合的过程。

const names = [];
for (const i of input) {
	if (i.job == "programmer") {
   		names.push(i.name); 
   }
}
====>
const names = input
    .filter(i => i.job == "programmer")
    .map(i => i.name)

8.9 移除死代码

一旦代码不再被使用,就应该马上删除它。就算是真的再需要使用这段代码,可以从版本控制系统里面找出。在版本控制系统普遍使用的今天,无用代码可以放心清理。

9 重新组织数据

9.1 拆分变量

Split Variable

let temp = 2 * (height + width)
console.log(temp);
temp = height * width
console.log(temp);

====>

const perimeter = 2 * (height + width)
console.log(perimeter);
const area = height * width
console.log(area);

如果可能的话,将新的变量声明为不可修改。

9.2 字段改名

Rename Field

字段保持良好的命名

9.3 以查询取代派生变量

Replace Derived Variable with Query

get discountedTotal() {return this._discountedTotal}
set discount(aNumber) {
const old = this._discount;
this._discount = aNumber;
this._discountedTotal = old - aNumber;
}

======>

get discountedTotal() {return this._baseTotal - this._discount}
set discount(aNumber) {
this._discount = aNumber;
}

9.4 将引用对象改为值对象

这个就是将成员重新new出来

class Product{
	applyDiscount(arg) {this._price.amount -= arg}
}

====>
class Product{
   applyDiscount(arg) {
    this._price = new Money(this._price.amount - arg, this._price.currency);
   }
}

把一个对象嵌入到另一个对象的时候,内部的对象既可以被视作值对象,也可以被视作引用对象。
如果被视为引用对象:更新其属性时, 保留原对象不动,更新内部对象的属性。如果在几个对象之间共享一个对象,以便几个对象都可以看见共享对象的修改,那么这个共享的对象就应该是引用。
如果被视为值对象:替换整个内部对象,新换上的对象具有新的想要的属性值。

9.5 将值对象改为引用对象

Change Value to Reference

把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着我会需要某种形式的仓库,在仓库中可以找到所有的这些实体对象。只为每个实体创建一次对象,以后始终从仓库中获取该对象。

let customer = new Customer(customaData);

=========>

let customer = customerRepository.get(customerData.id);

10 简化条件逻辑

10.1 分解条件表达式

if (condition1 && condition2 && condition3) {
	sentence1;
   sentence2;
   ......
} else {
	else sentence1;
   else sentence2;
   ......
}

====>
if (condition()) {
	if_function();
} else {
	else_function();
}

10.2 合并条件表达式

Consolidate Conditional Expression

if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
======>
if (isNotEligibleForDisability()) return 0;

function isNotEligibleForDisability() {
return anEmployee.seniority < 2
   || anEmployee.monthsDisabled > 12
   || anEmployee.isPartTime
}

常会有一长串的这样的条件检查:检查条件各不相同,最终行为却一致。如果出现这种情况就用与或合并为一个条件表达式。

合并条件代码的原因:(1)“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”。不合并的语义是“这里有一些各自独立的条件测试,只不过是同时发生而已”(2)为提炼函数做准备

如果条件判断缺失彼此独立,就不应该合并条件表达式。

10.3 以卫语句取代嵌套条件表达式

Replace Nested Conditional with Guard Clauses

条件表达式通常有两种风格。第一种风格:两个条件分支都属于正常行为。这种就应该使用if else。第二种风格:只有一个条件分支是正常行为,其余分支都是异常情况。罕见条件单独检查,条件为真时立刻从函数中返回。

卫语句就是给某一条分支特别的重视。这种情况不是本函数核心逻辑所关心的。

10.4 以多态取代条件表达式

10.5 引入特例

Introduce Special Case

if (aCustomer == "unkown") customerName = "occupant"
====>
class UnkownCustomer {
    get name() {return "occupant";}
}

引入Null对象。一个通常处理的值就是null。“Null对象”模式

10.6 引入断言

Introduce Assertion

代码假设通常不会在代码中明确表现出来。

断言可以明确说明代码的假设情况。便于debug。

平时很少看到断言。 慎用。

11 重构API

11.1 将查询函数和修改函数分离

“读写分离”

11.2 函数参数化

function tenPercentRaise(aPerson) {
    aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
    aPerson.salary = aPerson.salary.multiply(1.05);
}

========>
function tenPercentRaise(aPerson, factor) {
    aPerson.salary = aPerson.salary.multiply(1 + factor);
}

如果两个函数逻辑非常相似,仅仅是字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。

11.3 移除标记参数

function setDimention(name, value) {
    if (name == "height") {
        this._height = value;
        return
    }
    if (name == "width") {
        this._width = value;
        return
    }
}

===========>
function setHeight(value) {this._height = value}
function setWidth(value) {this._width = value}

只有参数值影响了函数内部的控制流,这才是标记参数。

标记参数隐藏了函数调用中存在的差异性,令人难以理解有哪些函数可以调用,应该怎么样调用

11.4 保持对象完整

int low = room.dayTemprature.low
int high = room.dayTemprature.high

if (aPlan.withRange(low, high))
      ====>
if (aPlan.withRange(room.dayTemprature))

11.5 以查询取代参数

Repace Parameter with Query

availableVacation(anEmployee, anEmployee.grade);

function availableVacation(anEmployee, grade){
	// calculate vacation
}

===================>
availableVacation(anEmployee);


function availableVacation(anEmployee){
    const grade = anEmployee.grade;
    // calculate vacation
}

函数的参数列表应该总结函数的可变性,标示出函数可能体现出行为差异的主要方式。参数列表应该尽量避免重复,并且参数列表越短越容易理解。

如果调用函数时传入了一个值,而这个值由函数自己来获得也同样容易,这就是重复。

去除参数意味着“获得正确的参数值”的责任被转移。一般而言,我习惯简化调用方

移除参数可能会给函数体增加不必要的依赖关系。所以这里还是要慎用。

如果想要去除的参数值只需要向另一个参数查询就能得到,这就是以查询取代参数的最安全的使用场景。

如果处理的函数具有引用透明性(referential transparency,即,不论任何时候,只要传入相同的参数值,该函数的行为永远一致),这种函数既容易理解又容易测试,不应该令其失去这种优秀的品质。

11.6 以参数取代查询

Replace Query with Parameter

targetTemprature(aPlan)

funciton functionTemprature(aPlan) {
    currentTemprature = thermoast.currentTemprature;
    // rest of function
}
======>
targetTemprature(aPlan, thermoast.currentTemprature)

funciton functionTemprature(aPlan, currentTemprature) {
}

在函数中可能存在一些令人不快的引用关系。例如,引用一个全局变量,或者引用一个想要移除的元素。此处,可以将这些令人不快的引用替换为函数参数,将处理引用关系的责任转交给函数的调用者。

如果一个函数使用了另一个元素,而后者不具有引用透明性,name包含该元素的函数也就失去了引用透明性。只要把“不具有引用透明性的元素”变成参数传入,函数就能重新获得引用透明性。

有一个常见的模式:在负责逻辑处理的模块中只有纯函数,其外,再包裹处理I/O和其他可变元素的逻辑代码。借助以参数取代查询,可以提纯程序中的某些组成部分,使其更容易测试、更容易理解。

11.7 移除设置函数

如果不希望字段被修改,就声明为不可变,并且不开放set方法

11.8 以工厂函数取代构造函数

Replace Constructor with Factory Function

leadEngineer = new Emplyee(document.leadEngieneer, 'E')
====================>
leadEngineer = createEngineer(document.leadEngineer)

与一般函数相比,构造函数有一些丑陋的局限性。例如,Java的构造函数只能返回当前所调用类的实例,也就是说,无法根据环境或者参数信息返回子类实例或代理对象;构造函数的名字是固定的,因此无法使用比默认名字更清晰的函数名;构造函数需要通过特殊的操作符来调用,所以在要求普通函数的场合就难以使用。

11.9 以命令取代函数

function score (param1, param2 ... paramn)

class Scorer {
	public Scorer(param1, param2 ... paramn){
    this.param1 = param1
    ......
   }
   
   execute() {
   }
}

11.10 以函数取代命令

Replace Command with Function

class ChargeCalculator {
    constructor (customer, usage) {
        this._customer = customer;
        this._usage = usage;
    } 
    
    execute() {
       return this._customer.rate * this._usage;
    }
}
============================================>
function charge(custormer, usage) {
    return customer.rate * usage;
}

命令对象为处理复杂计算提供了强大的机制。借助命令对象,可以轻松将原本复杂的函数拆解为多个方法,彼此之间通过字段共享状态;拆解后的方法可以分别调用;开始调用之前的数据状态也可以逐步构建。这种强大是有代价的。 如果就是调用一个函数,并且这个函数并不复杂,那么命令对象就会显得费而不惠,此时,应该将其变回普通函数。

12 处理继承关系

12.1 函数上移

Pull Up Method

子类函数体都相同(往往是复制粘贴出现的这种情况)。上移到超类

函数上移往往紧随其他重构发生。比如:(1)子类函数大体一致。可以通过函数参数化调整为相同的函数,然后函数上移。(2)字段上移(3)如果函数工作流程大体相似,但是实现细节存在差异。先考虑塑造模板函数

12.2 字段上移

Pull Up Field

字段上移可以从两方面减少重复:(1)去除了重复的声明。(2)会带动使用字段的行为进行上移

许多动态语言不需要在类定义中定义字段,相反,字段是在第一次被赋值的同时完成声明。在这种情况下,字段上移基本上是应用构造函数本体上移后的必然结果。

12.3 构造函数本体上移

Pull up Constructor Body

class Party{...}

class Employee extends Party {
    constructor(name, id, monthlyCost) {
        this._id = id;
        this._name = name;
        this._monthlyCost = monthlyCost;
    }
}
==============================>
class Party {
    constructor(name) {
        this._name = name;
    }
}

class Employee extends Party{
    constructor(name, id, monthlyCost) {
        super(name);
        this._id = id;
        this._monthlyCost = monthlyCost;
    }
}

12.4 函数下移

如果超类中的某个函数只与一个或者少数几个子类有关,最好将其从超类中挪走。

函数下移只有在超类明确知道哪些子类需要这个函数时适用。如果超类不知晓这个信息,需要使用以多态取代条件表达式,只保留公共的行为在超类中。

12.5 字段下移

如果超类中的某个字段只与一个或者少数几个子类有关,最好将其从超类中挪走。

12.6 以子类取代类型码

Replace Type Code with SubClass

function createEmployee(name, type) {
    return new Employee(name, type);
}
====>
function createEmployee(name, type) {
	switch(type){
    case "engineer": return new Engineer(name);
    case "salesman": return new Salesman(name);
    case "manager": return new Manager(name);
   }
}

12.7 移除子类

Remove Subclass

随着软件的演化,子类所支持的变化可能会被搬移至别处,甚至完全去除,这是子类就失去了价值;有时添加子类是为了应对未来的功能,但是设想中的功能可能压根没有被构造出来,或者使用了其他方式进行构造,此时子类也不被需要了。

子类的存在有成本,读者要花费心思去理解其用意。如果用处太少。删了算了。

12.8 提炼超类

如果两个类在做相同的事情,就可以将相同的成员上移至超类。

合理的继承关系是在程序演化过程中才浮现出来的。

12.9 折叠继承体系

Collapse Hierarchy

随着继承体系的演化,我们有时会发现一个类与其超类已经没有多大的区别了,不值得再作为独立的类存在。此时就需要把超类和子类合并。

12.10 以委托取代子类

Replace Subclass with Delegate

class Order {
 get daysToShip() {
     return this._warehouse.daysToShip;
 }
}
class PriorityOrder extends Order {
 get daysToShip() {
   return this._priorityPlan.daysToShip;
 }
}

======>
class Order {
    get daysToShip() {
    	return (this._priorityDelegate)?
        ? this._priorityDelegate.daysToShip
        : this._warehouse.daysToShip;
    }
}
class PriorityDelegate {
	get daysToShip() {
    return this._priorityPlan.daysToShip;
    }
}

继承尤其短板。最明显的就是继承这张牌只能打一次。导致行为不同的原因有很多种,但继承只能用于处理一个方向上的变化。比如,我希望人的行为可以根据年龄段进行变化,也可以根据收入水平的变化进行变化。使用继承,子类可以是“年轻人”和“老人”,也可以“富人”和“穷人”,但是不能采用两种继承方式。

“对象组合”优于类继承。

使用“状态模式”或者是“策略模式”取代子类。 可通过这两种设计模式理解此重构手法。

12.11 以委托取代超类

class List{}
class Stack extends List{}
=====>
class Stack {
	constructor() {
   		this._storage = new List(); 
   }
}
class List{}

如果超类的一些函数对于子类并不适用,说明就不应该通过继承来获得超类的功能。

合理的继承关系还有一个重要的特征:子类的所有实例都应该是超类的实例,通过超类的接口来使用子类应该完全不出问题。

使用委托能更清晰的表达“这是另一个东西,我只是需要用到其中的一些功能”这层意思。

首先尽量先使用继承,如果发现继承有问题,再使用以委托取代超类

4 构筑测试体系

确保所有测试都完全自动化,让它们检查自己的测试结果。(窃以为,有很大困难,涉及UI和动效的测试怎么自动化呢?)

在添加特性之前,会编写相应的测试代码。先写测试会强迫自己去明确需求,明确需要实现什么,有助于把注意力集中到接口,而非功能实现。(这个是很有道理的,开发和测试分开的大环境下,不可能实现。)

总是确保测试不该通过时真的会失败。编写测试时,要看到每个测试都至少失败一遍。

频繁地运行测试。正在处理的代码,其对应的测试应该几分钟运行一次。每天至少运行一次所有的测试。

“看到红条时永远不许进行重构”“回退到绿条”

测试的重点应该是最担心出错的地方。

4.6 探测边界条件

“正常路径”(happy path)

集合为空、负值

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值