《重构》读书笔记(一)

一.说明

1.《重构》读书笔记指的是对fowler先生《重构》第二版的阅读观感(以下称重构2),这个版本或许像译者所说的fowler先生想要传达的理念是:

  千里之行积于跬步,越是面对复杂多变的外部环境,越是要做好基本功、迈出扎实步。

2. 译者认为重构2的重构原则是: " 旧的不变,新的创建,一步切换,旧的再见。"

3.《重构》传达的是一种工匠精神。我喜欢的译者观点是:一个对匠艺上心的专业人士,日积月累对过程与方式的重视,是能有所成就的。

二.第一章

1.何为重构?

重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减小整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。

2.为何重构?

在软件开发的大部分历史时期,大部分人相信应该先设计而后编码:首先得有一个良好的设计,然后才能开始编码。

但是,随着时间流逝,人们不断修改代码,于是根据原先设计所得的系统,整体结构逐渐衰弱。代码质量慢慢沉沦,编码工作从严谨的工程堕落为胡砍乱劈的随性行为。

设计不是在一开始完成的,而是在整个开发过程中逐渐浮现出来。在系统构筑过程中,我学会了如何不断改进设计。这个“构筑-设计”的反复互动,可以让一个程序在开发过程中持续保有良好的设计。
 3.第一个重构实例
设想有一个戏剧演出团,演员们经常要去各种场合表演戏剧。通常客户(customer)会指定几出剧目,而剧团则根据观众(audience)人数及剧目类型来向客户收费。该团目前出演两种戏剧:悲剧(tragedy)和喜剧(comedy)。给客户发出账单时,剧团还会根据到场观众的数量给出“观众量积分”(volumecredit)优惠,下次客户再请剧团表演时可以使用积分获得折扣——你可以把它看作一种提升客户忠诚度的方式。
plays.json:
{  "hamlet": {"name": "Hamlet", "type": "tragedy"},  "as-like": {"name": "As You Like It", "type": "comedy"},  "othello": {"name": "Othello", "type": "tragedy"}}
invoices.json:
[  {    "customer": "BigCo",    "performances": [      {        "playID": "hamlet",        "audience": 55      },      {        "playID": "as-like",        "audience": 35      },      {        "playID": "othello",        "audience": 40      }    ]  }];
账单函数:
function statement (invoice, plays) {  
  let totalAmount = 0;  
  let volumeCredits = 0; 
  let result = `Statement for ${invoice.customer}\n`;  
  const format = new Intl.NumberFormat("en-US",
      { style: "currency", currency: "USD",
        minimumFractionDigits: 2 }).format;   for (let perf of invoice.performances) {   const play = plays[perf.playID];   let thisAmount = 0;   switch (play.type) {   case "tragedy":     thisAmount = 40000;     if (perf.audience > 30) {     thisAmount += 1000 * (perf.audience - 30);     }     break;   case "comedy":     thisAmount = 30000;     if (perf.audience > 20) {      thisAmount += 10000 + 500 * (perf.audience - 20); }        thisAmount += 300 * perf.audience;        break;     default:       throw new Error(`unknown type: ${play.type}`);   }
  // add volume credits   volumeCredits += Math.max(perf.audience - 30, 0);   // add extra credit for every ten comedy attendees    if ("comedy" === play.type)     volumeCredits += Math.floor(perf.audience / 5);     // print line for this order     result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;    totalAmount += thisAmount;
  }   result
+= `Amount owed is ${format(totalAmount/100)}\n`;  result += `You earned ${volumeCredits} credits\n`; return result;
 } 用上面的数据文件(invoices.json和plays.json)作为测试输入,运行这段代码,会得到如下输出: Statement
for BigCo Hamlet: $650.00 (55 seats) As You Like It: $580.00 (35 seats) Othello: $500.00 (40 seats) Amount owed is $1,730.00 You earned 47 credits

这样的设计:代码组织不太清晰,但还在可以忍受的限度内。但如果这段代码身处一个更大规模--也许几百行--的程序中,把所有代码放在一个函数里就很难理解了。

虽然结构不清晰却是可以使用,对编译器来说没有影响,但是,当我需要修改的时候,差劲的系统就很难找到修改点。就很可能犯错误,引入bug。

因此如果要修改一个几百行的程序,我们会期望它有良好的结构,并且已经分解成一系列函数和其他程序要素,这能帮我们更易于清楚的了解这段代码在做什么。但如果程序杂乱无章,先为它整理出结构来,再做需要的修改,通常更加简单。

* 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,
   使其比较容易添加该特性,然后再添加该特性。

 试想修改:1.以HTML格式输出详单;2.戏剧分类规则和计费规则的变化;这些修改可能在几个月后还会出现变化。

如果不重构,函数则会变得越来越杂乱,需求的变化使重构变得必要!!!

重构过程:

A.重构第一步:得确保即将修改的代码拥有一组可靠的测试。使测试能自我检验至关重要,否则就得耗费大把时间来回比对,这会降低开发速度。

* 重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力。

B.分解statement函数:

[原文]:每当看到这样长长的函数,我便下意识地想从整个函数中分离出不同的关注点。第一个引起我注意的就是中间那段switch语句。

 switch (play.type) {   
   case "tragedy":      
    thisAmount = 40000;      
    if (perf.audience > 30) {
      thisAmount += 1000 * (perf.audience - 30);
    }
    break;
  case "comedy":
    thisAmount = 30000;
    if (perf.audience > 20) {
      thisAmount += 10000 + 500 * (perf.audience - 20);
    }
    thisAmount += 300 * perf.audience;
    break;
  default:
    throw new Error(`unknown type: ${play.type}`);
  }
先将这块代码抽取成一个独立的函数,按它所干的事情给它命名,比如叫amountFor(performance)。 
function amountFor(perf, play) {  let thisAmount = 0;  switch (play.type) {  case "tragedy":    thisAmount = 40000;    if (perf.audience > 30) {      thisAmount += 1000 * (perf.audience - 30);    }    break;  case "comedy":    thisAmount = 30000;    if (perf.audience > 20) {      thisAmount += 10000 + 500 * (perf.audience - 20);    }    thisAmount += 300 * perf.audience;    break;  default:      throw new Error(`unknown type: ${play.type}`);  }  return thisAmount;}

做完这个改动后,我会马上编译并执行一遍测试,看看有无破坏了其他东西。无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。小步修改,以及它带来的频繁反馈,正是防止混乱的关键。

重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它
做完上面的修改,测试是通过的,因此下一步我要把代码提交到本地的版本控制系统。我会使用诸如git或mercurial这样的版本控制系统,因为它们可以支持本地提交。每次成功的重构后我都会提交代码,如果待会不小心搞砸了,我便能轻松回滚到上一个可工作的状态。把代码推送(push)到远端仓库前,我会把零碎的修改压缩成一个更有意义的提(commit)。 
傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。

C.移除play变量

当分解一个长函数时,我喜欢将play这样的变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。

 编译、测试、提交。完成变量内联后,我可以对amountFor函数应用改变函数声明(124),移除play参数。我会分两步走。首先在amountFor函数内部使用新提炼的函数。

 
编译、测试、提交,最后将参数删除。

 

然后再一次编译、测试、提交。

 这次重构可能在一些程序员心中敲响警钟:重构前,查找play变量的代码在每次循环中只执行了1次,而重构后却执行了3次。我会在后面探讨重构与性能之间的关系,但现在,我认为这次改动还不太可能对性能有严重影响,即便真的有所影响,后续再对一段结构良好的代码进行性能调优,也容易得多。

D.提炼计算关总量积分的逻辑

 

 E.移除format变量

 正如我上面所指出的,临时变量往往会带来麻烦。它们只在对其进行处理的代码块中有用,因此临时变量实质上是鼓励你写长而复杂的函数。因此,下一步我要替换掉一些临时变量,而最简单的莫过于从format变量入手。这是典型的“将函数赋值给临时变量”的场景,我更愿意将其替换为一个明确声明的函数。

 

format函数更名

 F.移除观众量积分总和

把与更新volumeCredits变量相关的代码都集中到一起,有利于以查询取代临时变量(178)手法的施展。第一步同样是先对变量的计算过程应用提炼函数(106)手法。

 

 与复杂代码打交道时,细小的步子是快速前进的关键。

 

 现在代码结构已经好多了。顶层的statement函数现在只剩7行代码,而且它处理的都是与打印详单相关的逻辑。与计算相关的逻辑从主函数中被移走,改由一组函数来支持。每个单独的计算过程和详单的整体结构,都因此变得更易理解了。

G. 分离到两个文件
statement.js
 

createStatementData.js

 

 

 

 代码行数由我开始重构时的44行增加到了70行(不算htmlStatement),这主要是将代码抽取到函数里带来的额外包装成本。虽然代码的行数增加了,但重构也带来了代码可读性的提高。额外的包装将混杂的逻辑分解成可辨别的部分,分离了详单的计算逻辑与样式。这种模块化使我更容易辨别代码的不同部分,了解它们的协作关系。虽说言以简为贵,但可演化的软件却以明确为贵。通过增强代码的模块化,我可以轻易地添加HTML版本的代码,而无须重复计算部分的逻辑。

H.按类型重组计算过程

支持更多类型的戏剧,以及支持它们各自的价格计算和观众量积分计算。

创建演出计算器:

enrichPerformance函数是关键所在,因为正是它用每场演出的数据来填充中转数据结构。目前它直接调用了计算价格和观众量积分的函数,我需要创建一个类,通过这个类来调用这些函数。由于这个类存放了与每场演出相关数据的计算函数,于是我把它称为演出计算器(performance calculator)。

使用以工厂函数取代构造函数:

 createStatementData.js

 

  代码量仍然有所增加,因为我再次整理了代码结构。新结构带来的好处是,不同戏剧种类的计算各自集中到了一处地方。如果大多数修改都涉及特定类型的计算,像这样按类型进行分离就很有意义。当添加新剧种时,只需要添加一个子类,并在创建函数中返回它。

三.第一章结语

本章的重构有3个较为重要的节点,分别是:

(1)将原函数分解成一组嵌套的函数

(2)应用拆分阶段(154)分离计算逻辑与输出格式化逻辑

(3)为计算器引入多态性来处理计算逻辑

每一步都给代码添加了更多的结构,以便我能更好地表达代码的意图。

好代码的检验标准就是人们是否能轻而易举地修改它。好代码应该直截了当:有人需要修改代码时,他们应能轻易找到修改点,应该能快速做出更改,而不易引入其他错误。

一个健康的代码库能够最大限度地提升我们的生产力,支持我们更快、更低成本地为用户添加新特性。为了保持代码库的健康,就需要时刻留意现状与理想之间的差距,然后通过重构不断接近这个理想。

a.好代码的检验标准就是人们是否能轻而易举地修改它。
b.开展高效有序的重构,关键的心得是:
  小的步子可以更快前进,请保持代码永远处于可工作状态,小步修改累积起来也能大大改善系统的设计。

 

posted @ 2020-11-16 15:34  yangdq  阅读(501)  评论(0编辑  收藏  举报