【重构】读书笔记 7-10章

本文详细解读了《重构》一书中关于封装、搬移特性、重新组织数据和简化条件逻辑的章节。重点介绍了如何通过封装记录、集合、以对象取代基本类型等提高代码质量。同时讲解了搬移函数、字段,以及如何处理条件逻辑,如分解条件表达式、以多态取代条件。此外,还讨论了数据拆分、移除死代码和引入断言等重构技巧,以提升代码的可读性和维护性。
摘要由CSDN通过智能技术生成

第7章 封装

7.1 封装记录(Encapsulate Record)

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

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

7.2 封装集合(Encapsulate Collection)

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

我喜欢封装程序中的所有可变数据。这使我很容易看清楚数据被修改的地点和修改方式,这样当我需要更改数据结构时就非常方便。我们通常鼓励封装——使用面向对象技术的开发者对封装尤为重视——但封装集合时人们常常犯一个错误:只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。

为避免此种情况,我会在类上提供一些修改集合的方法——通常是“添加”和“移除”方法。这样就可使对集合的修改必须经过类,当程序演化变大时,我依然能轻易找出修改点。

7.3 以对象取代基本类型(Replace Primitive with Object)

orders.filter(o => "high" === o.priority	|| "rush" === o.priority);

–>

orders.filter(o => o.priority.higherThan(new Priority("normal")))

一旦我发现对某个数据的操作不仅仅局限于打印时,我就会为它创建一个新类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要类有了,日后添加的业务逻辑就有地可去了。

7.4 以查询取代临时变量(Replace Temp with Query)

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 提炼类(Extract Class)

反向重构:内联类(186)

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

决定如何分解类所负的责任。创建一个新的类,用以表现从旧类中分离出来的责任。

7.6 内联类(Inline Class)

反向重构:提炼类(182)

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

内联类正好与提炼类(182)相反。如果一个类不再承担足够责任,不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),我就会挑选这一“萎缩类”的最频繁用户(也是一个类),以本手法将“萎缩类”塞进另一个类中。

7.7 隐藏委托关系(Hide Delegate)

反向重构:移除中间人(192)

manager = aPerson.department.manager;

–>

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

如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一受托类修改了接口,变化会波及通过服务对象使用它的所有客户端。我可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即使将来委托关系发生变化,变化也只会影响服务对象,而不会直接波及所有客户端。

7.8 移除中间人(Remove Middle Man)

反向重构:隐藏委托关系(189)

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

–>

manager = aPerson.department.manager;

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 => candidates.includes(p)) || '';
}

如果我发现做一件事可以有更清晰的方式,我就会用比较清晰的方式取代复杂的方式。“重构”可以把一些复杂的东西分解为较简单的小块,但有时你就必须壮士断腕,删掉整个算法,代之以较简单的算法。

第8章 搬移特性

还有另一种类型的重构也很重要,那就是在不同的上下文之间搬移元素。我会通过搬移函数(198)手法在类与其他模块之间搬移函数,对于字段可用搬移字段(207)手法做类似的搬移。

8.1 搬移函数(Move Function)

搬移函数最直接的一个动因是,它频繁引用其他上下文中的元素,而对自身 上下文中的元素却关心甚少。此时,让它去与那些更亲密的元素相会,通常能取 得更好的封装效果,因为系统别处就可以减少对当前模块的依赖。

8.2 搬移字段(Move Field)

搬移字段的操作通常是在其他更大的改动背景下发生的。实施字段搬移后, 我可能会发现字段的诸多使用者应该通过目标对象来访问它,而不应该再通过源 对象来访问。诸如此类的清理,我会在此后的重构中一并完成。同样,我也可能 因为字段当前的一些用法而无法直接搬移它。我得先对其使用方式做一些重构, 然后才能继续搬移工作。

8.3 搬移语句到函数(Move Statements into Function)

反向重构:搬移语句到调用者(217)

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

如果某些语句与一个函数放在一起更像一个整体,并且更有助于理解,那我 就会毫不犹豫地将语句搬移到函数里去。

8.4 搬移语句到调用者(Move Statements to Callers)

反向重构:搬移语句到函数(213)

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

8.6 移动语句(Slide Statements)

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

->

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

让存在关联的东西一起出现,可以使代码更容易理解。如果有几行代码取用 了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据 结构的代码中间。最简单的情况下,我只需使用移动语句就可以让它们聚集起 来。此外还有一种常见的“关联”,就是关于变量的声明和使用。有人喜欢在函数 顶部一口气声明函数用到的所有变量,我个人则喜欢在第一次需要使用变量的地 方再声明它。

8.7 拆分循环(Split Loop)

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;

但如果你在一次循环中做了两件不同的事,那么每 当需要修改循环时,你都得同时理解这两件事情。如果能够将循环拆分,让一个 循环只做一件事情,那就能确保每次修改时你只需要理解要修改的那块代码的行 为就可以了。

8.8 以管道取代循环(Replace Loop with Pipeline)

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

集合管道是这样一种技术,它允许我使用一组运算来描述集合的迭代过程,其中每种运算 接收的入参和返回值都是一个集合。这类运算有很多种,最常见的则非map和 filter莫属:
map运算是指用一个函数作用于输入集合的每一个元素上,将集合变 换成另外一个集合的过程;
filter运算是指用一个函数从输入集合中筛选出符合条 件的元素子集的过程。运算得到的集合可以供管道的后续流程使用。我发现一些 逻辑如果采用集合管道来编写,代码的可读性会更强——我只消从头到尾阅读一 遍代码,就能弄清对象在管道中间的变换过程。

8.9 移除死代码(Remove Dead Code)

一旦代码不再被使用,我们就该立马删除它。有可能以后又会需要这段代 码,但我从不担心这种情况;就算真的发生,我也可以从版本控制系统里再次将 它翻找出来。

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

如果它们被赋值超过一次,就意味它们在 函数中承担了一个以上的责任。如果变量承担多个责任,它就应该被替换(分 解)为多个变量,每个变量只承担一个责任。

第10章 简化条件逻辑

10.1 分解条件表达式(Decompose Conditional)

在这里插入图片描述

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

和任何大块头代码一样,我可以将它分解为多个独立的函数,根据每个小块 代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函 数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函 数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并 且突出每个分支的原因。

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

有时我会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如 果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达 式。
之所以要合并条件代码,有两个重要原因。首先,合并后的条件代码会表 述“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这 一次检查的用意更清晰。

10.3 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)

在这里插入图片描述

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

如果某个条件 极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样 的单独检查常常被称为“卫语句”(guard clauses)。

以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如 果使用if-then-else结构,你对if分支和else分支的重视是同等的。这样的代码 结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它 告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请 做一些必要的整理工作,然后退出。”

10.4 以多态取代条件表达式(Replace Conditional with Polymorphism)

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/else和 switch/case

10.5 引入特例(Introduce Special Case)

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

–>

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

一种常见的重复代码是这种情况:一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同。如果我发现代码库中有多处以同样方式应对同一个特殊值,我就会想要把这个处理逻辑收拢到一处。

处理这种情况的一个好办法是使用“特例”(Special Case)模式:创建一个特例元素,用以表达对这种特例的共用行为的处理。这样我就可以用一个函数调用取代大部分特例检查逻辑。

10.6 引入断言(Introduce Assertion)

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

–>

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

常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行。
这样的假设通常并没有在代码中明确表现出来,你必须阅读整个算法才能看出。有时程序员会以注释写出这样的假设,而我要介绍的是一种更好的技术——使用断言明确标明这些假设。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值