典型重构手法

 


倚天剑:拆分阶段

以以下代码为例:

复制代码
public class TheatricalPlayers {
   public String print(Invoice invoice) {
       var totalAmount = 0;
       var volumeCredits = 0;
       var result = String.format("Statement for %s\n", invoice.customer);

       NumberFormat format = NumberFormat.getCurrencyInstance(Locale.US);

       for (var perf : invoice.performances) {
           var play = perf.play;
           var thisAmount = 40000;
           if (perf.audience > 30) {
               thisAmount += 1000 * (perf.audience - 30);
           }

           var thisCredits = Math.max(perf.audience - 30, 0);
           if ("comedy".equals(play.type)) thisCredits += Math.floor((double) perf.audience / 5);

           totalAmount += thisAmount;
           volumeCredits += thisCredits;
       }
       
       result += String.format("Amount owed is %s\n", format.format(totalAmount / 100));
       result += String.format("You earned %s credits\n", volumeCredits);
       return result;
   }
}
复制代码

这段代码计算了演出的总费用以及观众积分量,并对这两项数据进行格式化并返回。

首先这段代码违反了单一职责原则(SRP):一个类活方法应该只有一个引起它变化的原因。而对于这个方法来说,显然有三个。如果计算费用的逻辑发生变化,比如观众的基数从 30 改成了 50;如果计算积分的逻辑发生变化,比如演出悲剧也有相应的积分;如果格式化的逻辑发生变化,比如用 HTML 来输出清单,你都需要修改这个方法。引起它变化的原因,不是一个,而是三个。

所以我们可以使用拆分阶段的方法将它们分开。

1. 在开始正式重构之前,应该先运行以下所有的测试,确保通过。

2. 给局部变量更小的作用域,在使用它之前再声明。我们先把result和format两个变量的声明往下挪,挪到result使用之前。之后需要运行测试。

复制代码
public class TheatricalPlayers {
   public String print(Invoice invoice) {
       var totalAmount = 0;
       var volumeCredits = 0;
       
       for (var perf : invoice.performances) {
           var play = perf.play;
           var thisAmount = 40000;
           if (perf.audience > 30) {
               thisAmount += 1000 * (perf.audience - 30);
           }

           var thisCredits = Math.max(perf.audience - 30, 0);
           if ("comedy".equals(play.type)) thisCredits += Math.floor((double) perf.audience / 5);

           totalAmount += thisAmount;
           volumeCredits += thisCredits;
       }
       
       var result = String.format("Statement for %s\n", invoice.customer);
       NumberFormat format = NumberFormat.getCurrencyInstance(Locale.US);
       result += String.format("Amount owed is %s\n", format.format(totalAmount / 100));
       result += String.format("You earned %s credits\n", volumeCredits);
       return result;
   }
}
复制代码

3. 修改for循环部分内容。play的变量只有一个地方在使用,可以直接内联。thisAmount的部分提取成一个方法。

复制代码
public String print(Invoice invoice) {
    var totalAmount = 0;
    var volumeCredits = 0;

    for (var perf : invoice.performances) {
        int thisAmount = getThisAmount(perf);
        
        var thisCredits = Math.max(perf.audience - 30, 0);
        if ("comedy".equals(perf.play.type)) thisCredits += Math.floor((double) perf.audience / 5);
        
        totalAmount += thisAmount;
        volumeCredits += thisCredits;
    }
    
    // format代码
}
复制代码

4. 同样把thisCredits提取成一个方法。thisAmount同样只有一处调用,可以直接内联。这样for循环部分只剩两行代码

for (var perf : invoice.performances) {
    totalAmount += getThisAmount(perf);
    volumeCredits += getThisCredits(perf);
}

5. 复制这个for循环,分别删掉两个循环中的volumeCredits和totalAmount,用两个for循环分别计算totalAmount和volumeCredits,再把total Amount和volumeCredits的声明和各自的for循环放在一起,形成以下这个样子

复制代码
public String print(Invoice invoice) {
    var totalAmount = 0;
    for (var perf : invoice.performances) {
        totalAmount += getThisAmount(perf);
    }
    
    var volumeCredits = 0;
    for (var perf : invoice.performances) {
        volumeCredits += getThisCredits(perf);
    }
    
    var result = String.format("Statement for %s\n", invoice.customer);
    var format = NumberFormat.getCurrencyInstance(Locale.US);
    result += String.format("Amount owed is %s\n", format.format(totalAmount / 100));
    result += String.format("You earned %s credits\n", volumeCredits);
    return result;
}
复制代码

6. 拆分阶段基本完成,代码的内容分成了三段,分别负责计算totalAmount,计算volumnCredits和格式化输出结果。最后一步,把各个阶段提取成单独的方法,彻底完成重构:

public String print(Invoice invoice) {
    int totalAmount = getTotalAmount(invoice);
    int volumeCredits = getVolumeCredits(invoice);
    return getResult(invoice, totalAmount, volumeCredits);
}

总结一下重构代码的所有步骤:

 

 

 

 

屠龙刀:方法对象

方法对象,就是只只包含一个方法的对象,这个方法就是该对象主要的业务逻辑。拆分阶段完成之后的完整代码如下:

复制代码
public class TheatricalPlayers {
    public String print(Invoice invoice) {
        int totalAmount = getTotalAmount(invoice);
        int volumeCredits = getVolumeCredits(invoice);
        return getResult(invoice, totalAmount, volumeCredits);
    }
    
    private int getTotalAmount(Invoice invoice) {
        var totalAmount = 0;
        for (var perf : invoice.performances) {
            totalAmount += getThisAmount(perf);
        }
        return totalAmount;
    }
    
    private int getThisAmount(Performance perf) {
        var thisAmount = 40000;
        if (perf.audience > 30) {
            thisAmount += 1000 * (perf.audience - 30);
        }
        return thisAmount;
    }
    private int getVolumeCredits(Invoice invoice) {
        var volumeCredits = 0;
        for (var perf : invoice.performances) {
            volumeCredits += getThisCredits(perf);
        }
        return volumeCredits;
    }
    
    private int getThisCredits(Performance perf) {
        var thisCredits = Math.max(perf.audience - 30, 0);
        if ("comedy".equals(perf.play.type)) thisCredits += Math.floor((double) perf.audience / 5);
        return thisCredits;
    }
    
    private String getResult(Invoice invoice, int totalAmount, int volumeCredits) {
        var result = String.format("Statement for %s\n", invoice.customer);
        var format = NumberFormat.getCurrencyInstance(Locale.US);
        result += String.format("Amount owed is %s\n", format.format(totalAmount / 100));
        result += String.format("You earned %s credits\n", volumeCredits);
        return result;
    }
}
复制代码

代码仍然是发散式变化,只不过从方法级别变成了类级别,当三个阶段的任何一个逻辑发生变化的时候,你都需要修改这个类。我们要做的就是把getTotalAmount,getVolumeCredits和getResult三个方法都移动到不同的方法对象中。

可以用方法名的变形作为类名。如果方法名叫complexCalculation,那么类名就可以叫ComplexCalculation。getTotalAmount就可以叫做CalculateTotalAmount,创建完新类,移动方法,完成之后的代码如下:

public String print(Invoice invoice) {
    int totalAmount = new TotalAmountCalculator().getTotalAmount(invoice);
    int volumeCredits = new VolumeCreditsCalculator().getVolumeCredits(invoice);
    return new ResultFormatter().getResult(invoice, totalAmount, volumeCredits);
}

将方法中新实例化的类提取成接缝:

复制代码
public class TheatricalPlayers {
    private TotalAmountCalculator totalAmountCalculator;
    private VolumeCreditsCalculator volumeCreditsCalculator;
    private ResultFormatter resultFormatter;
    
    public TheatricalPlayers(TotalAmountCalculator totalAmountCalculator, VolumeCreditsCalculator volumeCreditsCalculator, ResultFormatter resultFormatter) {
        this.totalAmountCalculator = totalAmountCalculator;
        this.volumeCreditsCalculator = volumeCreditsCalculator;
        this.resultFormatter = resultFormatter;
    }
    
    public String print(Invoice invoice) {
        int totalAmount = totalAmountCalculator.calculate(invoice);
        int volumeCredits = volumeCreditsCalculator.calculate(invoice);
        return resultFormatter.format(invoice, totalAmount, volumeCredits);
    }
}
复制代码

到这里,方法对象的重构就全部完成了,它彻底分开了不同职责之间的联系,让他们个子位于自己的方法对象里。

 

 

以下为IntelliJ IDEA的常用快捷键。VS不可用。

重构到策略模式

TotalAmountCalculator 和 VolumeCreditsCalculator类都只接受一个Invoice参数,返回一个int。这种坏味道叫做异曲同工的类(Alternative Classes with Different Interfaces),我们ke以提取接口,让这两个类实现同一个接口。但这只是看上去像重构到设计模式,并不是真正的重构到策略模式。

public interface InvoiceCalculator {
    int calculate(Invoice invoice);
}
复制代码
public class TheatricalPlayers {
    private InvoiceCalculator totalAmountCalculator;
    private InvoiceCalculator volumeCreditsCalculator;
    private ResultFormatter resultFormatter;
    
    public TheatricalPlayers(InvoiceCalculator totalAmountCalculator, InvoiceCalculator volumeCreditsCalculator, ResultFormatter resultFormatter) {
        this.totalAmountCalculator = totalAmountCalculator;
        this.volumeCreditsCalculator = volumeCreditsCalculator;
        this.resultFormatter = resultFormatter;
    }
    
    // print方法
}
复制代码

重构到领域模型

TotalAmountCalculator这个方法对象,它只依赖Invoice类,本身没有任何数据。这种大量依赖外部数据,而不依赖自己内部数据的坏味道,叫做依恋情结。

可以将方法移动到Invoice内部来解决这种坏味道。

复制代码
public class Invoice {
    // 其他代码
  int calculateTotalAmount() {
      var totalAmount = 0;
      for (var perf : performances) {
          int thisAmount = getThisAmount(perf);
          totalAmount += thisAmount;
      }
      return totalAmount;
  }
  private int getThisAmount(Performance perf) {
      var thisAmount = 40000;
      if (perf.audience > 30) {
          thisAmount += 1000 * (perf.audience - 30);
      }
      return thisAmount;
  }
}
复制代码

同样,getThisAmount只依赖Perfomance,可以把这个方法移动到Performance内部来消除,移动完之后就是这个样子:

int calculateTotalAmount() {
    var totalAmount = 0;
    for (var perf : performances) {
        int thisAmount = perf.calculateAmount();
        totalAmount += thisAmount;
    }
    return totalAmount;
}

同样的方法可以用来修改VolumeCreditsCalculator。都完成之后TheatricalPlayers类的print方法将如下所示:

public String print(Invoice invoice) {
    int totalAmount = invoice.calculateTotalAmount();
    int volumeCredits = invoice.calculateVolumeCredits();
    return resultFormatter.getResult(invoice, totalAmount, volumeCredits);
}

如此一来,这个方法和ResultFormatter的职责重叠了,可以直接把getResult的内容提取出来放在print中

public String print(Invoice invoice) {
    var format = NumberFormat.getCurrencyInstance(Locale.US);
    var result = String.format("Statement for %s\n", invoice.customer);
    result += String.format("Amount owed is %s\n", format.format(invoice.calculateTotalAmount() / 100));
    result += String.format("You earned %s credits\n", invoice.calculateVolumeCredits());
    return result;
}

这样,我们重构创建出来的三个方法对象,又全部消除了。这种把数据行为都放在对象中的模式,叫做领域模型模式。

相比上一种看上去像策略模式的重构,但实际上策略接口的两个实现类并不是相互替换的关系,而是毫无关系。所有行为模式的共同特点是,不同行为可以根据某些条件相互替换。

但代码中的TotalAmountCalculator 和 VolumeCreditsCalculator 虽然都叫 calculator,但没有 if/else,它们在原方法中是顺序执行的,不能相互替换。

在此案例中,明显第二种领域模型模式更为合理。

posted @   李琦贝尔蒙特  阅读(105)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示