重构-改善既有代码的设计-书摘

重构-改善既有代码的设计-书摘

重构是在不改变软件可观察行为的前提下改善其内部结构。
设计模式为重构行为提供了目标,模式是你希望到达的目标,重构则是到达之路。

重构,第一个案例

  • 如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
  • 重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力。
  • 重构技术就是以微小的步伐修改程序,如果你犯下错误,很容易便可以发现它。
  • 任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。

重构原则

  • 三次法则:事不过三,三则重构。
  • 不要过早的发布接口,请修改你的代码所有权政策,使重构更顺畅。

代码的坏味道

  • 重复代码(Duplicated Code)
    • 多于一处的相同代码
  • 过长函数(Long Method)
    • 每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途命名。
    • 条件表达式和循环常常是提炼的信号。
  • 过大的类(Large Class)
    • 大类内往往出现太多实例变量,可以将彼此相关的变量提炼至新类。
  • 过长参数列(Long Parameter List)
  • 发散式变化(Divergent Change)
    • 一个类受多种变化影响
  • 散弹式修改(Shotgun Surgery)
    • 一种变化引发多个类相应修改
  • 依恋情结(Feature Envy)
    • 哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据放一起。
  • 数据泥团(Data Clumps)
    • 删掉众多数据中的一项,如果其他数据不再有意义,你应该把这些数据封装成一个新的对象。
  • 基本类型偏执(Primitive Obsession)
  • switch惊悚现身(Switch Statements)
    • 少用switch case,可以用多态替代。
  • 平行继承体系(Parallel Inheritance Hierarchies)
  • 冗赘类(Lazy Class)
  • 夸夸其谈未来性(Speculative Generality)
  • 令人迷惑的暂时字段(Temporary Filed)
    • 将临时变量放在实例变量
  • 过渡耦合的消息链(Message Chains)
  • 中间人(Middle Man)
  • 过渡亲昵(Inappropriate Intimacy)
  • 异曲同工的类(Alternative Classes with Different Interfaces)
  • 不完美的库类(Incomplete Library Class)
  • 纯稚的数据类(Data Class)
  • 被拒绝的遗赠(Refused Bequest)
  • 过多的注释(Comments)
    • 当你感觉需要注视时,请先尝试重构,试着让所有注释变得多余。

构筑测试体系

  • 确保所有测试都完全自动化,让它们检查自己的测试结果。
  • 一套测试就是一个强大的bug侦查器,能够大大缩减查找bug所需要的时间。
  • 每当你受到bug报告,请先写一个单元测试来暴露bug。
  • 考虑可能出错的边界条件,把测试火力集中在那儿。

重新组织函数

Extract Method最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。

  • 内联临时变量

    • 示例:
      1
      2
      3
      4
      double basePrice = anOrder.hasePrice();
      return (basePrice > 100)
      //inline
      return (anOrder.basePrice() > 1000)
  • 运用Replace Temp with Query去掉所有可去掉的临时变量。

    • 由于临时变量只在所属的函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替代为一个查询,那么同一个类中的所有函数都可以获得这份信息。
    • 示例:
      1
      2
      3
      4
      5
      double basePrice = quantity * itemPrice;
      //extract
      double basePrice(){
      return quantity * itemPrice;
      }
  • 如果表达式非常复杂而难以阅读,引入解释性变量Introduce Explaining Variable

    • 我更倾向于提炼成函数,因为可以被所有函数使用。
    • 示例:
      1
      2
      3
      4
      5
      6
      if(platform.toUpperCase().indexOf("MAC") > -1){
      ...
      //extract
      final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
      if(isMaOs){
      ...
  • 如果临时变量被赋值超过一次(处理循环变量和结果收集变量),就应该分解临时变量Split Temporary Variable

    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      double temp = 1.0;
      System.out.println(temp);
      temp = 2.0;
      System.out.println(temp);
      //replace
      double a = 1.0;
      System.out.println(a);
      double b = 2.0;
      System.out.println(b);
  • 如果你在函数内对参数赋值,就得使用Remove Assignments to Parameters

    • 因为对参数赋值降低了代码的清晰度,而且混用了按值传递和按引用传递。
    • 示例:
      1
      2
      3
      4
      5
      6
      int discount (int inputVal, int quantity, int yearToDate) {
      if(inputVal > 50) inputVal -=2;
      //replace
      int discount (int inputVal, int quantity, int yearToDate) {
      int result = inputVal;
      if(inputVal > 50) result -=2;
  • 如果临时变量太混乱,难以替换。使用Replace Method with Method Object。代价是引入一个新类。

  • 替换算法Substitute Algorithm。使用简洁高效的算法。

提炼函数的优点:

  • 函数细粒度越小,复用的机会就越大。
  • 使高层函数读起来就像一系列注释。
  • 函数细粒度越小,覆写也会越容易些。

在对象之间搬移特性

  • 如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就需要搬移函数Move Method
    • 做法:在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全的移除。
  • 如果发现,对于一个字段,在其所在的类之外的另一个类中有更多函数使用了它,就应该考虑搬移这个字段Move Filed
    • 做法:在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。
  • 某个类做了应该由两个类做的事,就需要提炼类Extract Class
    • 做法:建立一个新类,将相关的字段和函数从旧类搬移到新类。
  • 某个类没有做太多事情,就将这个类的所有特性搬移到另一个类中,然后移除原类。
  • 如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户必须知晓这一层委托关系。万一委托关系发生变化,客户也得相应变化。你需要隐藏委托关系Hide Delegate

    • 做法:在服务类上建立客户所需的所有函数,用以隐藏委托关系。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      //人
      class Person{
      Department department;
      //getter and setter
      }
      //部门
      class Department {
      private String chargeCode;
      private Person manager;
      //getter and setter
      }
      //如果客户需要知道某人的经理是谁,必须先取得Department对象;
      manager = john.getDepartment().getManager();
      //在Person类中隐藏委托关系
      public Person getManager() {
      return department.getManager();
      }
      //修改后
      manager = john.getManager();
  • 如果你使用的类无法提供你需要的服务,就需要引入外加函数Introduce Foreign Method

  • 做法:在客户类中建立一个函数,并以第一参数形式传入ige服务类事例。
  • 示例:

    1
    2
    3
    4
    5
    6
    Date newStart = new Date (previousEnd.getYear(), previousEnd.getMonth(), priviousEnd.getDate()+1);
    // extract
    Date new Start = nextDay(previousEnd);
    private static Date nextDay(Date arg) {
    return new Date (arg.getYear(), arg.getMonth(), arg.getDate()+1);
    }
  • 如果上诉情况过多,就需要引入本地扩展Introduce Local Extension,使用子类扩展包装类

重新组织数据

  • 自封装字段(Self Encapsulate Filed)
    • 为字段建立setter/getter,并且只以这些函数来访问字段。
    • 好处:子类可以覆写函数改变获取数据的途径,支持延迟初始化。
    • 缺点:难以阅读。
  • 以对象取代数据值(Replace Data Value with Object)
    • 一个数据项与其它数据和行为一起才有意义,将数据项变成对象。
  • 将值对象改为引用对象(Change Value to Reference)
    • 从一个类衍生出许多批次相等的实例,希望它们替换为同一个对象,将这个值对象变成引用对象。
  • 将引用对象改为值对象(Change Reference to Value)
    • 一个引用对象很小且不可变,而且不易管理。将它变成一个值对象。
  • 以对象取代数组(Replace Array with Object)
    • 一个数组,其中的元素各代表不同的东西,以对象替代数组。
  • 字面常量取代魔法数(Replace Magic Number with Symbolic Constant)
    • 你有一个字面数值,带有特别含义,创建一个常量,根据其意义命名,并用字面数值替换这个常量。
    • 魔法数:具有特使意义,却又不能明确表现出这种意义的数字。
    • 一旦这些数发生改变,你就必须找到这些魔法数,并将它们全部修改一遍。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      double potentialEnerty(double mass, double height){
      return mass * 9.81 * height;
      }
      //replace
      double potentialEnergy(double mass, double height){
      return mass * GRAVITATIONAL_CONSTANT * height;
      }
      static final double GRAVITATIONAL_CONSTANT = 9.81;

简化条件表达式

  • 分解条件表达式(Decompose Conditional)

    • 一个复杂的条件语句,从if、then、else三个段落中分别提炼出独立的函数。
    • 可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      if(date.before(UMMER_START) || date.after(SUMMER_END)){
      charge = quantity * winterRate + winterServiceCharge;
      } else {
      charge = quantity * summerate;
      }
      //replace
      if(notSummer(date)){
      charge = winterCharge(quantity);
      } else {
      charge = summerCharge(quantity);
      }
  • 合并条件表达式(Consolidate Conditional Expression)

    • 一系列的条件测试,都得到相同的结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
    • 使检查的用意更清晰。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      double disablilitAmount(){
      if(seniority < 2) reutrn 0;
      if(monthsDisabled > 12) return 0;
      if(isPartTime) return 0;
      }
      //replace
      double disabilityAmout(){
      if(isNotEligableForDisability()) return 0;
      }
  • 移除控制标记(Remove Control Flag)

    • 在一些列布尔表达式中,某个变量带有“控制标记”的作用,以break语句或者return语句取代控制标记。
  • 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)

    • 如果两条分支都是正常行为,就应该使用if…else…的条件表示式,如果某个条件极其罕见,就应该单独检查该条件,并在条件为真时立刻从函数中返回。这种单独检查常常被称为“卫语句”。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      double getPayAmout(){
      double result;
      if(isDead){
      result = deadAmount();
      } else {
      if(isSeparated){
      result = separatedAmount();
      } else {
      if(isRetired){
      result = retiredAmount();
      } else {
      result = normalPayAmount();
      }
      }
      }
      return result;
      }
      //replace
      double getPayAmount(){
      if(isDead) return deadAmount();
      if(isSeparated) return separatedAmount();
      if(isRetired) return retiredAmount();
      return normalPayAmount();
      }
  • 以多态取代条件表达式(Replace Conditional with Polymorphism)

    • 如果尼需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式。
  • 引入Null对象(Introduce Null Object)
    • 你需要再三检查某个对象是否为null。将null值替换为null对象。
    • Java 8 已经引入了Optional类。
  • 引入断言(Introduce Assertion)
    • 某一段代码需要对程序状态做出某种假设,以断言明确表现这种假设。
    • 断言是一个条件表达式,应该总是为真。如果它失败,表示程序员犯了错误。因此断言的失败应该导致一个非受控异常(unchecked exception)。断言绝对不能被系统的其它部分使用。程序最后的成品往往将断言删除。
    • 在交流的角度上,断言可以帮助程序阅读这理解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们。
    • 请只使用它来检查“一定必须为真”的条件。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      double getExpenseLimit(){
      //should have either expense limit or a pirmary project
      return (expenseLimit != NULL_EXPENSE)? expenseLimit:primaryProjct.getMemberExpenseLimit();
      }
      //replace
      double getExpenseLimit(){
      Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);
      return (expenseLimit != NULL_EXPENSE)? expenseLimit:primaryProjct.getMemberExpenseLimit();
      }

简化函数调用

  • 函数改名(Rename Method)

    • 技巧:首先应该考虑给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称。
    • 如果旧函数是public的接口,你可能无法安全地删除它,这种情况最好保留在原地,并将它标记为deprecated(不建议使用)。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public String getTelephoneNumber(){
      reutrn ("(" + officeAreaCode + ") " + officeNumber);
      }
      //replace
      public String getTelephoneNumber(){
      return getOfficeTelephoneNumber();
      }
      public String getOfficeTelephoneNumber(){
      reutrn ("(" + officeAreaCode + ") " + officeNumber);
      }
  • 添加参数(Add Parameter)

  • 移除参数(RemoveParameter)
  • 将查询函数和修改函数分离(Separate Query from Modifier)
    • 某个函数既返回对象的状态值,又修改对象的状态。建立两个不同的函数,其中一个负责查询,另一个负责修改。
    • 如果一个函数只提供一个值,那么是很好的,应该你可以任意调用这个函数,没有副作用。
    • 多线程环境下,需要synchoronized分离出的方法,保证一致性。
  • 保持对象完整(Preserve Whole Object)

    • 你从某个对象中取出若干值,将它们作为某一次函数调用的参数。改为传递整个对象。
    • 优点:如果将来需要修改参数,就可以避免修改。同时减少了参数长度。
    • 缺点:函数与对象之间耦合,应该考虑将函数一道对象中。
    • 示例:
      1
      2
      3
      4
      5
      int low = daysTempRange().getLow();
      int hight = daysTempRange().getHigh();
      withinPlan = plan.withinRange(low. high);
      //replace
      withinPlan = plan.withinRange(daysTempRange());
  • 隐藏函数(Hide Method)

    • 一个类从来没有被其他任何类用到,将这个函数修改为private。
    • 尽可能降低函数的可见度。
  • 工厂函数取代构造函数(Replace Constructor with Facotory Method)

    • 你希望在闯将对象时不仅仅做简单的构建动作。将构造函数替换为工厂函数。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      Employee(int type){
      this.type = type;
      }
      //replace
      static Employee create(int type){
      return new Employee(type)
      }
  • 以异常取代错误码(Replace Error Code with Exception)

    • 将“普通程序“”与“错误处理”分开,使程序更容易理解。
    • 如果调用者有责任在调用钱检查必须状态,就抛出非受控异常(unchecked)
    • 如果想抛出受控异常(checked),你可以新建一个异常类,也可以使用现有的异常类。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      int vithdraw(int amount){
      if(amount > balance)
      return -1;
      else {
      balance = amount;
      return 0;
      }
      }
      //replace
      void withdraw(int amount) throws BalanceException{
      if(amount > balance) throw new BalanceException();
      balance = amount;
      }

处理概括关系

  • 字段上移:两个子类拥有相同的字段,将该字段移至超类。
  • 函数上移:有些函数,在各个子类中产生完全相同的结果,将函数移至超类。
  • 提炼超类:两个类有相似特性,为这两个类建立一个超类,将相同特性移至超类。
© 2017 Hello World All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero