模板方法重构实战:三大常见陷阱与避坑指南
摘要
项目代码重复率高达30%,通过模板方法模式重构后降至5%以下。该模式适用于业务流程骨架
代码审查时,Leader指着屏幕问:“这段代码和上次提交的有什么区别?”仔细一看,两个业务流程的代码有九成相似,只是中间一小块校验逻辑不同。“复制粘贴一时爽,维护起来火葬场。”留下这句话后,Leader转身离开了。
事后统计发现,项目里类似的重复代码比例竟然超过了30%。最近花了一周时间,用模板方法模式进行重构,踩了几个典型的坑,最终将代码重复率降到了5%以下。下面就来聊聊这次重构的核心思路和那些值得警惕的“坑”。
一、重复代码的三种典型场景
在动手重构之前,先得识别出哪些代码在“重复造轮子”。通常,下面这三种场景最为常见。
场景1:业务流程相似,细节不同
public void processOrder(Order order) {
validateOrder(order); // 校验订单
calculatePrice(order); // 计算价格
createPayment(order); // 创建支付
sendNotification(order); // 发送通知(普通订单信息)
}
public void processVipOrder(Order order) {
validateOrder(order); // 校验订单
calculateVipPrice(order); // 计算VIP价格(不同)
createPayment(order); // 创建支付
sendVipNotification(order); // 发送VIP通知(不同)
}
public void processGroupOrder(Order order) {
validateOrder(order); // 校验订单
calculateGroupPrice(order); // 计算团购价格(不同)
createPayment(order); // 创建支付
sendGroupNotification(order);// 发送团购通知(不同)
}
这三个方法,骨架几乎一模一样,差异点只集中在计算价格和发送通知这两个步骤上。这就是模板方法模式最典型的用武之地。
场景2:重复的异常处理
public void exportUserExcel() {
try {
List data = queryUsers();
Excel excel = generateExcel(data);
download(excel);
} catch (Exception e) {
log.error("导出失败", e);
throw new BusinessException("导出失败");
}
}
public void exportOrderExcel() {
try {
List data = queryOrders();
Excel excel = generateExcel(data);
download(excel);
} catch (Exception e) {
log.error("导出失败", e);
throw new BusinessException("导出失败");
}
}
异常处理的逻辑被完全复制粘贴,一旦需要调整错误信息或日志格式,就得修改多处。
场景3:相似的数据库操作
public void sa veUser(User user) {
validate(user);
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);
clearCache(user.getId());
}
public void sa veProduct(Product product) {
validate(product);
product.setCreateTime(LocalDateTime.now());
productMapper.insert(product);
clearCache(product.getId());
}
数据持久化的流程高度一致,区别仅在于操作的对象和Mapper不同。
二、模板方法模式重构
针对第一种“骨架相同,细节不同”的场景,模板方法模式堪称“对症下药”。其核心思想是:在一个抽象类中定义一个操作中的算法骨架,而将一些步骤延迟到子类中实现。
2.1 定义抽象模板
public abstract class OrderProcessor {
public final void process(Order order) {
validateOrder(order);
calculatePrice(order);
createPayment(order);
sendNotification(order);
}
protected void validateOrder(Order order) {
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new BusinessException("订单商品不能为空");
}
if (order.getTotalAmount() == null || order.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("订单金额无效");
}
}
protected abstract void calculatePrice(Order order);
protected void createPayment(Order order) {
Payment payment = new Payment();
payment.setOrderId(order.getId());
payment.setAmount(order.getPayAmount());
paymentService.create(payment);
}
protected abstract void sendNotification(Order order);
}
这里,process方法就是“模板方法”,它定义了订单处理的固定流程。validateOrder和createPayment是通用步骤,直接在抽象类中实现。而calculatePrice和sendNotification则是抽象方法,留给子类去定义各自的具体行为。
2.2 具体实现
@Component
public class NormalOrderProcessor extends OrderProcessor {
@Override
protected void calculatePrice(Order order) {
order.setPayAmount(order.getTotalAmount());
}
@Override
protected void sendNotification(Order order) {
smsService.send(order.getUserId(), "您的订单已创建");
}
}
@Component
public class VipOrderProcessor extends OrderProcessor {
@Override
protected void calculatePrice(Order order) {
BigDecimal discount = order.getTotalAmount().multiply(new BigDecimal("0.9"));
order.setPayAmount(discount);
}
@Override
protected void sendNotification(Order order) {
smsService.send(order.getUserId(), "VIP用户您好,您的订单已创建");
pushService.push(order.getUserId(), "VIP专属消息");
}
}
@Component
public class GroupOrderProcessor extends OrderProcessor {
@Override
protected void calculatePrice(Order order) {
BigDecimal discount = order.getTotalAmount().multiply(new BigDecimal("0.8"));
order.setPayAmount(discount);
}
@Override
protected void sendNotification(Order order) {
smsService.send(order.getUserId(), "团购订单已创建");
wechatService.sendGroupMessage(order.getGroupId(), "快来参团");
}
}
每个子类只关心自己与众不同的部分,代码的复用性和清晰度都得到了提升。
2.3 工厂模式配合使用
为了让客户端代码更方便地获取合适的处理器,通常会搭配一个简单的工厂。
@Component
public class OrderProcessorFactory {
@Autowired
private NormalOrderProcessor normalOrderProcessor;
@Autowired
private VipOrderProcessor vipOrderProcessor;
@Autowired
private GroupOrderProcessor groupOrderProcessor;
public OrderProcessor getProcessor(OrderType type) {
switch (type) {
case VIP:
return vipOrderProcessor;
case GROUP:
return groupOrderProcessor;
default:
return normalOrderProcessor;
}
}
}
@Service
public class OrderService {
@Autowired
private OrderProcessorFactory processorFactory;
public void processOrder(Order order) {
OrderProcessor processor = processorFactory.getProcessor(order.getType());
processor.process(order);
}
}
这样一来,业务层只需根据订单类型获取对应的处理器,无需关心具体的实现类,符合“开闭原则”。
三、踩过的3个坑
模式虽好,但用起来也有不少细节需要注意,一不小心就会踩坑。
坑1:把钩子方法定义成抽象方法
错误代码:
public abstract class OrderProcessor {
protected abstract void validateOrder(Order order); // 抽象方法
protected abstract void calculatePrice(Order order);
protected abstract void sendNotification(Order order);
}
问题:并非所有子类都需要自定义校验逻辑。将validateOrder也设为抽象方法,会强制每个子类都去实现它,哪怕它们的校验逻辑完全一样,这导致了不必要的代码重复。
解决:使用钩子方法(Hook Method)。钩子方法在父类中提供默认实现,子类可以根据需要选择是否覆盖。
public abstract class OrderProcessor {
protected void validateOrder(Order order) { // 钩子方法,有默认实现
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new BusinessException("订单商品不能为空");
}
}
protected abstract void calculatePrice(Order order); // 抽象方法,必须实现
}
钩子方法原则:
- 大多数子类都需要的通用逻辑 → 定义为有默认实现的钩子方法(子类可选覆盖)。
- 差异化的核心逻辑 → 定义为抽象方法(子类必须实现)。
坑2:忘记用final修饰模板方法
错误代码:
public abstract class OrderProcessor {
public void process(Order order) { // 没有final
validateOrder(order);
calculatePrice(order);
createPayment(order);
sendNotification(order);
}
}
问题:子类可以覆盖process方法,从而破坏既定的算法骨架,可能引发严重的安全或逻辑漏洞。
public class BadOrderProcessor extends OrderProcessor {
@Override
public void process(Order order) {
calculatePrice(order); // 跳过校验,直接计算价格
createPayment(order); // 安全漏洞!
}
}
解决:使用final关键字修饰模板方法,确保算法结构不被子类篡改。
public abstract class OrderProcessor {
public final void process(Order order) { // final防止覆盖
validateOrder(order);
calculatePrice(order);
createPayment(order);
sendNotification(order);
}
}
坑3:模板方法过于复杂
错误代码:
public abstract class ComplexProcessor {
public final void process(Order order) {
step1(order);
if (condition1(order)) {
step2(order);
} else {
step3(order);
}
for (Item item : order.getItems()) {
step4(item);
if (condition2(item)) {
step5(item);
}
}
step6(order);
if (condition3(order)) {
step7(order);
}
// ... 20多个步骤
}
}
问题:
- 模板方法过长,难以理解和维护。
- 条件判断太多,逻辑混乱。
- 违背了单一职责原则,一个方法做了太多事情。
解决:拆分模板,将复杂的流程分解为几个清晰的阶段。
public abstract class OrderProcessor {
public final void process(Order order) {
validate(order); // 校验步骤
doProcess(order); // 核心处理(子类扩展)
postProcess(order); // 后置处理
}
protected void validate(Order order) {
validatorChain.validate(order);
}
protected abstract void doProcess(Order order);
protected void postProcess(Order order) {
notificationService.notify(order);
}
}
四、Spring中的模板方法模式
模板方法模式在Spring等主流框架中应用广泛,理解这些实例有助于更好地掌握其精髓。
4.1 JdbcTemplate
public class JdbcTemplate {
public T query(String sql, RowMapper rowMapper, Object... args) {
return execute(connection -> {
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = connection.prepareStatement(sql);
setParameters(ps, args);
rs = ps.executeQuery();
return rowMapper.mapRow(rs);
} finally {
close(rs);
close(ps);
}
});
}
// 模板方法:定义固定流程(获取连接、创建语句、设置参数、执行、关闭资源)
// 延迟到子类/回调:结果映射rowMapper
}
使用:
List users = jdbcTemplate.query(
"SELECT * FROM user WHERE age > ?",
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")),
18
);
JdbcTemplate的query方法定义了数据库查询的标准流程,而如何将ResultSet的一行数据映射成一个Ja va对象(即RowMapper),则延迟给调用者通过回调函数(或Lambda表达式)来提供。
4.2 HttpServlet
public abstract class HttpServlet {
// 模板方法
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if ("GET".equals(method)) {
doGet(req, resp); // 钩子方法
} else if ("POST".equals(method)) {
doPost(req, resp); // 钩子方法
} else if ("PUT".equals(method)) {
doPut(req, resp);
}
// ...
}
// 子类覆盖
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
service方法作为模板方法,根据HTTP请求方法路由到对应的doXxx钩子方法。开发者只需继承HttpServlet并覆盖特定的doGet、doPost等方法即可。
五、最佳实践
5.1 何时使用模板方法模式
当多个类有相同或相似的算法骨架,但其中某些步骤的实现不同时,就应考虑使用模板方法模式。它特别适用于重构那些存在大量重复流程代码的场景。
5.2 设计原则
1. 钩子方法:有默认实现,子类可选覆盖
2. 抽象方法:无默认实现,子类必须覆盖
3. 模板方法:final修饰,防止覆盖
4. 单一职责:模板方法不要太长,可拆分
5.3 命名规范
良好的命名能显著提升代码的可读性:
public abstract class AbstractProcessor {
// 模板方法
public final void execute() { }
// 抽象方法:通常以do开头,表示具体要执行的动作
protected abstract void doProcess();
// 钩子方法:on开头(常用于前置/后置处理)
protected void onBefore() { }
protected void onAfter() { }
// 钩子方法:should开头(常用于条件判断)
protected boolean shouldValidate() { return true; }
}
六、面试加分Q&A
Q1:模板方法模式和策略模式有什么区别?
A:这是设计模式面试的经典问题。核心区别在于:
- 模板方法模式:通过继承来定义算法骨架,子类负责实现特定步骤。它强调“整体流程固定,局部可变”。
- 策略模式:通过组合来定义一系列算法族,并使它们可以相互替换。它强调“整个算法可替换”,更注重灵活性。
简单说,模板方法是“父类定流程,子类填细节”;策略模式是“接口定规范,实现类随便换”。
Q2:为什么模板方法要用final修饰?
A: 为了防止子类覆盖模板方法,从而破坏父类定义的算法骨架。用final修饰保证了流程结构的稳定性和不可篡改性,这完美符合“开闭原则”的精神:对扩展开放(允许子类覆盖钩子方法以改变细节),对修改关闭(禁止子类修改核心流程)。
Q3:钩子方法和抽象方法的区别?
A:
- 抽象方法:没有默认实现,子类必须实现。用于定义算法中必须由子类提供的可变部分。
- 钩子方法:有默认实现,子类可选覆盖。它为子类提供了一个“挂钩”,让子类有机会在算法的特定点进行干预,常用于前置检查、后置处理或条件判断。
Q4:模板方法模式有什么缺点?
A:
- 类数量增加:每个不同的实现都需要一个子类,可能导致类的数量膨胀。
- 继承的局限性:Ja va是单继承,使用模板方法会占用继承位。而且子类覆盖方法可能会对父类行为产生意想不到的影响,调试起来更复杂。
- 现代替代方案:在JDK8之后,接口的
default方法可以在一定程度上替代模板方法模式,提供更灵活的代码复用方式。
Q5:Spring中哪些地方用了模板方法模式?
A: Spring框架大量使用了此模式,例如:
JdbcTemplate:数据库操作的固定流程模板。HttpServlet:HTTP请求处理的模板。RestTemplate(虽已标记为Deprecated,但其继任者WebClient的设计思想类似):HTTP客户端请求的模板。AbstractHandlerExceptionResolver:异常解析的模板。
七、总结
模板方法模式的核心在于“固定骨架,可变细节”。它通过将不变的行为搬移到超类,去除子类中的重复代码,是重构重复代码的一把利器。
记住三条实践铁律:
- 模板方法务必用
final修饰,锁死核心流程。 - 合理使用钩子方法提供默认实现,减少子类的强制负担。
- 保持模板方法简洁,一旦过长或过于复杂,就要考虑拆分了。
下次再看到那些似曾相识的代码块时,不妨想想,是不是可以用模板方法把它们“收编”起来。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。