Skip to content

Latest commit

 

History

History
557 lines (406 loc) · 11.2 KB

代码重构.md

File metadata and controls

557 lines (406 loc) · 11.2 KB

代码重构

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

  • 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。
  • 重构前,先检查自己是否有一套可靠的测试代码。这些测试必须有自我检验能力。
  • 重构技术就是以微小的步伐修改程序。 如果你犯下错误,很容易便可发现它。

重构原则

为何重构

  • 改进软件的设计
  • 使代码更容易理解
  • 提高编程速度

何时重构

  • 预备性重构:添加新功能的时候
  • 帮助理解的重构:为了理解系统或者代码所做的工作
  • 捡垃圾式重构:偶然发现一处坏代码,重构它
  • 修复错误的时候
  • 代码审查的时候

何时不该重构

  • 不会被用到的代码
  • 重构的代价比重写的代价还高的代码

如何保证重构的正确性

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

  • 自动化
  • 测试不通过真的会失败
  • 频繁运行测试
  • 注意边界条件
  • 使用测试来重现bug

代码的坏味道

  • 奇怪的命名
  • 重复代码
  • 过长的函数
  • 过长的参数列表
  • 全局数据
  • 可变数据
  • 发散式变化
    • 一个修改会影响到许多地方
  • 霰弹式修改
    • 一个变化需要修改许多地方
  • 过度依赖外部模块
  • 类中重复的数据
  • 基本类型偏执
    • 总觉得基本类型效率更高,不愿使用对象
  • 大量重复的switch/if
  • 复杂的循环语句
  • 冗余的元素
    • 一个简单的函数、一个简单的操作
  • 过度设计的通用性
    • 过度考虑了对象/函数的用途
  • 临时字段
  • 过长的对象调用
  • 没有必要的中间对象
  • 两个模块耦合过紧
    • 考虑将它们移动到新模块
  • 过大的类
  • 过度相似的类
  • 纯数据类
    • 数据和行为没有在一起
  • 继承父类,但不提供父类的接口

重构列表

函数/变量

  • 提炼函数

批注 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);
  • 移除死代码

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