模板方法模式:复杂业务代码的解耦与复用之道
在经典的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. 核心设计图AbstractBusinessServicefinal execute(Context ctx)#abstract validate(Context ctx)#abstract doProcess(Context ctx)#abstract postProcess(Context ctx)-logAction(Context ctx)ConcreteServiceA-orderDao-inventoryDaovalidate()doProcess()postProcess()ConcreteServiceB-vipOrderDao-pointDaovalidate()doProcess()postProcess()OrderDAOInventoryDAO复杂业务案例多类型供应链采购入库系统业务背景某大型零售企业的供应链系统需要处理多种类型的采购入库单普通商品入库标准流程校验数量更新库存生成财务应付账款。生鲜商品入库需额外校验保质期、温度记录若不合格直接拒收合格则生成损耗预估单。跨境保税入库需先调用海关接口申报申报通过后才能入库并生成保税账册记录。共同流程算法骨架前置准备加载单据详情锁定数据库记录防止并发。业务校验根据商品类型执行不同的校验规则。核心入库更新库存表生成入库流水。衍生处理生成财务单据、通知上下游、发送消息。后置清理释放锁记录操作日志。差异点校验规则不同、核心入库时的附加字段不同、衍生处理的逻辑完全不同。代码实现1. 定义上下文对象 (Context)用于在模板方法的各个步骤间传递数据避免参数列表过长。DatapublicclassInboundContext{privateLongorderId;privateStringorderType;// NORMAL, FRESH, CROSS_BORDERprivateInboundOrderorderInfo;privateListOrderItemitems;privatebooleanisSuccess;privateStringfailReason;// 用于步骤间传递临时数据privateMapString,ObjectextraDatanewHashMap();}2. 抽象 Service 层 (模板核心)这里使用 Spring 的Transactional保证整个流程的事务性。ServicepublicabstractclassAbstractInboundService{AutowiredprivateInboundOrderDaoinboundOrderDao;AutowiredprivateOperationLogDaologDao;AutowiredprivateMessageProducermessageProducer;/** * 模板方法定义不可变的业务流程骨架 * 使用 final 防止子类修改流程顺序 */Transactional(rollbackForException.class)publicfinalvoidexecute(InboundContextcontext){try{// 1. 前置准备 (通用)prepare(context);// 2. 业务校验 (子类实现)validate(context);// 3. 核心入库处理 (子类实现)doInboundProcess(context);// 4. 衍生业务处理 (子类实现可选钩子)postInboundProcess(context);// 5. 通用后置发送成功消息sendSuccessNotification(context);context.setSuccess(true);}catch(BusinessExceptione){// 捕获业务异常标记失败context.setSuccess(false);context.setFailReason(e.getMessage());handleBusinessError(context,e);throwe;// 回滚事务}catch(Exceptione){// 捕获系统异常context.setSuccess(false);context.setFailReason(系统异常e.getMessage());handleSystemError(context,e);thrownewSystemException(入库流程执行失败,e);}finally{// 6. 最终清理记录日志 (无论成功失败都执行)logExecution(context);releaseLock(context);}}// --- 通用步骤实现 ---protectedvoidprepare(InboundContextcontext){// 加载订单详情context.setOrderInfo(inboundOrderDao.selectById(context.getOrderId()));context.setItems(inboundOrderDao.selectItems(context.getOrderId()));// 分布式锁逻辑 (伪代码)// lockService.lock(INBOUND_ context.getOrderId());System.out.println([通用] 加载订单并加锁: context.getOrderId());}protectedvoidsendSuccessNotification(InboundContextcontext){messageProducer.send(inbound.success,context.getOrderId());}protectedvoidhandleBusinessError(InboundContextcontext,BusinessExceptione){System.out.println([通用] 记录业务错误告警);}protectedvoidhandleSystemError(InboundContextcontext,Exceptione){System.out.println([通用] 记录系统异常堆栈并通知运维);}protectedvoidlogExecution(InboundContextcontext){OperationLoglognewOperationLog();log.setOrderId(context.getOrderId());log.setStatus(context.isSuccess()?SUCCESS:FAIL);log.setRemark(context.getFailReason());logDao.insert(log);}protectedvoidreleaseLock(InboundContextcontext){// lockService.unlock(INBOUND_ context.getOrderId());System.out.println([通用] 释放锁);}// --- 抽象步骤 (强制子类实现) ---/** * 步骤2校验逻辑 * 不同商品类型校验规则完全不同 */protectedabstractvoidvalidate(InboundContextcontext)throwsBusinessException;/** * 步骤3核心入库 * 更新库存表写入入库明细 */protectedabstractvoiddoInboundProcess(InboundContextcontext)throwsBusinessException;/** * 步骤4衍生处理 (钩子方法) * 默认空实现子类按需覆盖 */protectedvoidpostInboundProcess(InboundContextcontext)throwsBusinessException{// 默认不做任何事}}3. 具体业务实现类场景 A生鲜入库 (需校验保质期生成损耗单)ServicepublicclassFreshInboundServiceextendsAbstractInboundService{AutowiredprivateFreshStockDaofreshStockDao;AutowiredprivateLossEstimateDaolossEstimateDao;Overrideprotectedvoidvalidate(InboundContextcontext){System.out.println([生鲜] 校验保质期和温度记录...);for(OrderItemitem:context.getItems()){if(item.getExpireDays()3){thrownewBusinessException(生鲜商品剩余保质期不足3天拒收);}if(item.getTransportTemp()5){thrownewBusinessException(运输温度超标拒收);}}}OverrideprotectedvoiddoInboundProcess(InboundContextcontext){System.out.println([生鲜] 更新生鲜专用库存表记录批次号和生产日期...);// 调用 DAO 更新特定字段freshStockDao.batchInsert(context.getItems());}OverrideprotectedvoidpostInboundProcess(InboundContextcontext){System.out.println([生鲜] 计算预计损耗率生成损耗预估单...);// 特有逻辑生鲜需要预估损耗LossEstimateestimatecalculateLoss(context.getItems());lossEstimateDao.insert(estimate);context.getExtraData().put(lossId,estimate.getId());}privateLossEstimatecalculateLoss(ListOrderItemitems){// 复杂计算逻辑returnnewLossEstimate();}}场景 B跨境保税入库 (需先报关)ServicepublicclassCrossBorderInboundServiceextendsAbstractInboundService{AutowiredprivateBondedStockDaobondedStockDao;AutowiredprivateCustomsClientcustomsClient;// 调用外部海关接口AutowiredprivateBondedLedgerDaoledgerDao;Overrideprotectedvoidvalidate(InboundContextcontext){System.out.println([跨境] 校验备案清单状态和额度...);// 校验是否在海关备案清单内if(!customsClient.checkManifestStatus(context.getOrderId())){thrownewBusinessException(海关备案清单状态异常);}}OverrideprotectedvoiddoInboundProcess(InboundContextcontext){System.out.println([跨境] 调用海关接口申报入库...);// 关键差异必须先调外部接口成功后才写库StringcustomsNocustomsClient.declareInbound(context.getOrderInfo());context.getExtraData().put(customsNo,customsNo);System.out.println([跨境] 更新保税仓库存表...);bondedStockDao.batchInsert(context.getItems(),customsNo);}OverrideprotectedvoidpostInboundProcess(InboundContextcontext){System.out.println([跨境] 生成保税电子账册记录...);StringcustomsNo(String)context.getExtraData().get(customsNo);ledgerDao.createLedgerRecord(context.getOrderId(),customsNo);}// 甚至可以重写错误处理跨境失败可能需要触发自动重试报关OverrideprotectedvoidhandleBusinessError(InboundContextcontext,BusinessExceptione){super.handleBusinessError(context,e);if(e.getMessage().contains(海关)){System.out.println([跨境] 触发海关申报重试队列...);// retryQueue.add(context.getOrderId());}}}4. Controller 层调用Controller 层非常干净只需要根据类型获取对应的 Service 实例通常通过工厂模式或 Map 注入。RestControllerRequestMapping(/inbound)publicclassInboundController{// 通过 Map 注入所有实现类Key 为 Bean 名称或自定义注解值AutowiredprivateMapString,AbstractInboundServiceinboundServiceMap;PostMapping(/execute)publicResultVoidexecute(RequestBodyInboundDTOdto){InboundContextcontextconvertToContext(dto);// 根据类型路由到具体的 ServiceAbstractInboundServiceserviceinboundServiceMap.get(getServiceBeanName(context.getOrderType()));if(servicenull){returnResult.fail(不支持的入库类型);}// 执行模板方法service.execute(context);returnResult.success();}privateStringgetServiceBeanName(Stringtype){// 简单的映射逻辑实际可用策略模式优化查找switch(type){caseFRESH:returnfreshInboundService;caseCROSS_BORDER:returncrossBorderInboundService;default:returnnormalInboundService;// 假设有一个默认实现}}}这种设计的优势事务一致性保障在抽象类的execute方法上标注Transactional确保了从校验、入库到衍生处理的全过程要么全成功要么全回滚。子类无需关心事务边界避免了在子类中错误地拆分事务。流程标准化与合规对于金融、供应链等强合规场景prepare加锁、logExecution审计日志、releaseLock资源释放等关键步骤由父类强制执行子类无法跳过杜绝了“忘记写日志”或“忘记释放锁”的隐患。高内聚低耦合DAO 层保持纯粹的数据访问不掺杂业务判断。Service 层抽象类管流程具体类管业务规则。新增一种入库类型如“退货入库”只需新增一个类继承抽象类无需修改现有代码开闭原则。便于单元测试可以单独测试抽象类中的通用逻辑如日志记录是否正确。可以单独 Mock DAO 测试某个具体子类的业务逻辑如生鲜的保质期校验。应对复杂变化如果未来公司要求所有入库流程在“核心处理”前增加一步“AI 风险预测”只需在抽象类的execute方法中插入一行代码aiRiskCheck(context)所有子类自动生效无需逐个修改。总结在 DAO-Service-Controller 架构中将模板方法模式应用于 Service 层的抽象基类是处理复杂多变业务流的最佳实践。它既利用了面向对象的多态性来隔离差异又通过继承机制固化了核心流程非常适合中国企业中常见的“大流程统一、小细节各异”的业务场景如多银行支付、多物流对接、多省份政务对接等。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2433930.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!