跳转到内容

模板方法 (Template Method)

同一类业务往往是一套固定流程:先校验、再算钱、再支付、再记流水、最后发凭证。不同业务(例如学费和住宿费)只是「算钱」和「记流水」的方式不一样,整体顺序和框架是一样的。如果在每个业务里都把整条流程写一遍,既重复又容易漏步或顺序不一致。

模板方法模式的做法是:在抽象类里把「流程怎么走」定死——一个模板方法按顺序调用若干步骤,其中几步是抽象方法或钩子,由子类实现;其余步骤在超类里给默认实现。这样流程只写一处,子类只关心「我这一种业务在那几步上有什么不同」。

下面先说明模板方法、抽象步骤和默认步骤在代码里怎么对应,学校缴费流程的示例:学费与住宿费共用「校验学生 → 计算应缴 → 支付 → 记流水 → 发凭证」的骨架,其中计算应缴和记流水由具体缴费类型实现。


很多后台流程是「步骤固定、只是其中几步的实现因业务而异」:例如各类缴费都是「校验 → 算金额 → 支付 → 记账 → 通知」,但学费按学年和学分算、住宿费按宿舍类型算;门禁都是「验卡 → 查权限 → 记通行 → 可选告警」,但宿舍和图书馆的权限规则不同。如果在每个业务类里都完整写一遍流程,会有重复代码,而且一旦要加一步(例如统一加审计),就得改多处;步骤顺序也容易在复制粘贴时搞错。

更稳妥的做法是:把「流程长什么样」提到抽象类里,写一个模板方法,按固定顺序调用 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 形式存在,由服务层按缴费类型选择并执行。

一次缴费请求里带上学生标识和缴费类型相关参数;结果里包含是否成功、流水号等。

package com.example.demo.template;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class PaymentRequest {
private String studentId;
private String academicYear; // 学年,学费用
private String dormId; // 宿舍号,住宿费用
private String feeType; // TUITION | DORM
}
@Data
public class PaymentResult {
private boolean success;
private String ledgerId;
private BigDecimal amount;
private String message;
}

模板方法固定顺序:校验学生 → 计算应缴金额 → 执行支付 → 记流水 → 发凭证。其中「计算应缴」和「记流水」因缴费类型不同,定为抽象方法;校验、支付、发凭证在抽象类里给默认实现(实际可再拆成可覆盖的钩子)。

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 或统一入口)按项目现有实现注入即可;TuitionFeeTuitionLedgerEntryDormLedgerEntry 等实体与表结构按校务系统约定。这里只表达「流程调谁」。

学费按学年查应缴(例如从培养方案或配置里取);住宿费按宿舍类型/楼栋取单价再算。记流水时分别写入学费流水表、住宿费流水表或不同字段,由子类实现。

package com.example.demo.template;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@Component
public 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;
@Component
public 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);
}
}

TuitionFeeRepositoryDormFeePolicyRepository 以及 ledgerRepository.saveTuition / saveDorm 按你项目里的表结构实现即可;重点是「算金额」和「记流水」在子类里各自实现,流程顺序由抽象类的模板方法保证。

服务层根据请求里的 feeType 选对应的流程 Bean,调用其 execute(request),不写 if-else 流程分支。

package com.example.demo.template;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
@RequiredArgsConstructor
public 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 注入,例如:

@Configuration
public class PaymentProcessConfig {
@Bean
public Map<String, AbstractPaymentProcess> paymentProcessMap(
TuitionPaymentProcess tuition,
DormPaymentProcess dorm) {
return Map.of(
"TUITION", tuition,
"DORM", dorm
);
}
}

这样新增一种缴费类型时,加一个继承 AbstractPaymentProcess 的 Bean 并放进 Map 即可,流程骨架不用动。


模板方法在抽象类里定好算法骨架和步骤顺序,把可变步骤留给子类实现,既保证流程一致,又便于扩展。学校缴费示例里,「校验 → 算金额 → 支付 → 记流水 → 发凭证」只写在一处,学费和住宿费通过子类实现「计算应缴」和「记流水」,符合开闭原则;若要支持更多缴费类型或增加统一步骤(例如审计),只需在抽象类里改模板方法或加钩子,子类按需覆盖即可。