[关闭]
@suyuening 2017-01-22T06:58:56.000000Z 字数 4826 阅读 1420

重构

关于重构的一些讨论和示例

什么是重构?

所谓重构是这样一个过程:「在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构」。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法,可以将整理过程中不小心引入错误的机率降到最低。本质上说,重构就是「在代码写好之后改进它的设计」。

何谓重构

第一个定义是名词形式:

重构(名词):对软件内部结构的一种调整,目的是在不改变「软件可观察行为」前提下,提高其可理解性,降低其修改成本。

「重构」的另一个用法是动词形式:

重构(动词):使用一系列重构准则(手法〕,在不改变「软件可观察行为」前提 下,调整其结构。
所以,在软件开发过程中,你可能会花上数小时的时间进行重构,其间可能用上数十个不同的重构准则。

何时重构

三次法则〔The Rule of Three〕

Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是做了;第三次再做类似的事,你就应该重构。

事不过三,三则重构(Three strikes and you refactor)

添加功能时一并重构

最常见的重构时机就是我想给软件添加新特性的时候。此时,重构的第一个原因往往是为了帮助我理解需要修改的代码。这些代码可能是别人写的,也可能是我自己写的。无论何时只要我想理解代码所做的事,我就会问自己:是否能对这段代码进行重构,使我能更快理解它。 然后我就会重构。

代码的坏味道

Duplicated Code(重复的代码)

臭味行列中首当其冲的就是Duplicated Code。如果你在一个以上的地点看到相同的程序结构,那么当可肯定:设法将它们合而为一,程序会变得更好。

Long Method(过长函数)

拥有[短函数」(short methods)的对象会活得比较好、比较长。不熟悉面向对象技术的人,常常觉得对象程序中只有无穷无尽的delegation(委托),根本没有进行任何计算。和此类程序共同生活数年之后,你才会知道,这些小小函数有多大价值。 的匹配项

Large Class(过大类)

如果想利用单一class做太多事情,其内往往就会出现太多instance变量。一旦如此,Duplicated Code也就接踵而至了。

Long Parameter List(过长参数列)

刚开始学习编程的时候,老师教我们:把函数所需的所有东西都以参数传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据是邪恶的东西。对象 技术改变了这一情况,因为如果你手上没有你所需要的东西,总可以叫另一个对象给你。因此,有了对象,你就不必把函数需要的所有东西都以参数传递给它了,你只需传给它足够的东西、让函数能从中获得自己需要的所有东西就行了。函数需要的东西多半可以在函数的宿主类(host class)中找到。面向对象程序中的函数,其参数列通常比在传统程序中短得多。

Divergent Change(发散式变化)

我们希望软件能够更容易被修改——毕竟软件再怎么说本来就该是「软」的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻味道中的一种了。
如果某个class经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。当你看着一个class说:『呃,如果新加入一个数据库,我必须修改这三个函数;如果新出现一种金融工具,我必须修改这四个函数』,那么此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因一种变化而需要修改。当然,往往只有在加入新数据库或新金融工具后,你才能发现这一点。针对某一外界 变化的所有相应修改,都只应该发生在单一class中,而这个新class内的所有内容都应该反应该外界变化。为此,你应该找出因着某特定原因而造成的所有变化,然后运用Extract Class 将它们提炼到另一个class中。

Comments(过多的注释)

别担心,我们并不是说你不该写注释。从嗅觉上说,Comments不是一种坏味道;事实上它们还是一种香味呢。我们之所以要在这里提到Comments,因为人们常把它当作除臭剂来使用。常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实 在令人吃惊。

这里只是列举了代码坏味道的几个方面,更多的内容需要阅读《重构改善既有代码的设计》一书。

构筑测试体系

自我测试代码的价值

  1. 每个程序员都能讲出「花一整天(甚至更多)时间只找出一只小小臭虫」的故事。
  2. 『class应该包含它们自己的测试代码。』这激发了我的灵感,让我想到一种组织测试的好方法。

确保所有测试都完全自动化,让它们检查自己的测试结果。
一整组(a suite of)测试就是一个强大的「臭虫」侦测器,能够大大缩减查找「臭虫」所耑要的时间。

JUnit测试框架

我用的是JUnit,一个由Erich Gamma 和 Kent Beck [JUnit]开发的开放源码测试框架。这个框架非常简单,却可让你进行测试所需的所有重要事情。本章中我将运用这个测试框架来为一些IO classes幵发测试代码。

重构列表

Extract Method(提炼函数)

示例

你有一段代码可以被组织在一起并独立出来。
将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。

  1. void printOwing(double amount) {
  2. printBanner();
  3. //print details
  4. System.out.println ("name:" + _name);
  5. System.out.println ("amount" + amount);
  6. }

=>

  1. void printOwing(double amount) {
  2. printBanner();
  3. printDetails(amount);
  4. }
  5. void printDetails (double amount) {
  6. System.out.println ("name:" + _name);
  7. System.out.println ("amount" + amount);
  8. }

操作步骤

没有特别说明示例的操作步骤在Eclipse中完成

  1. 选中要抽取成函数的代码
  2. 选择菜单Refactor->Extract Method,弹出Extract Method对话框
  3. 在Extract Method对话框输入方法名,编辑参数列表(可选),然后按下回车即可

Eclipse快捷键 : ALT+SHIFT+M

Inline Method(将函数内联化)

示例

一个函数,其本体(method body)应该与其名称(method name)同样清楚易懂。
在函数调用点插入函数本体,然后移除该函数。

  1. int getRating() {
  2. return (moreThanFiveLateDeliveries()) ? 2 : 1;
  3. }
  4. boolean moreThanFiveLateDeliveries() {
  5. return _numberOfLateDeliveries > 5;
  6. }

=>

  1. int getRating() {
  2. return (_numberOfLateDeliveries > 5) ? 2 : 1;
  3. }

操作步骤

  1. 选中要内联的函数,把光标放点到函数名即可
  2. 选择菜单Refactor->Inline,弹出Inline对话框
  3. 在Inline对话框,然后按下回车即可

Eclipse快捷键 : ALT+SHIFT+I

Inline Temp(将临时变量内联化)

你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
将所有对该变量的引用动作,替换为对它赋值的那个表达式本身。

  1. double basePrice = anOrder.basePrice();
  2. return (basePrice > 1000)

=>

  1. return (anOrder.basePrice() > 1000)

操作步骤

  1. 把basePrice 替换成anOrder.basePrice()
  2. 删除double basePrice = anOrder.basePrice();即可

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

示例

你的程序以一个临时变量(temp)保存某一表达式的运算结果。
将这个表达式提炼到一个独立函数(译注:所谓查询式,query)中。
将这个临时变量的所有「被引用点」替换为「对新函数的调用」。新函数可被其他函数使用。

  1. double basePrice = _quantity * _itemPrice;
  2. if (basePrice > 1000)
  3. return basePrice * 0.95;
  4. else
  5. return basePrice * 0.98;

=>

  1. if (basePrice() > 1000)
  2. return basePrice() * 0.95;
  3. else
  4. return basePrice() * 0.98;
  5. ...
  6. double basePrice() {
  7. return _quantity * _itemPrice;
  8. }

操作步骤

  1. 用鼠标选中_quantity * _itemPrice,按下ALT+SHIFT+M,输入basePrice,回车
  2. 把使用basePrice变量的地方改成basePrice()
  3. 删除double basePrice = basePrice();

Introduce Explaining Variable(引入解释性变量)

示例

你有一个复杂的表达式。
将该表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

  1. if ( (platform.toUpperCase().indexOf("MAC") > -1) &&
  2. (browser.toUpperCase().indexOf("IE") > -1) &&
  3. wasInitialized() && resize > 0 )
  4. {
  5. // do something
  6. }

=>

  1. final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
  2. final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
  3. final boolean wasResized = resize > 0;
  4. if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
  5. // do something
  6. }

操作步骤

  1. 用鼠标选中(platform.toUpperCase().indexOf("MAC") > -1),按下ALT+SHIFT+L,弹出Extract Local Variable对话框
  2. 在Extract Local Variable对话框的Variable name输入isMacOs,回车,然后给isMacOs变量加上final修饰符
  3. 对(browser.toUpperCase().indexOf("IE") > -1)和resize > 0执行上述相同的操作

ALT+SHIFT+L,此快捷键用来抽取本地变量,相当于执行菜单Refactor->Extract Local Variable

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注