代码重构

在不改变代码外在行为的前提下,对代码进行修改,以改进程序的内部结构

重构类型

重构度量

对于中小型重构,可以观察代码健康度相关的指标变化来度量重构的价值:比如代码的圈复杂度、平均函数行数、类行数等

而对于大型重构,则可以通过工程效率上的指标变化来可视化重构的收益

重构原则

为何重构

何时重构

何时不该重构

如何保证重构的正确性

测试是保证代码正确性的强有力保证

代码的坏味道

重构列表

函数/变量

批注 2020-06-30 103655

根据代码意图进行拆分函数,如果发现一段代码需要阅读一会才能知道是干嘛的,那就提炼它

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

批注 2020-06-30 104427

提炼函数的反向操作

如果函数的代码跟函数名称一样拥有可读性,那么可以直接内联它

批注 2020-06-30 104817

给一些表达式起个有意义的名称,有助于阅读、调试

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 defaultOwner = {firstName: "Martin", lastName: "Fowler"};

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

好的命名是整洁代码的核心

让数据项自己的关系变得清晰,并且缩短参数列表

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

一段代码做了多件事,将它拆分为多个函数

封装

封装能更好地应对变化

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

class Organization {...}

对集合成员变量进行封装,返回其一个副本,避免其被修改带来的诸多问题

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

上述的反向操作,由于类职责的改变,或者两个类合并在一起会更加简单

封装意味着模块间相互了解的程度应该尽可能小,一旦发生变化,影响也会较小

manager = aPerson.department.manager;

manager = aPerson.manager; 

class Person {
  get manager() {return this.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() {...}
}

批注 2020-07-02 124318

对于早期设计不良的数据结构,使用此方法改造它

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: ${aPhoto.title}</p>`,
  `<p>location: ${aPhoto.location}</p>`,
  `<p>date: ${aPhoto.date.toDateString()}</p>`,
 ];
}

上述的反向操作

对于代码演进,函数某些代码职责发生变化,将它们移除出去

一些函数的函数名就拥有足够的表达能力

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

appliesToMass = states.includes("MA");

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

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

const pricingPlan = retrievePricingPlan(); 
const chargePerUnit = pricingPlan.unit; 
const order = retreiveOrder();
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);

移除那些永远不会允许的代码

重新组织数据

如果一个变量被用于多种用途,很明显违反了单一职责原则,这样的代码会造成理解上的困难

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

对于命名不够良好的字段进行改名

使用查询封装变量是消除可变数据的第一步

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

如果非一定需要引用对象,使用值对象不可变的特性能避免很多问题

如果一个对象需要在多个地方做更新,值对象就不适合了,需要改为引用

简化条件逻辑

使用函数封装条件逻辑,提升代码的可理解性

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

if (summer())
 charge = summerCharge(); 
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";
}

所谓特例,就是满足这个类的行为,但却表达了特例的含义

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

重构API

对于无副作用的函数,有助于测试

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 = aPerson.salary.multiply(1.05);
}

function raise(aPerson, factor) {
  aPerson.salary = aPerson.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;} 
function setWidth (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) {}

上述操作的反向重构,如果不想函数依赖某个元素,那就使用这个方式

取消设值函数,代表着数据不应该被修改的意图

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

class Person {
  get name() {...}
}

构造函数使用起来较不灵活,尝试把创建对象的职责交给工厂

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

leadEngineer = createEngineer(document.leadEngineer);

命令对象大都服务于单一的函数,命令相交于过程性代码,拥有了大部分面向对象的能力

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

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

  execute() { 
    this._result = 0;
    this._healthLevel = 0;
    // long body code
  }
}

上述的反向重构,在不是很复杂的情况下,直接使用函数完成任务即可

处理继承关系

本质上还是为了避免重复,重复代码是滋生bug的温床

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

函数上移的反向重构,如果超类的某个函数只与部分子类有关,那就需要将函数下移

字段上移的反向重构,动机同上

使用多态来替代逻辑判断

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

速查表

坏味道(英文)

坏味道(中文)

页码

常用重构

Alternative Classes with Different Interfaces

异曲同工的类

83

改变函数声明(124),搬移函数(198),提炼超类(375)

Comments

注释

84

提炼函数(106),改变函数声明(124),引入断言(302)

Data Class

纯数据类

83

封装记录(162),移除设值函数(331),搬移函数(198),提炼函数(106),拆分阶段(154)

Data Clumps

数据泥团

78

提炼类(182),引入参数对象(140),保持对象完整(319)

Divergent Change

发散式变化

76

拆分阶段(154),搬移函数(198),提炼函数(106),提炼类(182)

Duplicated Code

重复代码

72

提炼函数(106),移动语句(223),函数上移(350)

Feature Envy

依恋情结

77

搬移函数(198),提炼函数(106)

Global Data

全局数据

74

封装变量(132)

Insider Trading

内幕交易

82

搬移函数(198),搬移字段(207),隐藏委托关系(189),以委托取代子类(381),以委托取代超类(399)

Large Class

过大的类

82

提炼类(182),提炼超类(375),以子类取代类型码(362)

Lazy Element

冗赘的元素

80

内联函数(115),内联类(186),折叠继承体系(380)

Long Function

过长函数

73

提炼函数(106),以查询取代临时变量(178),引入参数对象(140),保持对象完整(319),以命令取代函数(337),分解条件表达式(260),以多态取代条件表达式(272),拆分循环(227)

Long Parameter List

过长参数列

74

以查询取代参数(324),保持对象完整(319),引入参数对象(140),移除标记参数(314),函数组合成类(144)

Loops

循环语句

79

以管道取代循环(231)

Message Chains

过长的消息链

81

隐藏委托关系(189),提炼函数(106),搬移函数(198)

Middle Man

中间人

81

移除中间人(192),内联函数(115),以委托取代超类(399),以委托取代子类(381)

Mutable Data

可变数据

75

封装变量(132),拆分变量(240),移动语句(223),提炼函数(106),将查询函数和修改函数分离(306),移除设值函数(331),以查询取代派生变量(248),函数组合成类(144),函数组合成变换(149),将引用对象改为值对象(252)

Mysterious Name

神秘命名

72

改变函数声明(124),变量改名(137),字段改名(244)

Primitive Obsession

基本类型偏执

78

以对象取代基本类型(174),以子类取代类型码(362),以多态取代条件表达式(272),提炼类(182),引入参数对象(140)

Refused Bequest

被拒绝的遗赠

83

函数下移(359),字段下移(361),以委托取代子类(381),以委托取代超类(399)

Repeated Switches

重复的switch

79

以多态取代条件表达式(272)

Shotgun Surgery

霰弹式修改

76

搬移函数(198),搬移字段(207),函数组合成类(144),函数组合成变换(149),拆分阶段(154),内联函数(115),内联类(186)

Speculative Generality

夸夸其谈通用性

80

折叠继承体系(380),内联函数(115),内联类(186),改变函数声明(124),移除死代码(237)

Temporary Field

临时字段

80

提炼类(182),搬移函数(198),引入特例(289)