《重构 改善既有代码的设计》之重构,第一个案例详解

本书《重构改善既有代码的设计》通过一个影片出租店的案例,展示了如何将冗长复杂的代码逐步分解,运用面向对象的原则和重构技巧,提高代码质量和可维护性。通过提炼函数、搬移代码、运用多态等手段,解决了代码职责混乱和难以扩展的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 简介

《重构 改善既有代码的设计》这本书是Martin Fowler等人撰写的一本关于代码重构的十分精彩的书籍,使用Java语言进行阐明主题,是一本非常精彩,甚至要成为优秀程序员不容错过的专业书籍。自己是2019-01-01在浙江图书馆开始看这本书,到2019-01-20在MBA405教读完这本书,感觉收获匪浅,为了防止遗忘,现在开始对这本书进行抽丝拨茧,把在阅读过程中遇到的一些问题进行摘抄出来,即是一种回顾,也是一种记忆的加深。也期待能够加深自己的理解吧。

看完这个书之后,觉得第一个案例写的真的是精彩卓绝,令人赞叹,代码并不复杂,却阐述了在过去一年半工作生活中自己完全没有注意到的事情,很震撼,也很欣慰自己在19年第一个月就读完了这样的一本书。

在江宁麒麟科技园楼下的兰州拉面馆自己开始重读第一章,绝大部分的内容是在汉庭酒店中看完的,仍然叹为观止,现在自己重新梳理一下本章的思路和步骤。

2 起点

实例非常简单。这是一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单。操作者告诉程序:顾客租了那些影片、租期多长,程序根据租赁事件和影片类型算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用,还要为常客计算积分,积分会根据租片种类是否为新片而不同。

2.1 Movie类(影片)

package chapter01.ver01;

public class Movie {
   public static final int CHILDRENS = 2;
   public static final int REGULAR = 0;
   public static final int NEW_RELEASE = 1;
   private String _title;
   private int _priceCode;

   public Movie(String title, int priceCode) {
      _title = title;
      _priceCode = priceCode;
   }

   public int getPriceCode() {
      return _priceCode;
   }

   public String getTitle() {
      return _title;
   }

   public void setPriceCode(int priceCode) {
      _priceCode = priceCode;
   }

}

Movie类只是一个单纯的数据类

2.2 Rental(租赁)

package chapter01.ver01;

public class Rental {
   private Movie _movie; // 影片
   private int _daysRented; // 租期

   public Rental(Movie movie, int daysRented) {
      _movie = movie;
      _daysRented = daysRented;
   }

   public int getDaysRented() {
      return _daysRented;
   }

   public Movie getMovie() {
      return _movie;
   }
}

Rental表示某个顾客租了一部影片

2.3 Customer(顾客)

package chapter01.ver01;

import java.util.Enumeration;
import java.util.Vector;

public class Customer {
   private String _name; // 姓名
   private Vector _rentals = new Vector(); // 租借记

   public Customer(String name) {
      _name = name;
   };

   public void addRental(Rental arg) {
      _rentals.addElement(arg);
   }

   public String getName() {
      return _name;
   }

   public String statement() {
      double totalAmount = 0; // 总消费金。
      int frequentRenterPoints = 0; // 常客积点
      Enumeration rentals = _rentals.elements();
      String result = "Rental Record for " + getName() + "\n";
      while (rentals.hasMoreElements()) {
         double thisAmount = 0;
         Rental each = (Rental) rentals.nextElement(); // 取得一笔租借记。
         // determine amounts for each line
         switch (each.getMovie().getPriceCode()) { // 取得影片出租价格
         case Movie.REGULAR: // 普通片
            thisAmount += 2;
            if (each.getDaysRented() > 2)
               thisAmount += (each.getDaysRented() - 2) * 1.5;
            break;
         case Movie.NEW_RELEASE: // 新片
            thisAmount += each.getDaysRented() * 3;
            break;
         case Movie.CHILDRENS: // 儿童。
            thisAmount += 1.5;
            if (each.getDaysRented() > 3)
               thisAmount += (each.getDaysRented() - 3) * 1.5;
            break;
         }
         // add frequent renter points (累计常客积点。
         frequentRenterPoints++;
         // add bonus for a two day new release rental
         if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE)
               && each.getDaysRented() > 1)
            frequentRenterPoints++;
         // show figures for this rental(显示此笔租借记录)
         result += "\t" + each.getMovie().getTitle() + "\t"
               + String.valueOf(thisAmount) + "\n";
         totalAmount += thisAmount;
      }
      // add footer lines(结尾打印)
      result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
      result += "You earned " + String.valueOf(frequentRenterPoints)
            + " frequent renter points";
      return result;
   }
}

Customer类用来表示顾客,与其他类一样,它也提供了访问用户姓名和增加租赁的方法。另外Customer类提供了一个用于生成详单的函数。

2.4 问题

这个程序被Martin Fowler评价设计的不好,而且很不符合面向对象精神。这主要是因为Customer类中的statement()函数做的工作太多了,既要打印foot line,也要统计总的金额,统计常积分,根据影片类型去计算单个影片应该计算的价格。Customer类里这个长长的statement()做的事情实在太多了,它做了很多原本应该由其他类完成的事情。
在这里插入图片描述
在例子中,我们的用户希望对用户做一点修改,首先它们希望以HTML格式输出详单,这样就可以直接在网页上显示,非常符合潮流。你会发现,根本不可能在打印HTML报表的函数中复用目前statement()的任何代码,你唯一可以做的事情就是重新编写一个htmlStatement()函数,大量重复statement()的行为。这便会造成duplicated code的代码坏味道。

第二个变化:用户希望改变影片分类规则,但是还没有决定怎么改,他们设想了几种方案,这些方案都会影响顾客消费和常客积分点的计算方式。但你要肯定:不论用户提出什么方案,你唯一能够获得的保证就是他们一定会在六个月内再次修改它。

为了应对这种变化,如果我们把statement函数中的代码复制到打印HTML详单的函数中,就必须确保将来的任何修改在两个地方保持一致,而这就是最不好的一种编程行为,因为它增加了未来开发的复杂度和难度,相当于给后来挖坑。

如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便的达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

3 重构的第一步

第一个步骤永远相同:我得为即将修改的代码建立一组可靠的测试环境。好的测试是重构的根本,花时间建立一个优良的测试机制是完全值得的,因为当你修改程序时,好测试会给你必要的安全保障。
重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须要有自我检验能力。
IDEA中整合JUnit4过程详细步骤

4 分解并充足statement()

重构大刀阔斧,第一个目标就是长的离谱的statement函数。
每当看到这样的函数,我就想要把它大卸八块。要知道,代码块越小,代码的功能越容易管理,代码的处理和移动也就越轻松。

4.1 switch部分提炼函数

提炼函数是最常用的重构手法之一。当看到一个过长的函数或者一段需要注释才能理解用途的代码,就要把这段代码放进一个独立的函数。
检查被提炼代码段,看看是否有任何局部变量的值被它改变。

如果一个临时变量值被修改了,看看是否可以将被提炼代码处理为一个查询,并将结果赋值给修改变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动提炼出来。你可能需要先使用 Split Temporary Variable (分解临时变量),然后再尝试提炼。也可以使用 Replace Temp with Query (以查询取代临时变量)将临时变量消灭掉。

关键是在于函数名称和函数本地之间的语义距离所以函数多长不是问题。

重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它
重构的本质就是小改动,测试,小改动,测试,由于每次修改的幅度都很小,

所以任何错误都很容易发现。你不必耗费大把时间调试,哪怕你和我一样粗心

注意:任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。

代码应该表现自己的目的,所以阅读代码的时候应该不断的进行重构,不断的把理解嵌入代码。

4.2 搬移“金额计算”代码

这部分即对应了依恋情节。

class Customerprivate double amountDor(Rental each) {
   int thisAmount = 0;
   switch (each.getMovie().get_priceCode()) {
      case Movie.REGULAR:
         thisAmount += 2;
         if (each.getDaysRented() > 2) {
            thisAmount += (each.getDaysRented() - 2) * 1.5;
         }
         break;
      case Movie.CHILDRENS:
         thisAmount += each.getDaysRented() * 3;
         break;
      case Movie.NEW_RELEASE:
         thisAmount += 1.5;
         if (each.getDaysRented() > 3) {
            thisAmount += (each.getDaysRented() - 3) * 1.5;
         }
         break;
      default:
         break;
   }
   return thisAmount;
}

观察这个amountFor(),我发现这个函数使用了来自Rental类的信息,却没有使用使用来自Customer类的信息。立刻会怀疑是否放错了位置,绝大多数情况下,函数应该放在它所使用的数据的所属对象的对象内,所以amountFor()应该移到Rental类去。为了这么做,使用Move Method 。适应新家,去掉参数,重新改名。
这样,只要改变Customer.amountFor()函数内容,让它委托调用新函数即可。

class Customer...
   private double amountDor(Rental aRental) {
      return aRental.getCharge();
   }

4.3 提炼”常客积分计算”代码

class Rentalpublic int getFrequentRenterPoints() {
   if ((getMovie().getPriceCode() == Movie.NEW_RELEASE)
         && getDaysRented() > 1)
      return 2;
   else
      return 1;
}

4.4 去除临时变量

尽量除去这一类临时变量。临时变量往往会引发问题,它们会导致大量参数被传来传去,而其实完全没有这种必要。你很容易跟丢它们,尤其在长长的函数之中更是如此。当然这么做需要付出性能上的代价,例如本例的费用就被计算了两次。
此前的临时变量有两个totalAmount, frequentRenterPoints。使用查询函数query method来取代这两个临时变量。由于totalAmount在循环内部被赋值,不得不把循环复制到查询函数中

class Customer// 译注:此即所谓query method
private double getTotalCharge() {
   double result = 0;
   Enumeration rentals = _rentals.elements();
   while (rentals.hasMoreElements()) {
      Rental each = (Rental) rentals.nextElement();
      result += each.getCharge();
   }
   return result;
}

下述代码移除了totalAmount,同时用Customer类的getTotalCharge()取代totalAmount.

public String statement() {
   int frequentRenterPoints = 0;
   Enumeration rentals = _rentals.elements();
   String result = "Rental Record for " + getName() + "\n";
   while (rentals.hasMoreElements()) {
      Rental each = (Rental) rentals.nextElement();
      frequentRenterPoints += each.getFrequentRenterPoints();
      // show figures for this rental
      result += "\t" + each.getMovie().getTitle() + "\t"
            + String.valueOf(each.getCharge()) + "\n";
   }
   // add footer lines
   result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
   result += "You earned " + String.valueOf(frequentRenterPoints)
         + " frequent renter points";
   return result;
}

之后便可在类Rental中提供getFrequentRenterPoints()函数,
这次重构存在另一个问题,那就是性能。原本代码替换两个临时变量之前只执行while循环一次,而新版本

public String statement() {
   int frequentRenterPoints = 0;
   Enumeration rentals = _rentals.elements();
   String result = "Rental Record for " + getName() + "\n";
   while (rentals.hasMoreElements()) {
      Rental each = (Rental) rentals.nextElement();
      frequentRenterPoints += each.getFrequentRenterPoints();
      // show figures for this rental
      result += "\t" + each.getMovie().getTitle() + "\t"
            + String.valueOf(each.getCharge()) + "\n";
   }
   // add footer lines
   result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
   result += "You earned " + String.valueOf(getTotalFrequentRenterPoints())
         + " frequent renter points";
   return result;
}

要执行三次。如果while循环耗时很多,就可能大大降低程序的性能。单单为了这个原因,很多程序员就不愿进行这个重构动作。但是请注意我的用词,”如果”和“可能”。除非我进行评测,否则我无法确定循环的执行时间,也无法知道这个循环是否被经常使用以至于影响系统的整体性能。重构时你不必担心这些,优化时你才需要担心它们,但那时候你已经处于一个比较有利的位置,有更多的选择可以完成有效优化。

4.5 添加htmlStatement

至此,脱下“重构”的帽子,戴上“添加功能”的帽子,可以如下添加htmlStatement函数

class Customerpublic String htmlStatement() {
   Enumeration rentals = _rentals.elements();
   String result = "<H1>Rentals for <EM>" + getName() + "</EM></ H1><P>\n";
   while (rentals.hasMoreElements()) {
      Rental each = (Rental) rentals.nextElement();
      // show figures for each rental
      result += each.getMovie().getTitle() + ": "
            + String.valueOf(each.getCharge()) + "<BR>\n";
   }
   // add footer lines
   result += "<P>You owe <EM>" + String.valueOf(getTotalCharge())
         + "</EM><P>\n";
   result += "On this rental you earned <EM>"
         + String.valueOf(getTotalFrequentRenterPoints())
         + "</EM> frequent renter points<P>";
   return result;
}

可以看到,在这种情况下,不必进行剪剪贴贴,如果计算规则发生该改变,也只需要在一处修改,完成其他类型的详单也很快而且很容易。

更深入的重构动作可以清楚主要的循环设置部分代码。可以把处理表头header,表尾footer和详单西姆的代码都分别提炼出来,使用Form Template Method,利用模板方法模式进行重构。不在此赘述了。

4.6 运用多态取代与价格相关的条件逻辑

在这一步起始地状态如下:

4.6.1 起初

这个问题的第一部分是switch语句。最好不要再另一个对象的属性基础上运用switch语句,因为如果在另一个对象使用switch,如果该类型发生了变化,需要同时修改两种类型。

class Rentalpublic double getCharge() {
   double result = 0;
   switch (getMovie().getPriceCode()) {
   case Movie.REGULAR:
      result += 2;
      if (getDaysRented() > 2)
         result += (getDaysRented() - 2) * 1.5;
      break;
   case Movie.NEW_RELEASE:
      result += getDaysRented() * 3;
      break;
   case Movie.CHILDRENS:
      result += 1.5;
      if (getDaysRented() > 3)
         result += (getDaysRented() - 3) * 1.5;
      break;
   }
   return result;
}

4.6.2 搬移函数

getMovie().getPriceCode()暗示应该把getCharge()函数移到Movie类里去。修改成如下

class Moviepublic double getCharge(int daysRented) {
   double result = 0;
   switch (getPriceCode()) {
   case Movie.REGULAR:
      result += 2;
      if (daysRented > 2)
         result += (daysRented - 2) * 1.5;
      break;
   case Movie.NEW_RELEASE:
      result += daysRented * 3;
      break;
   case Movie.CHILDRENS:
      result += 1.5;
      if (daysRented > 3)
         result += (daysRented - 3) * 1.5;
      break;
   }
   return result;
}

为了让它得以运作,我必须把租期长度作为参数传递进去。当然,租期长度来自Rental对象。计算费用时需要两项数据:租期长度和影片类型。为什么选择将租期长度传给Movie对象,而不是将影片类型传给Rental对象呢?因为本系统可能发生的变化是加入新的影片类型,这种变化带有不稳定性。如果影片类型有所变化,我希望尽量控制它造成的影响,所以选择在Movie对象中计算。
把上述计费方法放进Movie类,然后修改Rental的getCharge(),让它调用这个新函数:

class Rentaldouble getCharge() {
   return _movie.getCharge(_daysRented);
}

Move Method(搬移函数)应用场景是你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

主要思想:在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
搬移函数是重构理论的支柱。如果一个类有太多行为,或如果一个类与另外一个类有太多合作而形成高度耦合就可以使用搬移函数。

在搬移getCharge()之后,以相同手法处理常客积分计算,这样就把根据影片类型而变化的所有东西,都放到了影片类型所属的类中。

5 去除switch

在这一部分,主要是回答了2.4问题中的第二个问题,如何添加新的类型码的问题。

5.1 继承

我们有数种影片类型,它们以不同的方式回答相同的问题。这听起来很像子类的工作,因此我们可以使用如下的结构图,建立三个子类,每个都有自己的计费方法
在这里插入图片描述
但遗憾的是这里有个小问题,一部影片可以在生命期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。此时可以使用State模式。
在这里插入图片描述
加入这一层间接层,就可以在Price对象内进行子类化动作,于是可以在任何必要时刻修改价格。此时需要三种重构手法

Replace Type code with State/Strategy 将与类型相关的行为banyi 到State模式内
使用Move Method将switch语句移到Price类
最后使用Replace conditional with Polymorphism去掉switch子句

5.2 用State/Strategy取代类型码

针对类型代码使用Self Encapsulate Field,确保任何时候都能通过取值函数和设值函数来访问类型代码。多数访问操作来自其他类,它们已经在使用取值函数,但构造函数仍然直接访问价格代码。

5.2.1 添加Price继承体系

现在建立一个新类Price类,并在其中提供类型相关的行为。为了实现这一点,在Price类中加入一个抽象函数,并在所有子类中加上对应的实现:

public abstract class Price {
   abstract int getPriceCode(); // 取得价格代号
}

分别建立三个Price的子类ChildrensPrice、NewReleasePrice、RegularPrice,并在其中实现getPriceCode()方法

public class NewReleasePrice extends Price {
   int getPriceCode() {
      return Movie.NEW_RELEASE;
   }
}

5.2.2 重构priceCode相关访问函数

修改Movie类中与“priceCode”相关的访问函数(取值函数/设置函数),让它们使用新类
重构之前

public class Movie {
   private int _priceCode;
   public int getPriceCode() {
      return _priceCode;
   }

   public void setPriceCode(int priceCode) {
      _priceCode = priceCode;
   }

}

重构要使用Price类型继承层次,意味着必须在Movie类内部保存一个Price对象,而不再是一个_priceCode变量。同时要修改访问函数

public class Movie {
   private Price _price;  //关键之处,把类型码替换为价格对象

   public int getPriceCode() { // 取得价格代号
      return _price.getPriceCode();
   }

   public void setPriceCode(int arg) { // 设定价格代号
      switch (arg) {
      case REGULAR:
         _price = new RegularPrice();
         break;
      case CHILDRENS:
         _price = new ChildrensPrice();
         break;
      case NEW_RELEASE:
         _price = new NewReleasePrice();
         break;
      default:
         throw new IllegalArgumentException("Incorrect Price Code");
      }
   }
}

5.3 将getCharge()移动到Price类

对getCharge()实施Move Method,因为getCharge()函数中有switch,因此要推入Price基类中。

class Moviepublic double getCharge(int daysRented) {
   return _price.getCharge(daysRented);
}


public abstract class Price {
   abstract int getPriceCode(); // 取得价格代号

   public double getCharge(int daysRented) {
      double result = 0;
      switch (getPriceCode()) {
      case Movie.REGULAR:
         result += 2;
         if (daysRented > 2)
            result += (daysRented - 2) * 1.5;
         break;
      case Movie.NEW_RELEASE:
         result += daysRented * 3;
         break;
      case Movie.CHILDRENS:
         result += 1.5;
         if (daysRented > 3)
            result += (daysRented - 3) * 1.5;
         break;
      }
      return result;
   }

}

5.4 去掉switch子句

搬移getCharge()到Price基类之后,开始使用Replace Conditional with Polymorphism,做法是一次取出一个case分支,在相应的类中建立一个覆盖函数。先从RegularPrice类开始:

public class RegularPrice extends Price {
   int getPriceCode() {
      return Movie.REGULAR;
   }

   public double getCharge(int daysRented) {
//少于两天,该类租金2块,每多一天,租金1.5元
      double result = 2;
      if (daysRented > 2)
         result += (daysRented - 2) * 1.5;
      return result;
   }
}

这个函数覆盖了父类中的case语句,而我暂时还把后者留在原处。现在编译并测试,然后取出下一个case分支,一直保持这样直到处理完所有的case分支。
处理完所有的case分支之后,就可以把Price.getCharge()声明为abstract。

class Priceabstract double getCharge(int daysRented);

5.5 处理getFrequentRenterPoints

以同样的手法处理getFrequentRenterPoints()
重构前

class Moviepublic int getFrequentRenterPoints(int daysRented) {
//if-else可以理解为switch结构,因此可以同样的手法处理该函数
   if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
      return 2;
   else
      return 1;
}

首先把这个函数移到Price类中:

class Moviepublic int getFrequentRenterPoints(int daysRented) {
   return _price.getFrequentRenterPoints(daysRented);//还是委托
}

//仅仅做一次搬移
public abstract class Price {
   abstract int getPriceCode(); // 取得价格代号

   abstract double getCharge(int daysRented);

   public int getFrequentRenterPoints(int daysRented) {
      if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
         return 2;
      else
         return 1;
   }
}

但是这一次不把超类声明为abstract,只是为新片类型增加一个覆写函数,并在超类内保留下一个已定义的函数,使他成为一种默认的行为。

public abstract class Price {
   abstract int getPriceCode(); // 取得价格代号

   abstract double getCharge(int daysRented);

// public int getFrequentRenterPoints(int daysRented) {
//    if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
//       return 2;
//    else
//       return 1;
// }
   //不声明为abstract类,作为默认行为
   public int getFrequentRenterPoints(int daysRented){
        return 1;
    }
}

public class NewReleasePrice extends Price {
   int getPriceCode() {
      return Movie.NEW_RELEASE;
   }

   public double getCharge(int daysRented) {
      return daysRented * 3;
   }
   //对于新影片,覆盖默认行为获取自定义的常客积分计算方式
   public int getFrequentRenterPoints(int daysRented) {
      return (daysRented > 1) ? 2 : 1;
   }
}

这么做有值得吗?这么做的收获是:如果我要修改任何与价格相关的行为,或是添加新的定价标准,或是加入其他取决于价格的行为,程序的修改会容易的多。
至此,已经完成了第二个重要的重构行为,从此,修改影片分类结构,或是改变费用计算规则,改变常客积分计算规则,都容易多了。
在这里插入图片描述

6 结语

在上述的重构过程中,我们可以体会到重构的威力,所有这些行为都使的责任的分配更加合理,代码的维护更加轻松。重构后的程序风格将迥异于过程话风格,一旦你习惯了这种风格,就很难再满足于结构化风格了。
这个例子给Martin Fowler的最大的启发是重构的节奏:

测试、小修改、测试、小修改、测试、小修改

正是这种节奏让重构得以快速而安全的前进。

7 总结

之前一直赞叹Martin Fowler在撰写这本书的用心,尤其是第一章给了我深刻的印象,所以花了两个晚上整理出来这篇博客,一来希望能够梳理完整的思路,二来希望能够更多的人了解到这本《重构 改善既有代码的设计》的好处,确实能够让一个程序员在编写代码时有长足的进步。

8 参考

重构手法01:Extract Method (提炼函数)
《重构 改善既有代码的设计》之重构原则
《重构 改善既有代码的设计》之重构,第一个案例详解
《重构 改善既有代码的设计》之JUnit测试框架以及IDEA与JUnit整合
《重构 改善既有代码的设计》之代码的坏味道
《重构 改善既有代码的设计》之重构列表

9 下载

《重构 改善既有代码的设计》第一个案例详解
《重构》第一章代码和重构.pdf

一直很喜欢重构这本书,但是由于自己记性不太好,书看过之后其中的方法总是记不住,于是想如果有电子版的重构书就好了,工作中遇到重构的问题可以随时打开查阅。在网上搜索了许久,发现重构这本书有英文chm版本的,而中文版的电子书只有扫描的PDF版本,用起来非常不方便。于是萌生想做一本重构工具书的想法,本来打算自己重新将重构书的内容再整理归类一下,后来发现原书的目录编排就很适合做工具书,包括坏味道分类,重构手法归类等,都有了一个比较系统的整理。因此,我利用空余时间制作了这样的一本中文的chm版重构,希望对大家有所帮助,也算对中国软件业做出一点小小的贡献。 本书基本上是取自”重构”中文版一书的内容,但格式上参照的是chm英文版的格式,还有一些格式小修改,比如第一章的重构前后代码对比。因为时间匆促,个人能力有限,本书难免存在一些缺漏,如果大家发现有问题,随时可以给我发邮件,我会尽快更新错误的内容。 最后再次感谢几位大师 Martin Fowler、Kent Beck等,还有翻译的侯捷和熊节先生,为我们带来这么精彩的一本书。谢谢。 免责声明:本书仅供个人学习研究之用,不得用于任何商业目的,不得以任何方式修改本作品,基于此产生的法律责任本人不承担任何连带责任。
第1章 重构第一个案例 1 1.1 起点 1 1.2 重构的第一步 7 1.3 分解并重组statement() 8 1.4 运用多态取代与价格相关的条件逻辑 34 1.5 结语 52 第2章 重构原则 53 2.1 何谓重构 53 2.2 为何重构 55 2.3 何时重构 57 2.4 怎么对经理说 60 2.5 重构的难题 62 2.6 重构设计 66 2.7 重构与性能 69 2.8 重构起源何处 71 第3章 代码的坏味道 75 3.1 Duplicated Code(重复代码) 76 3.2 Long Method(过长函数) 76 3.3 Large Class(过大的类) 78 3.4 Long Parameter List(过长参数列) 78 3.5 Divergent Change(发散式变化) 79 3.6 Shotgun Surgery(霰弹式修改) 80 3.7 Feature Envy(依恋情结) 80 3.8 Data Clumps(数据泥团) 81 3.9 Primitive Obsession(基本类型偏执) 81 3.10 Switch Statements(switch惊悚现身) 82 3.11 Parallel InheritanceHierarchies(平行继承体系) 83 3.12 Lazy Class(冗赘类) 83 3.13 Speculative Generality(夸夸其谈未来性) 83 3.14 Temporary Field(令人迷惑的暂时字段) 84 3.15 Message Chains(过度耦合的消息链) 84 3.16 Middle Man(中间人) 85 3.17 Inappropriate Intimacy(狎昵关系) 85 3.18 Alternative Classes with Different Interfaces(异曲同工的类) 85 3.19 Incomplete Library Class(不完美的库类) 86 3.20 Data Class(纯稚的数据类) 86 3.21 Refused Bequest(被拒绝的遗赠) 87 3.22 Comments(过多的注释) 87 第4章 构筑测试体系 89 4.1 自测试代码的价值 89 4.2 JUnit测试框架 91 4.3 添加更多测试 97 第5章 重构列表 103 5.1 重构的记录格式 103 5.2 寻找引用点 105 5.3 这些重构手法有多成熟 106 第6章 重新组织函数 109 6.1 Extract Method(提炼函数) 110 6.2 Inline Method(内联函数) 117 6.3 Inline Temp(内联临时变量) 119 6.4 Replace Temp with Query(以查询取代临时变量) 120 6.5 Introduce Explaining Variable(引入解释性变量) 124 6.6 Split Temporary Variable(分解临时变量) 128 6.7 Remove Assignments to Parameters(移除对参数的赋值) 131 6.8 Replace Method with Method Object(以函数对象取代函数) 135 6.9 Substitute Algorithm(替换算法) 139 第7章 在对象之间搬移特性 141 7.1 Move Method(搬移函数) 142 7.2 Move Field(搬移字段) 146 7.3 Extract Class(提炼类) 149 7.4 Inline Class(将类内联化) 154 7.5 Hide Delegate(隐藏“委托关系”) 157 7.6 Remove Middle Man(移除中间人) 160 7.7 Introduce Foreign Method(引入外加函数) 162 7.8 Introduce Local Extension(引入本地扩展) 164 第8章 重新组织数据 169 8.1 Self Encapsulate Field(自封装字段) 171 8.2 Replace Data Value with Object(以对象取代数据值) 175 8.3 Change Value to Reference(将值对象改为引用对象) 179 8.4 Change Reference to Value(将引用对象改为值对象) 183 8.5 Replace Array with Object(以对象取代数组) 186 8.6 Duplicate Observed Data(复制“被监视数据”) 189 8.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) 197 8.8 Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) 200 8.9 Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) 204 8.10 Encapsulate Field(封装字段) 206 8.11 Encapsulate Collection(封装集合) 208 8.12 Replace Record with Data Class(以数据类取代记录) 217 8.13 Replace Type Code with Class(以类取代类型码) 218 8.14 Replace Type Code with Subclasses(以子类取代类型码) 223 8.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码) 227 8.16 Replace Subclass with Fields(以字段取代子类) 232 第9章 简化条件表达式 237 9.1 Decompose Conditional(分解条件表达式) 238 9.2 Consolidate Conditional Expression(合并条件表达式) 240 9.3 Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 243 9.4 Remove Control Flag(移除控制标记) 245 9.5 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式) 250 9.6 Replace Conditional with Polymorphism(以多态取代条件表达式) 255 9.7 Introduce Null Object(引入Null对象) 260 9.8 Introduce Assertion(引入断言) 267 第10章 简化函数调用 271 10.1 Rename Method(函数改名) 273 10.2 Add Parameter(添加参数) 275 10.3 Remove Parameter(移除参数) 277 10.4 Separate Query from Modifier(将查询函数和修改函数分离) 279 10.5 Parameterize Method(令函数携带参数) 283 10.6 Replace Parameter with Explicit Methods(以明确函数取代参数) 285 10.7 Preserve Whole Object(保持对象完整) 288 10.8 Replace Parameter with Methods(以函数取代参数) 292 10.9 Introduce Parameter Object(引入参数对象) 295 10.10 Remove Setting Method(移除设值函数) 300 10.11 Hide Method(隐藏函数) 303 10.12 Replace Constructor with Factory Method(以工厂函数取代构造函数) 304 10.13 Encapsulate Downcast(封装向下转型) 308 10.14 Replace Error Code with Exception(以异常取代错误码) 310 10.15 Replace Exception with Test(以测试取代异常) 315 第11章 处理概括关系 319 11.1 Pull Up Field(字段上移) 320 11.2 Pull Up Method(函数上移) 322 11.3 Pull Up Constructor Body(构造函数本体上移) 325 11.4 Push Down Method(函数下移) 328 11.5 Push Down Field(字段下移) 329 11.6 Extract Subclass(提炼子类) 330 11.7 Extract Superclass(提炼超类) 336 11.8 Extract Interface(提炼接口) 341 11.9 Collapse Hierarchy(折叠继承体系) 344 11.10 Form Tem Plate Method(塑造模板函数) 345 11.11 Replace Inheritance with Delegation(以委托取代继承) 352 11.12 Replace Delegation with Inheritance(以继承取代委托) 355 第12章 大型重构 359 12.1 Tease Apart Inheritance(梳理并分解继承体系) 362 12.2 Convert Procedural Design to Objects(将过程化设计转化为对象设计) 368 12.3 Separate Domain from Presentation(将领域和表述/显示分离) 370 12.4 Extract Hierarchy(提炼继承体系) 375 第13章 重构,复用与现实 379 13.1 现实的检验 380 13.2 为什么开发者不愿意重构他们的程序 381 13.3 再论现实的检验 394 13.4 重构的资源和参考资料 394 13.5 从重构联想到软件复用和技术传播 395 13.6 小结 397 13.7 参考文献 397 第14章 重构工具 401 14.1 使用工具进行重构 401 14.2 重构工具的技术标准 403 14.3 重构工具的实用标准 405 14.4 小结 407 第15章 总结 409
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值