重构: 改善既有代码的设计

重构这本书由著名的世界软件开发大师Martin Fowler编写,是软件开发领域的经典书籍。书中的部分内容在refactoring.com上也有提及。

重构: 改善既有代码的设计

什么是重构

视上下文不同,重构有两个定义:

  • 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本
  • 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构

为什么要重构

重构是个工具,它可以用于以下几个目的:

  • 重构改进软件设计
  • 重构使软件更容易理解
  • 重构帮助找到bug
  • 重构提高编程速度

何时重构

不需要专门拨出时间进行重构,重构应该随时随地进行。你之所以重构,是因为你想做别的什么事,而重构可以帮助你把那些事做好。

  • 事不过三,三则重构
  • 添加功能时重构
  • 修补错误时重构
  • 复审代码时重构

何时不该重构

  • 当既有代码实在太混乱,重构不如重写来得简单
  • 当项目已接近最后期限,应该避免进行重构,因为已经没有时间了

代码的坏味道

「如果尿布臭了,就换掉它」。代码的坏味道指出了重构的可能性。

  • 重复代码 (Duplicated Code)
  • 过长函数 (Long Method)
  • 过大的类 (Large Class)
  • 过长参数列 (Long Parameter List)
  • 发散式变化 (Divergent Change)
  • switch语句 (Switch Statements)
  • 中间人 (Middle Man)
  • 异曲同工的类 (Alternative Classes with Different Interfaces)
  • 过多的注释 (Comments)
  • ...

构筑测试体系

重构的基本技巧「小步前进,频繁测试」已经得到了多年的实践检验。因此如果你想进行重构,首要前提就是拥有一个可靠的测试体系。

常用重构方法

提炼函数 (Extract Method)

当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立函数中

void printOwing() {
  printBanner();

  //print details
  System.out.println ("name:  " + _name);
  System.out.println ("amount " + amount);
}
void printOwing() {
  printBanner();
  printDetails(amount);
}

void printDetails (double amount) {
  System.out.println ("name:  " + _name);
  System.out.println ("amount " + amount);
}

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

表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。

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

分解临时变量 (Split Temporary Variable)

如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会令代码阅读者糊涂。

  double temp = 2 * (_height + _width);
  System.out.println (temp);
  temp = _height * _width;
  System.out.println (temp);
  final double perimeter = 2 * (_height + _width);
  System.out.println (perimeter);
  final double area = _height * _width;
  System.out.println (area);

移除对参数的赋值 (Remove Assignments to Parameters)

我之所以不喜欢(对参数赋值)这样的做法,因为它降低了代码的清晰度,而且混淆了按值传递和按引用传递这两种参数传递方式。
当然,面对那些使用「输出式参数」(output parameters)的语言,你不必遵循这条规则。不过在那些语言中我会尽量少用输出式参数。

int discount (int inputVal, int quantity, int yearToDate) {
    if (inputVal > 50) {
        inputVal -= 2;
    }
}
int discount (int inputVal, int quantity, int yearToDate) {
    int result = inputVal;
    if (inputVal > 50) {
        result -= 2;
    }
}

提炼类 (Extract Class)

某个类做了应该由两个类做的事。
此时你需要考虑哪些部分可以分离出去,并将它们分离到一个单独的类中。

提炼类

移除中间人 (Remove Middle Man)

每当客户要使用受托类的新特性时,你就必须在服务端添加一个简单委托函数。随着受托类的特性(功能)越来越多,这一过程会让你痛苦不已。服务类完全变成了一个“中间人”,此时你就应该让客户直接调用受托类。

移除中间人

以字面常量取代魔法数 (Replace Magic Number with Symbolic Constant)

所谓魔法数(magic number)是指拥有特殊意义,却又不能明确表现出这种意义的数字。如果你需要在不同的地点引用同一个逻辑数,魔法数会让你烦恼不已,因为一旦这些数发生改变,你就必须在程序中找到所有魔法数,并将它们全部修改一遍,这简直就是一场噩梦。就算你不需要修改,要准确指出每个魔法数的用途,也会让你颇费脑筋。

double potentialEnergy(double mass, double height) {
  return mass * 9.81 * height;
}
double potentialEnergy(double mass, double height) {
  return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;

分解条件表达式 (Decompose Conditional)

程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。你必须编写代码来检查不同的条件分支、根据不同的分支做不同的事,然后你很快就会得到一个相当长的函数。
对于条件逻辑,将每个分支条件分解成新函数可以给你带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。

if (date.before (SUMMER_START) || date.after(SUMMER_END))
  charge = quantity * _winterRate + _winterServiceCharge;
else charge = quantity * _summerRate;
if (notSummer(date))
  charge = winterCharge(quantity);
else charge = summerCharge (quantity);

合并条件表达式 (Consolidate Conditional Expression)

之所以要合并条件代码,有两个重要原因。首先,合并后的条件代码会告诉你“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。其次,这项重构往往可以为你使用提炼函数(Extract Method)做好准备。将检查条件提炼成一个独立函数对于厘清代码意义非常有用,因为它把描述“做什么”的语句换成了“为什么这样做”。

double disabilityAmount() {
  if (_seniority < 2) return 0;
  if (_monthsDisabled > 12) return 0;
  if (_isPartTime) return 0;
  // compute the disability amount
double disabilityAmount() {
  if (isNotEligableForDisability()) return 0;
  // compute the disability amount

合并重复的条件片段 (Consolidate Duplicate Conditional Fragments)

有时你会发现,一组条件表达式的所有分支都执行了相同的某段代码。如果是这样,你就应该将这段代码搬移到条件表达式外面。这样,代码才能更清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变。

if (isSpecialDeal()) {
  total = price * 0.95;
  send();
}
else {
  total = price * 0.98;
  send();
}
if (isSpecialDeal())
  total = price * 0.95;
else
  total = price * 0.98;
send();

移除控制标记 (Remove Control Flag)

人们之所以会使用这样的控制标记,因为结构化编程原则告诉他们:每个子程序只能有一个入口和一个出口。我赞同“单一入口”原则(而且现代编程语言也强迫我们这样做),但是“单一出口”原则会让你在代码中加入讨厌的控制标记,大大降低条件表达式的可读性。这就是编程语言提供break语句和continue语句的原因:用它们跳出复杂的条件语句。去掉控制标记所产生的效果往往让你大吃一惊:条件语句真正的用途会清晰得多。

boolean checkSecurity(String[] people) {
  boolean found = false;
  for (int i = 0; i < people.length; i++) {
    if (!found){
      if (people[i].equals("Don")) {
        sendAlert();
        found = true;
      }
      if (people[i].equals("John")) {
        sendAlert();
        found = true;
      }
    }
  }
  return found;
}
boolean checkSecurity(String[] people) {
  for (int i = 0; i < people.length; i++) {
    if (!found){
      if (people[i].equals("Don")) {
        sendAlert();
        return true;
      }
      if (people[i].equals("John")) {
        sendAlert();
        return true;
      }
    }
  }
  return false;
}

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

如果条件表达式的两条分支都是正常行为,就应该使用形如if…else…的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”(guard clauses)。

这个方法的精髓是:给某一条分支以特别的重视。它告诉阅读者:这种情况很罕见,如果它真地发生了,请做一些必要的整理工作,然后退出。

“每个函数只能有一个入口和一个出口”的观念,根深蒂固于某些程序员的脑海里。现今的编程语言都会强制保证每个函数只有一个入口,至于“单一出口”规则,其实不是那么有用。保持代码清晰才是最关键的:如果单一出口能使这个函数更清晰易读,那么就使用单一出口;否则就不必这么做。

double getPayAmount() {
  double result;
  if (_isDead) result = deadAmount();
  else {
    if (_isSeparated) result = separatedAmount();
    else {
      if (_isRetired) result = retiredAmount();
      else result = normalPayAmount();
    };
  }
  return result;
}; 
double getPayAmount() {
  if (_isDead) return deadAmount();
  if (_isSeparated) return separatedAmount();
  if (_isRetired) return retiredAmount();
  return normalPayAmount();
};

扩展阅读:关于如何重构嵌套条件表达式,可以阅读如何重构“箭头型”代码,这篇文章更深层次地讨论了这个问题。

将查询函数和修改函数分离 (Separate Query from Modifier)

下面是一条好规则:任何有返回值的函数,都不应该有看得到的副作用。

如果你遇到一个“既有返回值又有副作用”的函数,就应该试着将查询动作从修改动作中分割出来。

将查询函数和修改函数分离
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容