菜鸟AI - 让提示词生成更简单! 全站导航 全站导航
AI工具安装 新手教程 进阶教程 辅助资源 AI提示词 热点资讯 技术资讯 产业资讯 内容生成 模型技术 AI信息库

已有账号?

首页 > 资讯 > 模板方法重构实战:三大常见陷阱与避坑指南
其他资讯 模板 重构业务

模板方法重构实战:三大常见陷阱与避坑指南

2026-05-19
阅读 0
热度 0
作者 菜鸟AI编辑部
摘要

摘要

项目代码重复率高达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方法就是“模板方法”,它定义了订单处理的固定流程。validateOrdercreatePayment是通用步骤,直接在抽象类中实现。而calculatePricesendNotification则是抽象方法,留给子类去定义各自的具体行为。

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
);

JdbcTemplatequery方法定义了数据库查询的标准流程,而如何将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并覆盖特定的doGetdoPost等方法即可。

五、最佳实践

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:异常解析的模板。

七、总结

模板方法模式的核心在于“固定骨架,可变细节”。它通过将不变的行为搬移到超类,去除子类中的重复代码,是重构重复代码的一把利器。

记住三条实践铁律:

  1. 模板方法务必用final修饰,锁死核心流程。
  2. 合理使用钩子方法提供默认实现,减少子类的强制负担。
  3. 保持模板方法简洁,一旦过长或过于复杂,就要考虑拆分了。

下次再看到那些似曾相识的代码块时,不妨想想,是不是可以用模板方法把它们“收编”起来。

来源:互联网

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

同类文章推荐

相关文章推荐

更多