模板方法模式:复杂业务代码的解耦与复用之道
用于在模板方法的各个步骤间传递数据,避免参数列表过长。@Data// 用于步骤间传递临时数据在 DAO-Service-Controller 架构中,将模板方法模式应用于 Service 层的抽象基类是处理复杂多变业务流的最佳实践。它既利用了面向对象的多态性来隔离差异,又通过继承机制固化了核心流程,非常适合中国企业中常见的“大流程统一、小细节各异”的业务场景(如多银行支付、多物流对接、多省份政务对
这里写目录标题
在经典的 DAO - Service (业务层) - Controller 三层架构中,模板方法模式(Template Method Pattern) 的最佳落地位置通常是 Service 层(抽象基类)。
为什么放在 Service 层?
- Controller 层太薄:主要负责参数校验、协议转换和调用 Service,不适合承载复杂的业务流程骨架。放在改层代码不方便复用,在三层架构中,controller原则上不允许写业务逻辑。
- DAO 层太底层:只负责单表的 CRUD 或简单查询,无法跨越多张表或外部系统编排复杂的“业务流”。
- Service 层是核心:业务逻辑的核心流转(如:事务控制、前置校验、核心计算、后置通知、异常处理)都在这里。不同子业务(如不同类型的订单、不同渠道的支付)往往遵循相同的流程,但具体实现细节不同。
设计思路:基于 Service 层的模板方法架构
1. 架构分层职责
- Controller:接收请求,转换为 DTO,调用
AbstractService.execute()。 - Service (Abstract):定义
final的模板方法,编排流程(开启事务 -> 校验 -> 处理 -> 记录 -> 提交事务)。 - Service (Concrete):继承抽象类,实现具体的校验逻辑、核心处理逻辑。
- DAO:被 Service 调用,提供数据持久化能力(模板方法中会调用不同的 DAO 组合)。
2. 核心设计图
一、电商场景下复杂业务案例:多类型供应链采购入库系统
业务背景
某大型零售企业的供应链系统需要处理多种类型的采购入库单:
- 普通商品入库:标准流程,校验数量,更新库存,生成财务应付账款。
- 生鲜商品入库:需额外校验保质期、温度记录,若不合格直接拒收,合格则生成损耗预估单。
- 跨境保税入库:需先调用海关接口申报,申报通过后才能入库,并生成保税账册记录。
共同流程(算法骨架):
- 前置准备:加载单据详情,锁定数据库记录(防止并发)。
- 业务校验:根据商品类型执行不同的校验规则。
- 核心入库:更新库存表,生成入库流水。
- 衍生处理:生成财务单据、通知上下游、发送消息。
- 后置清理:释放锁,记录操作日志。
差异点:校验规则不同、核心入库时的附加字段不同、衍生处理的逻辑完全不同。
代码实现
1. 定义上下文对象 (Context)
用于在模板方法的各个步骤间传递数据,避免参数列表过长。
@Data
public class InboundContext {
private Long orderId;
private String orderType; // NORMAL, FRESH, CROSS_BORDER
private InboundOrder orderInfo;
private List<OrderItem> items;
private boolean isSuccess;
private String failReason;
// 用于步骤间传递临时数据
private Map<String, Object> extraData = new HashMap<>();
}
2. 抽象 Service 层 (模板核心)
这里使用 Spring 的 @Transactional 保证整个流程的事务性。
@Service
public abstract class AbstractInboundService {
@Autowired
private InboundOrderDao inboundOrderDao;
@Autowired
private OperationLogDao logDao;
@Autowired
private MessageProducer messageProducer;
/**
* 模板方法:定义不可变的业务流程骨架
* 使用 final 防止子类修改流程顺序
*/
@Transactional(rollbackFor = Exception.class)
public final void execute(InboundContext context) {
try {
// 1. 前置准备 (通用)
prepare(context);
// 2. 业务校验 (子类实现)
validate(context);
// 3. 核心入库处理 (子类实现)
doInboundProcess(context);
// 4. 衍生业务处理 (子类实现,可选钩子)
postInboundProcess(context);
// 5. 通用后置:发送成功消息
sendSuccessNotification(context);
context.setSuccess(true);
} catch (BusinessException e) {
// 捕获业务异常,标记失败
context.setSuccess(false);
context.setFailReason(e.getMessage());
handleBusinessError(context, e);
throw e; // 回滚事务
} catch (Exception e) {
// 捕获系统异常
context.setSuccess(false);
context.setFailReason("系统异常:" + e.getMessage());
handleSystemError(context, e);
throw new SystemException("入库流程执行失败", e);
} finally {
// 6. 最终清理:记录日志 (无论成功失败都执行)
logExecution(context);
releaseLock(context);
}
}
// --- 通用步骤实现 ---
protected void prepare(InboundContext context) {
// 加载订单详情
context.setOrderInfo(inboundOrderDao.selectById(context.getOrderId()));
context.setItems(inboundOrderDao.selectItems(context.getOrderId()));
// 分布式锁逻辑 (伪代码)
// lockService.lock("INBOUND_" + context.getOrderId());
System.out.println("[通用] 加载订单并加锁: " + context.getOrderId());
}
protected void sendSuccessNotification(InboundContext context) {
messageProducer.send("inbound.success", context.getOrderId());
}
protected void handleBusinessError(InboundContext context, BusinessException e) {
System.out.println("[通用] 记录业务错误告警");
}
protected void handleSystemError(InboundContext context, Exception e) {
System.out.println("[通用] 记录系统异常堆栈并通知运维");
}
protected void logExecution(InboundContext context) {
OperationLog log = new OperationLog();
log.setOrderId(context.getOrderId());
log.setStatus(context.isSuccess() ? "SUCCESS" : "FAIL");
log.setRemark(context.getFailReason());
logDao.insert(log);
}
protected void releaseLock(InboundContext context) {
// lockService.unlock("INBOUND_" + context.getOrderId());
System.out.println("[通用] 释放锁");
}
// --- 抽象步骤 (强制子类实现) ---
/**
* 步骤2:校验逻辑
* 不同商品类型校验规则完全不同
*/
protected abstract void validate(InboundContext context) throws BusinessException;
/**
* 步骤3:核心入库
* 更新库存表,写入入库明细
*/
protected abstract void doInboundProcess(InboundContext context) throws BusinessException;
/**
* 步骤4:衍生处理 (钩子方法)
* 默认空实现,子类按需覆盖
*/
protected void postInboundProcess(InboundContext context) throws BusinessException {
// 默认不做任何事
}
}
3. 具体业务实现类
场景 A:生鲜入库 (需校验保质期,生成损耗单)
@Service
public class FreshInboundService extends AbstractInboundService {
@Autowired
private FreshStockDao freshStockDao;
@Autowired
private LossEstimateDao lossEstimateDao;
@Override
protected void validate(InboundContext context) {
System.out.println("[生鲜] 校验保质期和温度记录...");
for (OrderItem item : context.getItems()) {
if (item.getExpireDays() < 3) {
throw new BusinessException("生鲜商品剩余保质期不足3天,拒收");
}
if (item.getTransportTemp() > 5) {
throw new BusinessException("运输温度超标,拒收");
}
}
}
@Override
protected void doInboundProcess(InboundContext context) {
System.out.println("[生鲜] 更新生鲜专用库存表,记录批次号和生产日期...");
// 调用 DAO 更新特定字段
freshStockDao.batchInsert(context.getItems());
}
@Override
protected void postInboundProcess(InboundContext context) {
System.out.println("[生鲜] 计算预计损耗率,生成损耗预估单...");
// 特有逻辑:生鲜需要预估损耗
LossEstimate estimate = calculateLoss(context.getItems());
lossEstimateDao.insert(estimate);
context.getExtraData().put("lossId", estimate.getId());
}
private LossEstimate calculateLoss(List<OrderItem> items) {
// 复杂计算逻辑
return new LossEstimate();
}
}
场景 B:跨境保税入库 (需先报关)
@Service
public class CrossBorderInboundService extends AbstractInboundService {
@Autowired
private BondedStockDao bondedStockDao;
@Autowired
private CustomsClient customsClient; // 调用外部海关接口
@Autowired
private BondedLedgerDao ledgerDao;
@Override
protected void validate(InboundContext context) {
System.out.println("[跨境] 校验备案清单状态和额度...");
// 校验是否在海关备案清单内
if (!customsClient.checkManifestStatus(context.getOrderId())) {
throw new BusinessException("海关备案清单状态异常");
}
}
@Override
protected void doInboundProcess(InboundContext context) {
System.out.println("[跨境] 调用海关接口申报入库...");
// 关键差异:必须先调外部接口,成功后才写库
String customsNo = customsClient.declareInbound(context.getOrderInfo());
context.getExtraData().put("customsNo", customsNo);
System.out.println("[跨境] 更新保税仓库存表...");
bondedStockDao.batchInsert(context.getItems(), customsNo);
}
@Override
protected void postInboundProcess(InboundContext context) {
System.out.println("[跨境] 生成保税电子账册记录...");
String customsNo = (String) context.getExtraData().get("customsNo");
ledgerDao.createLedgerRecord(context.getOrderId(), customsNo);
}
// 甚至可以重写错误处理,跨境失败可能需要触发自动重试报关
@Override
protected void handleBusinessError(InboundContext context, BusinessException e) {
super.handleBusinessError(context, e);
if (e.getMessage().contains("海关")) {
System.out.println("[跨境] 触发海关申报重试队列...");
// retryQueue.add(context.getOrderId());
}
}
}
4. Controller 层调用
Controller 层非常干净,只需要根据类型获取对应的 Service 实例(通常通过工厂模式或 Map 注入)。
@RestController
@RequestMapping("/inbound")
public class InboundController {
// 通过 Map 注入所有实现类,Key 为 Bean 名称或自定义注解值
@Autowired
private Map<String, AbstractInboundService> inboundServiceMap;
@PostMapping("/execute")
public Result<Void> execute(@RequestBody InboundDTO dto) {
InboundContext context = convertToContext(dto);
// 根据类型路由到具体的 Service
AbstractInboundService service = inboundServiceMap.get(getServiceBeanName(context.getOrderType()));
if (service == null) {
return Result.fail("不支持的入库类型");
}
// 执行模板方法
service.execute(context);
return Result.success();
}
private String getServiceBeanName(String type) {
// 简单的映射逻辑,实际可用策略模式优化查找
switch (type) {
case "FRESH": return "freshInboundService";
case "CROSS_BORDER": return "crossBorderInboundService";
default: return "normalInboundService"; // 假设有一个默认实现
}
}
}
这种设计的优势
-
事务一致性保障:
在抽象类的execute方法上标注@Transactional,确保了从校验、入库到衍生处理的全过程要么全成功,要么全回滚。子类无需关心事务边界,避免了在子类中错误地拆分事务。 -
流程标准化与合规:
对于金融、供应链等强合规场景,prepare(加锁)、logExecution(审计日志)、releaseLock(资源释放)等关键步骤由父类强制执行,子类无法跳过,杜绝了“忘记写日志”或“忘记释放锁”的隐患。 -
高内聚低耦合:
- DAO 层:保持纯粹的数据访问,不掺杂业务判断。
- Service 层:抽象类管流程,具体类管业务规则。新增一种入库类型(如“退货入库”),只需新增一个类继承抽象类,无需修改现有代码(开闭原则)。
-
便于单元测试:
- 可以单独测试抽象类中的通用逻辑(如日志记录是否正确)。
- 可以单独 Mock DAO 测试某个具体子类的业务逻辑(如生鲜的保质期校验)。
-
应对复杂变化:
如果未来公司要求所有入库流程在“核心处理”前增加一步“AI 风险预测”,只需在抽象类的execute方法中插入一行代码aiRiskCheck(context),所有子类自动生效,无需逐个修改。
上面的案例好则好矣,但是最大的问题是违反了软件设计中的“开闭原则”。所以一下案例会通过一个策略工程去进行优化。
在 Java 开发中,模板方法模式(Template Method Pattern) 的核心目的是定义一个操作中的算法骨架,而将一些步骤延迟到子类中。它让子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
当结合 策略模式(Strategy Pattern) 使用时,通常是为了解决“算法骨架固定,但具体实现类需要根据运行时条件动态选择”的场景。这种组合在 DAO - Service - Controller 架构中非常强大,常用于处理流程标准化但业务细节差异化的复杂场景。
二、核心设计理念:在三层架构中的位置
| 层级 | 职责描述 | 模板方法 + 策略模式的应用 |
|---|---|---|
| Controller | 接收请求,识别业务类型(如 type=IMPORT_EXCEL),调用 Service。 |
传递业务类型标识给 Service,无需关心具体执行逻辑。 |
| Service | 核心战场。1. 抽象模板类 (AbstractProcessor):定义标准流程(校验 -> 解析 -> 转换 -> 保存 -> 通知)。2. 具体实现类:实现具体的步骤(如 Excel 解析 vs CSV 解析)。3. 策略工厂 (ProcessorFactory):根据类型标识,从 Spring 容器中获取对应的具体实现类。 |
利用 Spring 的 Map<String, Strategy> 自动注入特性实现策略分发。模板方法保证流程不乱,策略模式保证灵活扩展。 |
| DAO | 提供数据持久化接口。 | 被模板方法中的 save 步骤调用。 |
设计图解:
三、复杂业务案例:多源异构“数据导入中心”
1. 业务背景
某金融风控系统需要支持多种渠道的数据导入,用于更新客户风险评级。
- 来源多样性:
- Excel 文件:银行线下报送,格式复杂,包含合并单元格,需要复杂的表头校验。
- CSV 文件:第三方合作机构推送,格式简单,但字符集编码常有问题。
- JSON API:内部其他系统实时推送,数据结构嵌套深。
- XML 文件:老旧监管系统导出,结构严格,需 Schema 校验。
- 流程一致性:无论哪种来源,都必须遵循严格的五步法:
- 前置校验(文件格式、大小、签名)。
- 数据解析(读取文件流转为内存对象)。
- 数据清洗与转换(字典映射、空值处理、格式标准化)。
- 业务落库(批量插入/更新,事务控制)。
- 后置处理(发送通知、记录审计日志、触发风控规则)。
痛点:
如果不用模板方法,每个导入逻辑里都会复制粘贴这五步代码,只是中间的实现不同。一旦流程变更(比如在“落库”前增加一步“黑名单过滤”),需要修改所有实现类,极易遗漏。
2. 架构设计实现
(1) 定义抽象模板类 (AbstractDataImporter)
这是核心,定义了不可变的算法骨架和可变的抽象步骤。
public abstract class AbstractDataImporter {
/**
* 模板方法:定义最终执行流程 (final 防止子类修改流程顺序)
*/
public final void execute(InputStream inputStream, String fileName) {
log.info("开始导入任务:{}", fileName);
// 1. 前置校验
validate(inputStream, fileName);
// 2. 数据解析
List<RiskData> rawData = parse(inputStream);
log.info("解析完成,共 {} 条记录", rawData.size());
// 3. 数据清洗与转换
List<RiskData> cleanData = transform(rawData);
// 4. 业务落库 (通常包含事务)
save(cleanData);
// 5. 后置处理
postProcess(cleanData.size());
log.info("导入任务成功结束:{}", fileName);
}
/**
* 步骤 1:校验 (子类必须实现)
*/
protected abstract void validate(InputStream inputStream, String fileName);
/**
* 步骤 2:解析 (子类必须实现)
*/
protected abstract List<RiskData> parse(InputStream inputStream);
/**
* 步骤 3:转换 (子类可选覆盖,提供默认空实现)
*/
protected List<RiskData> transform(List<RiskData> rawData) {
// 默认做一些通用清洗,子类可扩展
return rawData.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 步骤 4:保存 (子类必须实现,或调用 DAO)
*/
protected abstract void save(List<RiskData> data);
/**
* 步骤 5:后置处理 (钩子方法,子类可选覆盖)
*/
protected void postProcess(int count) {
// 默认发送通用通知
notificationService.sendGeneralReport(count);
}
}
(2) 具体实现类 (Concrete Implementations)
这些类只关注自己特有的逻辑,完全不用管流程顺序。
A. Excel 导入器 (ExcelDataImporter)
@Component
public class ExcelDataImporter extends AbstractDataImporter {
@Autowired
private RiskDataDAO riskDataDAO;
@Override
protected void validate(InputStream inputStream, String fileName) {
if (!fileName.endsWith(".xlsx")) {
throw new BusinessException("仅支持 .xlsx 格式");
}
// 检查文件大小、Excel 结构完整性等
}
@Override
protected List<RiskData> parse(InputStream inputStream) {
// 使用 POI 或 EasyExcel 解析复杂的 Excel
System.out.println("正在使用 EasyExcel 解析...");
// 模拟解析逻辑
return EasyExcel.read(inputStream).head(RiskData.class).sheet().doReadSync();
}
@Override
protected List<RiskData> transform(List<RiskData> rawData) {
// Excel 特有逻辑:处理合并单元格导致的空值,映射特定的字典
return rawData.stream().map(this::fixExcelMergedCells).collect(Collectors.toList());
}
@Override
protected void save(List<RiskData> data) {
// Excel 数据量大,采用分批插入
List<List<RiskData>> partitions = Lists.partition(data, 1000);
for (List<RiskData> batch : partitions) {
riskDataDAO.batchInsert(batch);
}
}
@Override
protected void postProcess(int count) {
super.postProcess(count);
// Excel 导入特有:发送邮件给银行对接人
emailService.sendToBankContact(count);
}
private RiskData fixExcelMergedCells(RiskData data) {
// 具体修复逻辑
return data;
}
}
B. CSV 导入器 (CsvDataImporter)
@Component
public class CsvDataImporter extends AbstractDataImporter {
@Autowired
private RiskDataDAO riskDataDAO;
@Override
protected void validate(InputStream inputStream, String fileName) {
if (!fileName.endsWith(".csv")) {
throw new BusinessException("仅支持 .csv 格式");
}
// 检查编码是否为 UTF-8
}
@Override
protected List<RiskData> parse(InputStream inputStream) {
// 使用 OpenCSV 解析,注意处理 GBK 编码
System.out.println("正在使用 OpenCSV 解析 (GBK)...");
// 模拟解析
return new ArrayList<>();
}
@Override
protected void save(List<RiskData> data) {
// CSV 数据通常较小,直接全量插入
riskDataDAO.batchInsert(data);
}
// transform 和 postProcess 使用父类默认实现或按需覆盖
}
C. JSON API 导入器 (JsonApiImporter)
(逻辑类似,解析逻辑使用 Jackson/Fastjson)
(3) 策略工厂 (ImporterFactory)
利用 Spring 的特性,自动收集所有实现类并建立映射。
@Component
public class ImporterFactory {
// Key: 业务类型标识 (如 "EXCEL", "CSV"), Value: 对应的实现类 Bean
private final Map<String, AbstractDataImporter> importerMap;
// Spring 会自动将所有 AbstractDataImporter 的子类 Bean 注入到这个 List 中
public ImporterFactory(List<AbstractDataImporter> importers) {
this.importerMap = new HashMap<>();
for (AbstractDataImporter importer : importers) {
// 关键:如何确定 Key?
// 方案 A: 使用 @Qualifier 注解配合自定义注解
// 方案 B: 让子类提供一个 getType() 方法
// 这里演示方案 B,更灵活
String type = importer.getSupportType();
if (importerMap.containsKey(type)) {
throw new IllegalStateException("重复的导入类型: " + type);
}
importerMap.put(type, importer);
}
}
public AbstractDataImporter getImporter(String type) {
AbstractDataImporter importer = importerMap.get(type.toUpperCase());
if (importer == null) {
throw new BusinessException("不支持的导入类型: " + type);
}
return importer;
}
}
需要在抽象类中增加 getSupportType 方法:
// 在 AbstractDataImporter 中
protected abstract String getSupportType();
// 在 ExcelDataImporter 中实现返回 "EXCEL"
(4) Service 层整合
@Service
public class DataImportService {
@Autowired
private ImporterFactory importerFactory;
@Transactional // 整个导入过程的事务控制(视具体需求,有时解析不需要事务)
public void importData(String type, MultipartFile file) {
try {
// 1. 通过策略工厂获取具体的实现类
AbstractDataImporter importer = importerFactory.getImporter(type);
// 2. 执行模板方法
importer.execute(file.getInputStream(), file.getOriginalFilename());
} catch (IOException e) {
throw new BusinessException("文件读取失败", e);
}
}
}
(5) Controller 层
@RestController
@RequestMapping("/data")
public class DataImportController {
@Autowired
private DataImportService importService;
@PostMapping("/upload")
public Result<Void> upload(@RequestParam("type") String type,
@RequestParam("file") MultipartFile file) {
importService.importData(type, file);
return Result.success();
}
}
三、这种设计的核心优势
1. 流程控制与业务实现的完美分离
- 模板方法保证了“校验->解析->转换->保存->通知”这个核心流程在所有导入场景中是强制且一致的。新人开发新的导入类型时,不可能漏掉“校验”或“保存”步骤。
- 策略模式使得具体的解析算法、保存策略可以独立变化,互不干扰。
2. 极高的可扩展性 (OCP)
- 新增一种格式(如 XML):只需新建一个
XmlDataImporter继承抽象类,实现 5 个方法,注册到 Spring 容器即可。不需要修改DataImportService、ImporterFactory或其他已有的 Importer 类。 - 修改流程:如果老板要求在所有导入前增加一步“病毒扫描”,只需在
AbstractDataImporter.execute()的第一行加一行代码,所有子类立即生效。
3. 代码复用与最小化冗余
通用的逻辑(如日志记录、异常捕获框架、通用的数据清洗规则)都写在父类中。子类只写几十行真正差异化的代码(如 POI 的写法 vs OpenCSV 的写法)。
4. 策略的动态路由
通过 ImporterFactory,系统将“硬编码”的 if (type.equals("EXCEL")) 变成了基于 Map 的动态查找。这使得系统可以轻松支持配置化的类型扩展,甚至可以从数据库加载类型配置。
五、注意事项与潜在陷阱
-
父类耦合度:
模板方法模式会导致子类对父类产生依赖。如果父类的流程设计不合理,子类可能会通过抛出异常或重写final方法(如果能绕过)来“对抗”流程。- 对策:在设计初期充分调研业务流程,确保骨架的通用性。对于确实不适用的步骤,可以提供空的默认实现或抛出不支持异常。
-
钩子方法(Hooks)的使用:
除了抽象方法,还可以定义钩子方法(有默认实现的普通方法),让子类选择性覆盖。例如boolean needTransaction(),默认返回 true,某些特殊导入可以返回 false 以关闭事务。这增加了灵活性。 -
Spring Bean 的管理:
在使用策略工厂时,要确保所有实现类都被 Spring 管理(加上@Component)。如果实现类有复杂的构造函数依赖,Spring 会自动处理,这比手动维护策略 Map 要方便得多。 -
事务边界:
注意execute方法上的事务注解。如果parse步骤非常耗时(如解析几百兆文件),长时间持有数据库事务可能导致连接池耗尽。- 对策:可以将
parse放在事务外,只在save步骤开启事务(通过在 Service 层拆分调用,或在模板方法内部编程式控制事务)。
- 对策:可以将
六、总结
在 DAO-Service-Controller 架构中,模板方法模式 + 策略模式 是处理**“标准化流程 + 多样化实现”**业务的黄金组合。
- 适用场景:
- 多格式文件导入/导出。
- 多渠道支付/通知发送(流程都是:准备参数->签名->发送->处理回调,但细节不同)。
- 报表生成(流程:查数->计算->格式化->输出,输出格式不同)。
- 工作流引擎中的节点处理。
- 核心价值:
- 模板方法:固化流程,防止逻辑遗漏,实现“反向控制”(父类调用子类)。
- 策略模式:解耦具体实现,支持运行时动态切换,符合开闭原则。
- 设计关键:
- 提取公共流程到
abstract父类,用final修饰模板方法。 - 将变化点定义为
abstract方法或hook方法。 - 利用 Spring 的依赖注入特性构建策略工厂,避免硬编码
if-else。
- 提取公共流程到
这种设计让你的代码既有军队的纪律性(流程统一),又有特种部队的灵活性(单兵作战能力强)。
更多推荐


所有评论(0)