典型重构手法
倚天剑:拆分阶段
以以下代码为例:
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,它们在原方法中是顺序执行的,不能相互替换。
在此案例中,明显第二种领域模型模式更为合理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!