模板方法 (Template Method)
同一类业务往往是一套固定流程:先校验、再算钱、再支付、再记流水、最后发凭证。不同业务(例如学费和住宿费)只是「算钱」和「记流水」的方式不一样,整体顺序和框架是一样的。如果在每个业务里都把整条流程写一遍,既重复又容易漏步或顺序不一致。
模板方法模式的做法是:在抽象类里把「流程怎么走」定死——一个模板方法按顺序调用若干步骤,其中几步是抽象方法或钩子,由子类实现;其余步骤在超类里给默认实现。这样流程只写一处,子类只关心「我这一种业务在那几步上有什么不同」。
下面先说明模板方法、抽象步骤和默认步骤在代码里怎么对应,学校缴费流程的示例:学费与住宿费共用「校验学生 → 计算应缴 → 支付 → 记流水 → 发凭证」的骨架,其中计算应缴和记流水由具体缴费类型实现。
一、问题从哪来?
Section titled “一、问题从哪来?”很多后台流程是「步骤固定、只是其中几步的实现因业务而异」:例如各类缴费都是「校验 → 算金额 → 支付 → 记账 → 通知」,但学费按学年和学分算、住宿费按宿舍类型算;门禁都是「验卡 → 查权限 → 记通行 → 可选告警」,但宿舍和图书馆的权限规则不同。如果在每个业务类里都完整写一遍流程,会有重复代码,而且一旦要加一步(例如统一加审计),就得改多处;步骤顺序也容易在复制粘贴时搞错。
更稳妥的做法是:把「流程长什么样」提到抽象类里,写一个模板方法,按固定顺序调用 step1、step2、…;哪些步骤是「因业务而异」的,就标成抽象方法或钩子,由子类实现;其余步骤在超类里写好默认逻辑。这样流程结构只在一处维护,子类只实现自己那几步,符合开闭原则。
二、模板方法、抽象步骤与钩子
Section titled “二、模板方法、抽象步骤与钩子”- 抽象类 (AbstractClass):定义模板方法(通常用
final防止子类改流程),方法里按固定顺序调用若干步骤。部分步骤是抽象方法,子类必须实现;部分步骤在超类里已有默认实现,子类可覆盖也可不覆盖(钩子)。 - 具体类 (ConcreteClass):继承抽象类,实现所有抽象步骤,按需覆盖钩子。不重写模板方法本身,只通过步骤实现来定制行为。
- 客户端:依赖抽象类类型,通过具体类实例调用模板方法,得到「流程一致、步骤实现不同」的行为。
模板方法保证「先做什么、后做什么」不变,扩展点只在「某一步怎么做」。
抽象类里模板方法调用多个步骤,部分步骤为抽象(子类实现),部分有默认实现。
classDiagram
class AbstractClass {
<<abstract>>
+templateMethod() void
#step1()* void
#step2()* void
#step3() void
}
class ConcreteClass {
#step1() void
#step2() void
}
AbstractClass <|-- ConcreteClass
四、最小示例:固定三步,两步由子类实现
Section titled “四、最小示例:固定三步,两步由子类实现”不依赖框架,用抽象类和两个抽象步骤把「骨架固定、步骤可替换」跑通。
public abstract class AbstractClass { public final void templateMethod() { step1(); step2(); step3(); }
protected abstract void step1(); protected abstract void step2();
protected void step3() { System.out.println("默认 step3"); }}
public class ConcreteClass extends AbstractClass { @Override protected void step1() { System.out.println("Concrete step1"); }
@Override protected void step2() { System.out.println("Concrete step2"); }}
new ConcreteClass().templateMethod();// 输出: Concrete step1 / Concrete step2 / 默认 step3下面把「流程」换成学校里的缴费:校验学生、算应缴、支付、记流水、发凭证,其中「算应缴」和「记流水」因缴费类型不同而由子类实现。
五、实战:学校缴费流程(学费 / 住宿费)
Section titled “五、实战:学校缴费流程(学费 / 住宿费)”学校场景里常见多种缴费:学费、住宿费、杂费等。流程大体一致(校验学生 → 计算应缴金额 → 调用支付 → 记流水 → 发凭证或更新状态),只是「怎么算应缴」「流水记成什么样」因类型不同。用模板方法可以把整条流程收在抽象类里,把「计算应缴」和「记流水」留给具体缴费类型实现;这样加一种新缴费主要是加一个子类,不用再抄一遍流程。
这里用学费和住宿费两种类型举例:抽象类里定义模板方法,子类实现计算金额与写流水的逻辑;流程类以 Bean 形式存在,由服务层按缴费类型选择并执行。
5.1 入参与结果(示意)
Section titled “5.1 入参与结果(示意)”一次缴费请求里带上学生标识和缴费类型相关参数;结果里包含是否成功、流水号等。
package com.example.demo.template;
import lombok.Data;import java.math.BigDecimal;
@Datapublic class PaymentRequest { private String studentId; private String academicYear; // 学年,学费用 private String dormId; // 宿舍号,住宿费用 private String feeType; // TUITION | DORM}
@Datapublic class PaymentResult { private boolean success; private String ledgerId; private BigDecimal amount; private String message;}5.2 抽象缴费流程(模板方法)
Section titled “5.2 抽象缴费流程(模板方法)”模板方法固定顺序:校验学生 → 计算应缴金额 → 执行支付 → 记流水 → 发凭证。其中「计算应缴」和「记流水」因缴费类型不同,定为抽象方法;校验、支付、发凭证在抽象类里给默认实现(实际可再拆成可覆盖的钩子)。
package com.example.demo.template;
import lombok.RequiredArgsConstructor;import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;
public abstract class AbstractPaymentProcess { protected final StudentService studentService; protected final PaymentGateway paymentGateway; protected final PaymentLedgerRepository ledgerRepository;
protected AbstractPaymentProcess(StudentService studentService, PaymentGateway paymentGateway, PaymentLedgerRepository ledgerRepository) { this.studentService = studentService; this.paymentGateway = paymentGateway; this.ledgerRepository = ledgerRepository; }
/** 模板方法:流程骨架 */ @Transactional(rollbackFor = Exception.class) public final PaymentResult execute(PaymentRequest request) { // 1. 校验学生 studentService.validateStudent(request.getStudentId()); // 2. 计算应缴金额(子类实现) BigDecimal amount = calculateAmount(request); // 3. 支付 String transactionId = paymentGateway.pay(request.getStudentId(), amount); // 4. 记流水(子类实现) String ledgerId = writeLedger(request, amount, transactionId); // 5. 发凭证(默认实现,子类可覆盖) sendReceipt(request, ledgerId, amount);
PaymentResult result = new PaymentResult(); result.setSuccess(true); result.setLedgerId(ledgerId); result.setAmount(amount); return result; }
/** 子类实现:按缴费类型计算应缴金额 */ protected abstract BigDecimal calculateAmount(PaymentRequest request);
/** 子类实现:按缴费类型写入对应流水表/格式 */ protected abstract String writeLedger(PaymentRequest request, BigDecimal amount, String transactionId);
protected void sendReceipt(PaymentRequest request, String ledgerId, BigDecimal amount) { // 默认:发站内消息或邮件,子类可覆盖 }}依赖的 StudentService(校验学生)、PaymentGateway(发起支付)、PaymentLedgerRepository(保存流水,需有 saveTuition / saveDorm 或统一入口)按项目现有实现注入即可;TuitionFee、TuitionLedgerEntry、DormLedgerEntry 等实体与表结构按校务系统约定。这里只表达「流程调谁」。
5.3 具体流程:学费、住宿费
Section titled “5.3 具体流程:学费、住宿费”学费按学年查应缴(例如从培养方案或配置里取);住宿费按宿舍类型/楼栋取单价再算。记流水时分别写入学费流水表、住宿费流水表或不同字段,由子类实现。
package com.example.demo.template;
import org.springframework.stereotype.Component;import java.math.BigDecimal;
@Componentpublic class TuitionPaymentProcess extends AbstractPaymentProcess { private final TuitionFeeRepository tuitionFeeRepository;
public TuitionPaymentProcess(StudentService studentService, PaymentGateway paymentGateway, PaymentLedgerRepository ledgerRepository, TuitionFeeRepository tuitionFeeRepository) { super(studentService, paymentGateway, ledgerRepository); this.tuitionFeeRepository = tuitionFeeRepository; }
@Override protected BigDecimal calculateAmount(PaymentRequest request) { return tuitionFeeRepository.findByStudentAndYear(request.getStudentId(), request.getAcademicYear()) .map(TuitionFee::getAmount) .orElseThrow(() -> new IllegalArgumentException("未找到应缴学费")); }
@Override protected String writeLedger(PaymentRequest request, BigDecimal amount, String transactionId) { TuitionLedgerEntry entry = new TuitionLedgerEntry(); entry.setStudentId(request.getStudentId()); entry.setAcademicYear(request.getAcademicYear()); entry.setAmount(amount); entry.setTransactionId(transactionId); return ledgerRepository.saveTuition(entry); }}package com.example.demo.template;
import org.springframework.stereotype.Component;import java.math.BigDecimal;
@Componentpublic class DormPaymentProcess extends AbstractPaymentProcess { private final DormFeePolicyRepository dormFeePolicyRepository;
public DormPaymentProcess(StudentService studentService, PaymentGateway paymentGateway, PaymentLedgerRepository ledgerRepository, DormFeePolicyRepository dormFeePolicyRepository) { super(studentService, paymentGateway, ledgerRepository); this.dormFeePolicyRepository = dormFeePolicyRepository; }
@Override protected BigDecimal calculateAmount(PaymentRequest request) { BigDecimal unitPrice = dormFeePolicyRepository.getUnitPriceByDorm(request.getDormId()); // 简化:按学期或月份,这里只做单价 return unitPrice; }
@Override protected String writeLedger(PaymentRequest request, BigDecimal amount, String transactionId) { DormLedgerEntry entry = new DormLedgerEntry(); entry.setStudentId(request.getStudentId()); entry.setDormId(request.getDormId()); entry.setAmount(amount); entry.setTransactionId(transactionId); return ledgerRepository.saveDorm(entry); }}TuitionFeeRepository、DormFeePolicyRepository 以及 ledgerRepository.saveTuition / saveDorm 按你项目里的表结构实现即可;重点是「算金额」和「记流水」在子类里各自实现,流程顺序由抽象类的模板方法保证。
5.4 按缴费类型选流程并执行
Section titled “5.4 按缴费类型选流程并执行”服务层根据请求里的 feeType 选对应的流程 Bean,调用其 execute(request),不写 if-else 流程分支。
package com.example.demo.template;
import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;import java.util.Map;
@Service@RequiredArgsConstructorpublic class PaymentFacadeService { private final Map<String, AbstractPaymentProcess> processMap;
public PaymentResult pay(PaymentRequest request) { AbstractPaymentProcess process = processMap.get(request.getFeeType()); if (process == null) { throw new IllegalArgumentException("unsupported feeType: " + request.getFeeType()); } return process.execute(request); }}processMap 可在配置类里按 feeType 注入,例如:
@Configurationpublic class PaymentProcessConfig { @Bean public Map<String, AbstractPaymentProcess> paymentProcessMap( TuitionPaymentProcess tuition, DormPaymentProcess dorm) { return Map.of( "TUITION", tuition, "DORM", dorm ); }}这样新增一种缴费类型时,加一个继承 AbstractPaymentProcess 的 Bean 并放进 Map 即可,流程骨架不用动。
模板方法在抽象类里定好算法骨架和步骤顺序,把可变步骤留给子类实现,既保证流程一致,又便于扩展。学校缴费示例里,「校验 → 算金额 → 支付 → 记流水 → 发凭证」只写在一处,学费和住宿费通过子类实现「计算应缴」和「记流水」,符合开闭原则;若要支持更多缴费类型或增加统一步骤(例如审计),只需在抽象类里改模板方法或加钩子,子类按需覆盖即可。