潜 渊

关于大恒的一切

0%

【读书】重构-改善既有代码的设计

作者:Martin Fowler

感谢Shawn推荐。

提升程序员的自我修养的第一课:学习重构代码。

第一章:重构,从一个例子谈起

对于重构这个宽泛的话题,作者并没有一上来就给出关于重构明确的定义,而是通过直接重构一段代码来向读者解释什么是重构。

书中例子是实现计算演出的费用及积分的功能,其中设计的类有Play, Performance, 每场演出会根据剧目和人数的不同而有相应的计算费用的方式,不同费用又会产生相应的积分,输出是一张账单。原始代码将所有逻辑放在一个方法中,方法遍历每一个performance,并通过switch-case来实现针对不同种类的演出的计价逻辑。

重构的信号

当眼前的代码结构让你无法轻而易举的为其添加新的功能,是时候考虑先对其重构,再去添加新的功能。

不是所有的代码都值得花时间去重构,这里主要指的是那些比较大型的工程项目,需要多人协同开发,并需要不断更新,添加新的功能的项目,重构的目的是为了让代码更短或者更高效,而是更容易读懂、维护和扩展。

在重构之前

在重构之前,首先要拥有一套靠谱的测试。

作者在书的第一章反复强调单元测试和小修改的重要性。小修改可以保证错误的可控,可回溯,测试则将人为带来的失误降到最低,虽然小修改->测试->小修改->测试 这种模式十分耗时且枯燥,但随着项目规模的扩大,前期“浪费”的时间可以大大降低后期维护的成本。

重构的步骤

作者分三步对原始代码进行重构:用子函数增强结构,分离计算和排版,利用多态代替条件。

子函数增强结构

这一步是为了是让代码有更强的可读性,与其通过注释来解释下面一段代码的功能,不如将完成一项任务的代码封装到一个方法中(并添加相应的测试),然后通过一个清晰的方法名来说明他的功能。从而得到如下结构:

function statement…

1
2
3
4
5
6
var result
for performance in performances:
result += formatline(amountFor(performance))
result += formatline(totalAmount(performaces))
result += formatline(totalCredits())
return result

其中

  • amountFor(performance) 用来计算每场演出的费用
  • totalAmount(performances) 用来计算总费用
  • totalCredits(performances) 用来计算总积分

这一步用到了很多重构的概念,以后会逐个进行解释,包括: Extract Function,Replace Temp with Query,Inline variable,Change function declaration,Split loop,Slide statements

分离计算和排版

注意到上面的例子,最终输出是一个经过排版的账单,这个账单的形式目前的要求仅仅是字符串,但未来可能是Html或者任何其他形式。如果可以将数据和排版形式去耦合,可以进一步提高代码的灵活性。从而得到如下结构

function statement…

1
return renderPlainText(createStatementData(invoice, plays));

其中

  • createStatementData()用来生成一个数据实体,包含通过invoiceplays计算得到的结果
  • *renderPlainText()*将数据实体渲染成所需要的格式

这样通过一个StatementData实现了计算和排版阶段的去耦合

多态代替条件

多态(polymorphic)是面向对象编程的重要概念,根据不同对象实例,同一行为可以有不同的表现形式。就本例而言,不同的剧目类型有着不同的价格计算方式,与其通过条件判断来进行区分,可以通过多态的概念来实现计算价格方法与类型之间的去耦合。具体来看,在根据演出计算费用那一步,引入PerformanceCalculator这一概念,并定义其两个子类:

  1. TragedyCalculator
  2. ComedyCalculator

PerformanceCalculator 实现 amount(), *credit()*两个方法,并通过工厂 *createPerformanceCalculator()*生产出来:

1
2
3
switch(aPlay.type):
case "tragedy" : return new TragedyCalculator(aPerformance, aPlay);
case "comedy" : return new ComedyCalculator(aPerformance, aPlay);

这样createStatementData()函数只要根据performance类型生产一个calculator,并用它来产生账单就可以了,而不需要

根据具体performance 类型来做判断。

总结

易于修改的代码才是好的代码

这一章的例子让我初步了解到了重构是什么,其实我们日常工作中经常面临重构,小到使用一个helper方法,大到套用复杂的设计模式,其根本目的,是为了写出容易测试,维护,协同开发的代码,有时候写一个通俗易懂的逻辑,比减少一个循环意义大得多,这也是企业级代码有别于个人代码的地方。

第二章:重构的原则

本章从具体讨论了重构的定义,为什么要重构,重构过程中会遇到的问题等。

什么是重构

重构是一项对代码进行修改的行为,重构的目的是为了增强代码的可读性、易用性、可扩展性,这一点已经被作者反复提及。同时,重构应该是循序渐进的若干微小改动,重构的过程应该保持代码的可用性。

如果有人说他为了重构一段代码,让其好几天都不可用,这不是重构。

两顶帽子

增加新的功能重构老的代码就像两顶帽子,应该在我们开发的过程中不断交替。

为什么要重构

  • 优化设计结构
  • 让代码易于理解
  • 易于发现问题
  • 提高开发效率

重构会遇到的问题

  1. 老板不理解。重构不会带来新的功能,又要花费时间,听起来是费力不讨好的事情,这个时候可以尝试去解释重构可以为将来的代码迭代带来方便,节省更多的时间,当然,如果老板还是无法理解,就不要告诉他好了。
  2. 拖慢开发进度, ownership,Branches, testsing, legacy code, databases(略过…)

第三章:坏味道

不是所有的代码都需要或值得花时间去重构,本章罗列出一些坏代码的特征,作为重构的信号。

迷之命名

好的方法名可以让人不用通读代码而知道其背后的功能,从而大大提高代码的可理解性。另一方面,命名也是代码的试金石,如果你感觉很难为一个方法抽象出一个简单明了的名称,大概率是因为这个代码的职能定义不清。

重复代码

重复会大大影响了代码的可维护性和理解性,同时增加测试成本,浪费存储资源。

过长方法/类

过长的结构带来的问题:

  1. 降低理解性: 过长的结构说明其内部做了很多事情,如果事情太多就不容易理解和抽象,因此需要将其进一步拆解。
  2. 难以复用:越长的代码功能约单一,不容易复用
  3. 难以维护:所谓维护,包括修改和测试两部分,较长的代码中逻辑状态比较复杂,修改起来容易出错,也需要依赖同样复杂的测试用例来覆盖所有情况

理想的构造方法或者类的原则应该是内紧外松,小巧轻量

  • 所谓内紧就是内聚性强,功能单一,状态单一,内部的所有变量应该都要被调动起来
  • 所谓外松就是方法不应该依赖过多外部的参数,这里也顺便提到了过长参数的问题,类似的,我们可以通过封装参数,用方法替代参数等方法来保证参数的精简。

可变数据(mutable data)

数据可以被不受控制的修改终将酿成灾难。所以要严格控制修改数据的方法。比如Encapsulate Variable 来保证修改操作只能通过受限的方法来实现。

发散修改(Divergent Change)

一种“责任耦合”现象,造成发散式修改的原因是违反了Single Responsibility Principle(SRP) 即单一功能原则,其结果是”一个类别会因为多种不同的原因而造成改变“。这里引用“搞笑谈软工”的解释,责任耦合造成的影响是:

  • Modifiability(修改性):一個類別會因為多種原因而一直需要被修改,代表不必要的責任耦合或是說缺乏內聚力。例如,一個Account類別除了表達銀行帳戶以外,還需要負責將自己存入資料庫,以及輸出到印表機。因此,帳戶欄位改變、資料儲存方式改變、列印方式改變,都會造成這個Account改變。
  • Understandability(可理解性):責任耦合的類別比較不容易被理解,因為閱讀程式碼的人需要自行判斷哪些instance variable與method是被哪一種責任所使用。
  • Testability(可測性):責任耦合的類別其邏輯相較於單一責任的類別來的複雜,也比較不易測試。此外,因為有多種原因會造成同一類別不斷被修改,因此也增加了可能需要伴隨修改測試案例的機會。
  • Reusability(重複使用性):一個負擔太多責任的類別通常隱含它和所處的context有比較緊密的關聯性,因此也比較不容易在其他的context中被重複使用。

散弹修改(Shotgun Surgery)

与「发散修改」的「责任耦合」相反,散弹修改是「责任分散」,即某种修改会横跨多个类别。例如每个类都包含数据库连接和写入的功能,如果这个连接的参数改变,就需要逐个修改每个类的该参数。当你再改动代码的时候遇到「牵一发而动全身」的问题的时候,就要警惕是否犯了散弹修改的错误。

特征依赖

如果一个模块中的函数被另一个模块中的方法调用的频率高于本地,就应该考虑将它放回该有的位置。一个例外,比如Gang of Four设计模式 中的Strategy and Visitor就会运用到「让变化只发生在一处」的思想,以牺牲调用距离来换取修改的便利。

数据泥团(Data Clumps)

特指总是一起出现的数据,比如XY坐标,meta data,config data中的各个fields。对于数据泥团,应该通过「Extract Class」等方法将其封装。数据泥团可视为一种重复数据,long parameter也可视作数据泥团的一种表现形式。

原始类型偏执

要勤于定义新的类型来反映数据的真实特征,而不是用String和int来描述一切,比如用String来记录电话号码,就丢掉了很多属于电话号码本身的信息和独有的方法。

Switch

程序中应该减少Switch的出现。Switch的本质是根据一个变量的不同内容而运行不同逻辑,可以想象这样的判断会在多处反复发生,频繁使用Switch会带来重复。同时switch的代码通常比较长,内部逻辑关系复杂,为代码的测试带来困难。

Loops

Loop是编程的核心手段之一,作者这里的意思是可以用pipeline来替代loop。

这里仅列举一些感觉比较重要或者是工作中遇到的怀味道,书中还有很多其他的坏味道我会在日后补充。

第四章:测试

(未完待续)