【重构系列】重构速查小手册

2,431 阅读31分钟

前言

该文章继续接上篇 什么时候需要进行代码重构? ,自己把《重构》中所有的手法全部整理成一篇文章,方便自己也方便他人快速查找,因为篇幅的问题,手法中未添加示例代码,如果你能坚持阅读完毕,相当于你重读了一遍该书精华。😄


提炼函数

操作做法

  • 创造一个新函数,根据这个函数的意图来对它命名。
  • 将待提炼的代码从源函数复制到新建的目标函数中。
  • 仔细检查提炼出的代码,看看其中是否引用了作用域限制源函数、在提炼出的新函数中访问不到的变量。
  • 所有变量处理完毕后,编译。
  • 在源函数中,将被提炼代码段替换为对目标函数的调用。
  • 查看其它代码是否有与被提炼的代码段相同或相似之处。

代码展示

原代码

function printOwin(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();
    
    // print details
    console.log(`name: ${invoice.customer}`);
    console.log(`amount: ${outstanding}`);
}

新代码

function printOwin(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();
    
    printDetails(invoice, outstanding);
}

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

内联函数

操作做法

  • 检查函数,确定它不具多态性。
  • 找出这个函数的所有调用点。
  • 将这个函数的所有调用点都替换为函数本体。
  • 每次替换之后,执行测试。
  • 删除该函数的定义。

代码展示

原代码

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(order.quantity * order.itemPrice * 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 defaultOwner = {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 amountInvoiced(aDateRange) {}

函数组合成类

操作做法

  • 运用封装记录对多个函数共用的数据记录加以封装。
  • 对于使用该记录结构的每个函数,运用搬移函数将其移入新类。
  • 用以处理该数据记录的逻辑可以用提炼函数提炼出来,并移入新类。

代码展示

原代码

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

新代码

class Reading {
    base() {}
    taxableCharge() {}
    calculateBaseCharge() {}
}

函数组合成变换

操作做法

  • 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
  • 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
  • 测试。
  • 针对其他相关的计算逻辑,重复上述步骤。

代码展示

原代码

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

新代码

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

拆分阶段

操作做法

  • 将第二阶段的代码提炼成独立的函数。
  • 测试。
  • 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
  • 测试。
  • 逐一检查提炼出的“第二阶段函数”的每个参数。
  • 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构。

代码展示

原代码

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];
}

封装记录

操作做法

  • 对持有记录的变量使用封装变量,将其封装到一个函数中。
  • 创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。测试。
  • 新建一个函数,让它返回该类的对象,而非那条原始的记录。
  • 对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对象的函数调用。
  • 移除类对原始记录的访问函数,那个容易搜索的返回原始数据的函数也要一并删除。
  • 测试。
  • 如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录封装集合手法。

代码展示

原代码

organization = {name: 'Acme Gooseberries', country: 'GB'};

新代码

class Organization {
    constructor(data) {
        this._name = data.name;
        this._country = data.country;
    }
    
    get name() { return this._name;}
    set name(arg) { this._name = arg;}
    
    get country() { return this._country; }
    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 => 'hight' === 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() { 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; }
}

内联类

操作做法

  • 对于待内联类中所有public函数,在目标类上创建一个对应的函数,新创建的所有函数应该直接委托至源类。
  • 修改源类public方法的所有引用点,令它们调用目标类对应的委托方法。每次更改后运行测试。
  • 将源类中的函数与数据全部搬移到目标类,每次修改之后进行测试,直到源类变成空壳为止。
  • 最后删除源类。

代码展示

原代码

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;}
}

隐藏委托关系

操作做法

  • 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数。
  • 调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试。
  • 如果将来不再有任何客户端需要取用Delegate(受托类),便可移除服务对象中的相关访问函数。
  • 测试。

代码展示

原代码

manager = aPerson.department.manager;

新代码

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

移除中间人

操作做法

  • 为受托对象创建一个取值函数。
  • 对于每个委托函数,让其客户端转为连续的访问函数调用。
  • 测试。

代码展示

原代码

manager = aPerson.department;
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] === 'Kent') {
            return 'Kent';
        }
    }
    
    return '';
}

新代码

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

搬移函数

操作做法

  • 检查函数在当前上下文里引用的所有程序元素(包括变量和函数),考虑是否需要将它们一并搬移。
  • 检查待搬移函数是否具备多态性。
  • 将函数复制一份到目标上下文中。
  • 执行静态检查。
  • 设法从源上下文中正确引用目标函数。
  • 修改源函数,使之成为一个纯委托函数。
  • 测试。
  • 考虑对源函数使用内联函数

代码展示

原代码

class Account {
    get overdraftCharge() {}
}

新代码

class AccountType {
    get overdraftCharge() {}
}

搬移字段

操作做法

  • 确保源字段已经得到了良好封装。
  • 测试。
  • 在目标对象上创建一个字段。
  • 执行静态检查。
  • 确保源对象里能够正常引用目标对象。
  • 调整源对象的访问函数,令其使用目标对象的字段。
  • 测试。
  • 移除源对象上的字段。
  • 测试。

代码展示

原代码

class Customer {
    get plan() { return this._plan;}
    get discountRate() { return this._discountRate; }
}

新代码

class Customer {
    get plan() { return this._plan;}
    get discountRate() { return this.plan._discountRate; }
}

搬移语句到函数

操作做法

  • 如果重复的代码段离调用目标函数的地方还有些距离,则先用移动语句将这些语句挪动到紧邻目标函数的位置。
  • 如果目标函数仅被唯一一个函数调用,那么只需将源函数中的重复代码段剪切并粘贴到目标函数中即可,然后运行测试。
  • 如果函数不止一个调用点,那么先选择其中一个调用点应用提炼函数,将待搬移的语句与目标函数一起提炼成一个新函数。给新函数取个临时的名字,只要易于搜索即可。
  • 调整函数的其他调用点,令它们调用新提炼的函数。
  • 完成所有引用点的替换后,应用内联函数将目标函数内联到新函数里,并移除原目标函数。
  • 对新函数应用函数改名,将其改名为原目标函数的名字。

代码展示

原代码

result.push(`<p>title: ${person.photo.title}</p>`);
result.concat(photoData(person.photo));

function photoData(aPhoto) {
    return [
        `<p>location: ${aPhoto.location}</p>`,
        `<p>date: ${aPhoto.date.toDateString()}</p>`
    ];
}

新代码

result.concat(photoData(person.photo));

function photoData(aPhoto) {
    return [
        `<p>title: ${person.photo.title}</p>`
        `<p>location: ${aPhoto.location}</p>`,
        `<p>date: ${aPhoto.date.toDateString()}</p>`
    ];
}

搬移语句到调用者

操作做法

  • 最简单的情况下,原函数非常简单,其调用者也只有寥寥一两个,此时只需要把要搬移的代码从函数里剪切出来并粘贴回调用端去即可,必要的时候做些调整。
  • 若调用点不止一两个,则需要先用提炼函数,将你不想搬移的代码提炼成一个新函数,函数名可以临时起一个,只要后续容易搜索即可。
  • 对原函数应用内联函数
  • 对提炼出来的函数应用改变函数声明,令其与原函数使用同一个名字。

代码展示

原代码

emitPhotoData(outStream, person.photo);

function emitPhotoData(outStream, photo) {
    outStream.write(`<p>title: ${photo.title}</p>\n`);
    outStream.write(`<p>location: ${photo.location}</p>\n`);
}

新代码

emitPhotoData(outStream, person.photo);
outStream.write(`<p>location: ${person.photo.location}</p>\n`);

function emitPhotoData(outStream, photo) {
    outStream.write(`<p>title: ${photo.title}</p>\n`);
}

以函数调用取代内联代码

操作做法

  • 将内联代码替代为对一个既有函数的调用。
  • 测试。

代码展示

原代码

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

新代码

appliesToMass = states.includes('MA');

移动语句

操作做法

  • 确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语句,看看搬移后是否会影响这些代码正常工作。
  • 剪切源代码片段,粘贴到上一步选定的位置上。
  • 测试。

代码展示

原代码

const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();

let charge;
const chargePerUnit = pricingPlan.unit;

新代码

const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retrieveOrder();
let charge;

拆分循环

操作做法

  • 复制一遍循环代码。
  • 识别并移除循环中的重复代码,使每个循环只做一件事。
  • 测试。

代码展示

原代码

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

averageAge = averageAge / people.length;

新代码

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

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

averageAge = averageAge / people.length;

以管道取代循环

操作做法

  • 创建一个新变量,用以存放参与循环过程的集合。
  • 从循环顶部开始,将循环里的每一块行为依次搬移出来,在上一步创建的集合变量上用一种管道运算替代之。
  • 搬移完循环里的全部行为后,将循环整个删除。

代码展示

原代码

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);

移除死代码

操作做法

  • 如果死代码可以从外部直接引用,比如它是一个独立的函数时,先查找一下还有无调用点。
  • 将死代码移除。
  • 测试。

代码展示

原代码

if (false) {
   doSomethingThatUsedToMatter(); 
}

新代码



拆分变量

操作做法

  • 在待分解变量的声明及第一次被赋值处,修改其名称。
  • 如果可能的话,将新的变量声明为不可修改。
  • 以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用新变量。
  • 测试。
  • 重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一处赋值。

代码展示

原代码

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);

字段改名

操作做法

  • 如果记录的作用域较小,可以直接修改所有该字段的代码,然后测试。后面的步骤就都不需要了。
  • 如果记录还未封装,请先使用封装记录.
  • 在对象内部对私有字段改名,对应调整内部访问该字段的函数。
  • 测试。
  • 如果构造函数的参数用了旧的字段名,运用改变函数声明将其改名。
  • 运用函数改名给访问函数改名。

代码展示

原代码

class Organization {
    get name() {}
}

新代码

class Organization {
    get title() {}
}

以查询取代派生变量

操作做法

  • 识别出所有对变量做更新的地方。如有必要,用拆分变量分割各个更新点。
  • 新建一个函数,用于计算该变量的值。
  • 引入断言断言该变量和计算函数始终给出同样的值。
  • 测试。
  • 修改读取该变量的代码,令其调用新建的函数。
  • 测试。
  • 移除死代码去掉变量的声明和赋值。

代码展示

原代码

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; }

将引用对象改为值对象

操作做法

  • 检查重构目标是否为不可变对象,或者是否可修改为不可变对象。
  • 移除设值函数逐一去掉所有设值函数。
  • 提供一个基于值的相等性判断函数,在其中使用值对象的字段。

代码展示

原代码

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

新代码

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

将值对象改为引用对象

操作做法

  • 为相关对象创建一个仓库。
  • 确保构造函数有办法找到关联对象的正确实例。
  • 修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测试。

代码展示

原代码

let customer = new Customer(customerData);

新代码

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

分解条件表达式

操作做法

  • 对条件判断和每个条件分支分别运用提炼函数手法。

代码展示

原代码

if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
    charge = quantity * plan.summerRate;
else 
    charge = quantity * plan.regularRate + plan.regularServiceCharge;

新代码

if (summer())
    charge = summer();
else
    charge = regularCharge();

合并条件表达式

操作做法

  • 确定这些条件表达式都没有副作用。
  • 使用适当的逻辑运算符,将两个相关条件表达式合并为一个。
  • 测试。
  • 重复前面的合并过程,直到所有相关的条件表达式都合并到一起。
  • 可以考虑对合并后的条件表达式实施提炼函数

代码展示

原代码

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));
}

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

操作做法

  • 选中最外层需要被替换的条件逻辑,将其替换为卫语句。
  • 测试。
  • 有需要的话,重复上述步骤。
  • 如果所有卫语句都引发同样的结果,可以使用合并条件表达式合并之。

代码展示

原代码

function getPayAmount() {
    let result;
    if (isDead)
        result = deadAmount();
    else {
        if (isSeparated)
            result = separatedAmount();
        else {
            if (isRetired)
                result = retiredAmount();
            else
                result = normalPayAmount();
        }
    }
    return result;
}

新代码

function getPayAmount() {
    if (isDead) return deadAmount();
    if (isSeparated) return separatedAmount();
    if (isRetired) return retiredAmount();
    
    return normalPayAmount();
}

以多态取代条件表达式

操作做法

  • 如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。
  • 在调用方代码中使用工厂函数获得对象实例。
  • 将带有条件逻辑的函数移到超类中。
  • 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。
  • 重复上述过程,处理其他条件分支。
  • 在超类函数中保留默认情况的逻辑。

代码展示

原代码

switch (bird.type) {
    case 'EuropeanSwallow':
        return 'average';
    case 'AfricanSwallow':
        return (bird.numberOfCoconuts > 2) ? 'tired' : 'average';
    case 'NorwegianBlueParrot':
        return (bird.voltage > 100) ? 'scorched' : 'beautiful';
    default:
        return 'unknown';
}

新代码

class EuropeanSwallow {
    get plumage() {
        return 'average';
    }
}

class AfricanSwallow {
    get plumage() {
        return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
    }
}

class NorwegianBlueParrot {
    get plumage() {
        return (this.voltage > 100) ? 'scorched' : 'beautiful';
    }
}

以多态取代条件表达式

操作做法

  • 如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。
  • 在调用方代码中使用工厂函数获得对象实例。
  • 将带有条件逻辑的函数移到超类中。
  • 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。
  • 重复上述过程,处理其他条件分支。
  • 在超类函数中保留默认情况的逻辑。

代码展示

原代码

switch (bird.type) {
    case 'EuropeanSwallow':
        return 'average';
    case 'AfricanSwallow':
        return (bird.numberOfCoconuts > 2) ? 'tired' : 'average';
    case 'NorwegianBlueParrot':
        return (bird.voltage > 100) ? 'scorched' : 'beautiful';
    default:
        return 'unknown';
}

新代码

class EuropeanSwallow {
    get plumage() {
        return 'average';
    }
}

class AfricanSwallow {
    get plumage() {
        return (this.numberOfCoconuts > 2) ? 'tired' : 'average';
    }
}

class NorwegianBlueParrot {
    get plumage() {
        return (this.voltage > 100) ? 'scorched' : 'beautiful';
    }
}

引入特例

操作做法

  • 给重构目标添加检查特例的属性,令其返回false。
  • 创建一个特例对象,其中只有检查特例的属性,返回true。
  • 对“与特例值做对比”的代码运用提炼函数,确保所有客户端都使用这个新函数,而不再直接做特例值的比对。
  • 将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成。
  • 修改特例比对函数的主体,在其中直接使用检查特例的属性。
  • 测试。
  • 使用函数组合成类函数组合成变换,把通用的特例处理逻辑都搬移到新建的特例对象中。
  • 对特例比对函数使用内联函数,将其内联到仍然需要的地方。

代码展示

原代码

if (aCustomer === 'unknown') customerName = 'occupant';

新代码

class UnknownCustomer {
    get name() {
        return 'occupant';
    }
}

引入断言

操作做法

  • 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况。因为断言应该不会对系统运行造成任何影响,所以“加入断言”永远都应该是行为保持的。

代码展示

原代码

if (this.discountRate)
    base = base - (this.discountRate * base);

新代码

assert(this.discountRate >= 0);
if (this.discountRate)
    base = base - (this.discountRate * base);

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

操作做法

  • 复制整个函数,将其作为一个查询来命名。
  • 从新建的查询函数中去掉所有造成副作用的语句。
  • 执行静态检查。
  • 查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测试。
  • 从原函数中去掉返回值。
  • 测试。

代码展示

原代码

function getTotalOutstandingAndSendBill() {
    const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
    sendBill();
    return result;
}

新代码

function totalOutstanding() {
    return customer.invoices.reduce((total, each) => each.amount + total, 0);
}

function sendBill() {
    emailGateway.send(formatBill(customer));
}

函数参数化

操作做法

  • 从一组相似的函数中选择一个。
  • 运用改变函数声明,把需要作为参数传入的字面量添加到参数列表中。
  • 修改该函数所有的调用处,使其在调用时传入该字面量值。
  • 测试。
  • 修改函数体,令其使用新传入的参数。每使用一个新参数都要测试。
  • 对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次修改后都要测试。

代码展示

原代码

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

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

新代码

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

移除标记参数

操作做法

  • 针对参数的每一种可能值,新建一个明确函数。
  • 对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。

代码展示

原代码

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

新代码

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

保持对象完整

操作做法

  • 新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数)。
  • 在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表(即来源于完整对象的各项数据)。
  • 执行静态检查。
  • 逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试。
  • 所有调用处都修改过来之后,使用内联函数把旧函数内联到新函数体内。
  • 给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名字,同时修改所有调用处。

代码展示

原代码

const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high))

新代码

if (aPlan.withinRange(aRoom.daysTempRange))

以查询取代参数

操作做法

  • 如果有必要,使用提炼函数将参数的计算过程提炼到一个独立的函数中。
  • 将函数体内引用该参数的地方改为调用新建的函数。每次修改后执行测试。
  • 全部替换完成后,使用改变函数声明将该参数去掉。

代码展示

原代码

availableVacation(anEmployee, anEmployee.grade);

function availableVacation(anEmployee, grade) {}

新代码

availableVacation(anEmployee);

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

以参数取代查询

操作做法

  • 对执行查询操作的代码使用提炼变量,将其从函数体中分离出来。
  • 现在函数体代码已经不再执行查询操作,对这部分代码使用提炼函数
  • 使用内联变量,消除刚才提炼出来的变量。
  • 对原来的函数使用内联函数
  • 对新函数改名,改回原来函数的名字。

代码展示

原代码

targetTemperature(aPlan);

function targetTemperature(aPlan) {
    currentTemperature = thermostat.currentTemperature;
}

新代码

targetTemperature(aPlan, thermostat.currentTemperature);

function targetTemperature(aPlan, currentTemperature) {}

移除设值函数

操作做法

  • 如果构造函数尚无法得到想要设入字段的值,就使用改变函数声明将这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设值。
  • 移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修改之后都要测试。
  • 使用内联函数消去设值函数。如果可能的话,把字段声明为不可变。
  • 测试。

代码展示

原代码

class Person {
    get name() {}
    set name(aString) {}
}

新代码

class Person {
    get name() {}
}

以工厂函数取代构造函数

操作做法

  • 新建一个工厂函数,让它调用现有的构造函数。
  • 将调用构造函数的代码改为调用工厂函数。
  • 每修改一处,就执行测试。
  • 尽量缩小构造函数的可见范围。

代码展示

原代码

leadEngineer = new Employee(document.leadEngineer, 'E');

新代码

leadEngineer = createEmployeer(document.leadEngineer);

以命令取代函数

操作做法

  • 为想要包装的函数创建一个空的类,根据该函数的名字为其命名。
  • 使用搬移函数把函数移到空的类里。
  • 可以考虑给每个参数创建一个字段,并在构造函数中添加对应的参数。

代码展示

原代码

function score(candidate, medicalExam, scoringGuide) {
    let result = 0;
    let healthLevel = 0;
}

新代码

class Scorer {
    constructor(candidate, medicalExam, scoringGuide) {
        this._candidate = candidate;
        this._medicalExam = medicalExam;
        this._scoringGuide = scoringGuide;
    }
    
    execute() {
        this._result = 0;
        this._healthLevel = 0;
    }
}

以函数取代命令

操作做法

  • 运用提炼函数,把“创建并执行命令对象”的代码单独提炼到一个函数中。
  • 对命令对象在执行阶段用到的函数,逐一使用内联函数
  • 使用改变函数声明,把构造函数的参数转移到执行函数。
  • 对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数。每次修改后都要测试。
  • 把“调用构造函数”和“调用执行函数”两步都内联到调用方。
  • 测试。
  • 移除死代码把命令类消去。

代码展示

原代码

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

新代码

function charge(customer, usage) {
    return customer.rate * usage;
}

函数上移

操作做法

  • 检查待提升函数,确定它们是完全一致的。
  • 检查函数体内引用的所有函数调用和字段都能从超类中调用到。
  • 如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名。
  • 在超类中新建一个函数,将某一个待提升函数的代码复制到其中。
  • 执行静态检查。
  • 移除一个待提升的子类函数。
  • 测试。
  • 逐一移除待提升的子类函数,直到只剩下超类中的函数为止。

代码展示

原代码

class Employee {}

class Salesman extends Employee {
    get name() {}
}

class Engineer extends Employee {
    get name() {}
}

新代码

class Employee {
    get name() {}
}

class Salesman extends Employee {}
class Engineer extends Employee {}

字段上移

操作做法

  • 针对待提升字段,检查它们的所有使用点,确认它们以同样的方式被使用。
  • 如果这些字段的名称不同,先使用变量改名 为它们取个相同的名字。
  • 在超类中新建一个字段。
  • 移除子类中的字段。
  • 测试。

代码展示

原代码

class Employee {} //Java

class Salesman extends Employee {
    private String name;
}

class Engineer extends Employee {
    private String name;
}

新代码

class Employee {
    protected String name;
}

class Salesman extends Employee {}
class Engineer extends Employee {}

构造函数本体上移

操作做法

  • 如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造函数。
  • 使用移动语句将子类中构造函数的的公共语句移动到超类的构造函数调用语句之后。
  • 逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数。
  • 测试。
  • 如果存在无法简单提升至超类的公共代码,先应用提炼函数,再利用函数上移提升。

代码展示

原代码

class Party {}

class Employee extends Party {
    constructor(name, id, monthlyCost) {
        super();
        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;
    }
}

函数下移

操作做法

  • 将超类中的函数本体复制到每一个需要此函数的子类中。
  • 删除超类中的函数。
  • 测试。
  • 将该函数从所有不需要它的那些子类中删除。
  • 测试。

代码展示

原代码

class Employee {
    get quota {}
}

class Engineer extends Employee {}
class Salesman extends Employee {}

新代码

class Employee {}

class Engineer extends Employee {}
class Salesman extends Employee {
    get quota {}
}

字段下移

操作做法

  • 在所有需要该字段的子类中声明该字段。
  • 将该字段从超类中移除。
  • 测试。
  • 将该字段从所有不需要它的那些子类中删掉。
  • 测试。

代码展示

原代码

class Employee { // Java
    private String quota;
}

class Engineer extends Employee {}
class Salesman extends Employee {}

新代码

class Employee {}
class Engineer extends Employee {}

class Salesman extends Employee {
    protected String quota;
}

以子类取代类型码

操作做法

  • 自封装类型码字段。
  • 任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返回该类型码的字面量值。
  • 创建一个选择器逻辑,把类型码参数映射到新的子类。
  • 测试。
  • 针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。
  • 去除类型码字段。
  • 测试。
  • 使用函数下移以多态取代条件表达式处理原本访问了类型码的函数。

代码展示

原代码

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);
    }
}

移除子类

操作做法

  • 使用以工厂函数取代构造函数,把子类的构造函数包装到超类的工厂函数中。
  • 如果有任何代码检查子类的类型,先用提炼函数把类型检查逻辑包装起来,然后用搬移函数将其搬到超类。
  • 新建一个字段,用于代表子类的类型。
  • 将原本针对子类的类型做判断的函数改为使用新建的类型字段。
  • 删除子类。
  • 测试。

代码展示

原代码

class Person {
    get genderCode() { return 'X';}
}

class Male extends Person {
    get genderCode() { return 'M';}
}

class Female extends Person {
    get genderCode() { return 'F';}
}

新代码

class Person {
    get genderCode() { return this._genderCode;}
}

提炼超类

操作做法

  • 为原本的类新建一个空白的超类。
  • 测试。
  • 使用构造函数本体上移函数上移字段上移手法,逐一将子类的共同元素上移到超类。
  • 检查留在子类中的函数,看它们是否还有共同的成分。如果有,可以先用提炼函数将其提炼出来,再用函数上移搬到超类。
  • 检查所有使用原本的类的客户端代码,考虑将其调整为使用超类的接口。

代码展示

原代码

class Department {
    get totalAnnualCost() {}
    get name() {}
    get headCount() {}
}

class Employee {
    get annualCost() {}
    get name() {}
    get id() {}
}

新代码

class Party {
    get name() {}
    get annualCost() {}
}

class Department extends Party {
    get annualCost() {}
    get headCount() {}
}

class Employee extends Party {
    get annualCost() {}
    get id() {}
}

折叠继承体系

操作做法

  • 选择想移除的类:是超类还是子类?
  • 使用字段上移字段下移函数上移函数下移把所有元素都移到同一个类中。
  • 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。
  • 移除我们的目标;
  • 测试。

代码展示

原代码

class Employee {}
class Salesman extends Employee {}

新代码

class Employee {}

以委托取代子类

操作做法

  • 如果构造函数有多个调用者,首先用以工厂函数取代构造函数把构造函数包装起来。
  • 创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并且经常以参数的形式接受一个指回超类的引用。
  • 在超类中添加一个字段,用于安放委托对象。
  • 修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例。
  • 选择一个子类中的函数,将其移入委托类。
  • 使用搬移函数手法搬移上述函数,不要删除源类中的委托代码。
  • 如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之外已经没有其他调用者,就用移除死代码去掉已经没人使用的委托代码。
  • 测试。
  • 重复上述过程,直到子类中所有函数都搬到委托类。
  • 找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数。
  • 测试。
  • 运用移除死代码去掉子类。

代码展示

原代码

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 PriorityOrderDelegate {
    get daysToShip() {
        return this._priorityPlan.daysToShip;
    }
}

以委托取代超类

操作做法

  • 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例。
  • 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。
  • 当所有超类函数都转发函数覆写后,就可以去掉继承关系。

代码展示

原代码

class List {}
class Stack extends List {}

新代码

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

class List {}