一、核心设计模式定义&设计初衷
1. 策略模式(Strategy Pattern)
定义 :一种行为型设计模式,定义一个统一的策略接口 ,将多个实现了该接口的具体策略类 封装为独立的业务逻辑单元,策略之间可互相替换,且不影响使用策略的客户端代码。
在你的业务中 :INodeAuditHandler是策略接口 ,定义了validate(校验)、execute(执行)的统一方法契约;ApplyInformationFirst(进件第一步)、BankInsuranceBeginAudit(保银初审)是具体策略类,每个类封装一个审核节点的专属业务逻辑,彼此独立、可替换。
2. 工厂模式(简单工厂+Spring容器增强)
定义 :一种创建型设计模式,提供一个工厂类 封装对象的创建逻辑,客户端无需关心对象的创建细节,只需通过指定标识获取对应的对象实例。
在你的业务中 :NodeAuditHandlerFactory是工厂类 ,封装了INodeAuditHandler所有实现类的获取逻辑;通过Spring容器自动注入的Map<String, INodeAuditHandler>管理所有策略类实例,客户端只需传入节点标识 (如applyInformationFirst),即可获取对应的策略类实例,无需手动创建。
3. 为什么要结合「策略模式+工厂模式」写?
你的原有代码是一个审核节点对应一个接口,存在「接口冗余、新增节点需修改原有代码、维护成本高」的问题;而结合两种模式,核心解决**「解耦」和「可扩展」** 两大核心痛点,同时适配「多节点审核、各节点逻辑独立、后续需持续新增节点」的业务特性,具体原因:
- 单靠策略模式:能实现业务逻辑解耦,但客户端(接口层)仍需手动注入/选择策略类,无法实现「单接口适配多节点」;
- 单靠工厂模式:能封装对象创建,但无统一的接口约束,各节点逻辑会杂乱无章,无法保证代码规范;
- 两者结合:策略模式保证业务逻辑的标准化、解耦性 ,工厂模式实现策略类的动态获取、单接口统一调度,完美适配你的进件审核业务。
4. 这样写的核心好处
- 符合开闭原则 :新增审核节点(如GPS安装审核、终审),仅需新增策略类(实现
INodeAuditHandler),无需修改工厂类、通用接口等原有代码,做到「对扩展开放,对修改关闭」; - 消除代码冗余:用1个通用接口替代N个节点专属接口,统一复用「校验→执行」的核心流程,避免重复编写接口方法和业务调用逻辑;
- 逻辑边界清晰 :每个审核节点的校验、业务逻辑封装在独立的策略类中,问题定位快(如进件第一步异常仅需排查
ApplyInformationFirst),便于团队分工维护; - 降低耦合度:接口层、工厂层、策略层各司其职,接口层仅负责接收请求,工厂层仅负责获取策略类,策略层仅负责实现专属业务,层与层之间无硬编码依赖;
- 扩展性极强:后续可无缝扩展节点优先级、节点组合校验等功能,无需重构核心架构。
二、基于你的代码的完整实现方案(直接复用)
所有代码基于你提供的INodeAuditHandler、ApplyInformationFirst、BankInsuranceBeginAudit改造,无需修改原有策略类,仅新增工厂类和通用接口,快速落地。
1. 原有代码(无需修改,基础支撑)
(1)策略接口:INodeAuditHandler
java
public interface INodeAuditHandler {
R validate(BladeUser bladeUser, Object... params);
R execute(BladeUser bladeUser,Object... params) throws JsonProcessingException;
}
(2)具体策略类1:进件第一步(已标注@Component,Bean名称:applyInformationFirst)
java
@Component("applyInformationFirst")
@AllArgsConstructor
@Slf4j
public class ApplyInformationFirst implements INodeAuditHandler {
private final String logTags = "====>进件初步提交";
private final InformationService informationService;
@Override
public R validate(BladeUser bladeUser, Object... params) {
log.info(logTags+"校验!!!");
InformationDTO info = (InformationDTO) params[0];
StringHelper.IsEmptyOrNullToMes(info.getCreateUser(), "业务员不能为空");
return null;
}
@Override
public R execute(BladeUser bladeUser, Object... params) throws JsonProcessingException {
log.info(logTags+"业务逻辑!!!");
InformationDTO info = (InformationDTO) params[0];
R r = informationService.applyInfomationFirst( info);
if(r.isSuccess() && r.getCode()==500){
return r;
}
return r;
}
}
(3)具体策略类2:保银初审(已标注@Component,Bean名称:bankInsuranceBeginAudit)
java
@Component("bankInsuranceBeginAudit")
@AllArgsConstructor
@Slf4j
public class BankInsuranceBeginAudit implements INodeAuditHandler {
private final String logTags = "====>初审";
private final InformationFlowableService informationFlowableService;
private final InformationMaterialsTempServiceImpl informationMaterialsTempService;
private final InformationMaterialsService informationMaterialsService;
@Override
public R validate(BladeUser bladeUser, Object... params) {
log.info(logTags + "校验!!!");
NodeDTO nodeDTO = (NodeDTO) params[0];
String operatorType = nodeDTO.getOperatorType();
if (!FlowableOperatorType.RESOLVE.getValue().equals(operatorType)
&& !FlowableOperatorType.REJECT_NODE.getValue().equals(operatorType)) {
throw new ServiceException("该节点仅支持通过/拒单操作");
}
if (StringUtil.isBlank(nodeDTO.getRemark())) {
throw new ServiceException("请填写审核意见");
}
return null;
}
@Override
@Transactional
public R execute(BladeUser bladeUser, Object... params) {
log.info(logTags + "业务逻辑!!!");
NodeDTO nodeDTO = (NodeDTO) params[0];
nodeDTO.setNodeName(FlowNodeEnum.BANK_INSURANCE_BEGIN_AUDIT.getDesc());
nodeDTO.setNodeKey(FlowNodeEnum.BANK_INSURANCE_BEGIN_AUDIT.getNode());
Long informationId = nodeDTO.getInformationId();
return informationFlowableService.doReview(nodeDTO, bladeUser);
}
}
2. 新增代码(核心改造,2个类即可实现)
(1)工厂类:NodeAuditHandlerFactory(策略类仓库,统一获取逻辑)
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 节点审核处理器工厂类
* 封装INodeAuditHandler所有实现类的获取逻辑,Spring自动注入所有策略类到Map
*/
@Component
public class NodeAuditHandlerFactory {
/**
* 核心:Spring容器启动时自动注入
* Key:策略类的@Component注解值(如applyInformationFirst、bankInsuranceBeginAudit)
* Value:对应的策略类实例(如ApplyInformationFirst、BankInsuranceBeginAudit)
*/
@Autowired
private Map<String, INodeAuditHandler> auditHandlerMap;
/**
* 根据节点标识获取对应的策略类实例
* @param handlerName 节点标识(与策略类@Component注解值完全一致)
* @return 对应的INodeAuditHandler实现类
* @throws ServiceException 未找到对应策略类时抛出明确异常,便于排查
*/
public INodeAuditHandler getHandler(String handlerName) {
INodeAuditHandler handler = auditHandlerMap.get(handlerName);
if (handler == null) {
throw new ServiceException("未找到对应的审核节点处理器,节点标识:" + handlerName + ",请检查策略类@Component注解值是否正确");
}
return handler;
}
}
(2)通用接口控制器:CommonNodeAuditController(单接口适配所有节点)
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 通用审核节点控制器
* 一个接口适配所有INodeAuditHandler实现类,无需为每个节点单独写接口
*/
@RestController
@RequestMapping("/api/node/audit")
public class CommonNodeAuditController {
// 注入工厂类,通过工厂类动态获取策略类
@Autowired
private NodeAuditHandlerFactory handlerFactory;
/**
* 通用审核接口:核心入口,所有审核节点请求均走此接口
* @param handlerName 路径参数:节点标识(与策略类@Component注解值一致)
* @param params 请求体:节点入参(兼容InformationDTO、NodeDTO等所有入参类型)
* @param bladeUser 登录用户信息(原有业务参数,Spring自动注入,无需修改)
* @return R 审核结果(与原有业务返回格式一致)
*/
@PostMapping("/{handlerName}")
public R audit(
@PathVariable String handlerName, // 核心:从URL获取节点标识,匹配策略类
@RequestBody @Valid Object params, // 通用入参,兼容所有节点的入参DTO
BladeUser bladeUser // 原有用户参数,保持业务一致性
) throws JsonProcessingException {
// 1. 从工厂类获取对应的策略类实例
INodeAuditHandler nodeHandler = handlerFactory.getHandler(handlerName);
// 2. 执行节点校验逻辑(与原有业务逻辑完全一致)
R validateResult = nodeHandler.validate(bladeUser, params);
if (validateResult != null) { // 校验失败,直接返回结果
return validateResult;
}
// 3. 校验通过,执行节点核心业务逻辑
return nodeHandler.execute(bladeUser, params);
}
}
三、方案实现流程(请求全过程,举2个核心例子)
以「进件第一步」和「保银初审」为例,从前端请求 到后端返回结果,一步一步拆解执行流程,清晰看到策略模式+工厂模式的联动逻辑。
例子1:调用「进件第一步」审核节点
1. 前端/Postman发起请求
- 请求URL:
POST http://你的域名/api/node/audit/applyInformationFirst(核心:路径最后拼接节点标识applyInformationFirst,与策略类@Component注解值一致) - 请求体:传入
InformationDTO类型的JSON参数(与你原有调用/applyInformationFirst的入参完全一致)
json
{
"createUser": "业务员007",
"name": "李四",
"cardNo": "310101199505056789",
"phoneNumber": "13900139000",
"imageInformationChildVO": {
"image2_1": ["https://xxx.com/idcard/front.png", "https://xxx.com/idcard/back.png"]
}
}
- 请求头:携带登录态、Content-Type等原有参数,无需额外添加。
2. 后端执行流程
- 通用接口接收请求 :
CommonNodeAuditController的audit方法接收请求,通过@PathVariable从URL中解析出handlerName=applyInformationFirst,请求体的JSON参数被封装为Object params,BladeUser由Spring自动注入(如拦截器解析登录态); - 工厂类获取策略类 :调用
handlerFactory.getHandler("applyInformationFirst"),工厂类从auditHandlerMap中根据KeyapplyInformationFirst,取出对应的Value------ApplyInformationFirst策略类实例; - 执行策略类校验逻辑 :调用
ApplyInformationFirst.validate(bladeUser, params),将BladeUser和InformationDTO参数传入,执行专属校验(业务员、姓名、身份证号、身份证正反面等非空校验),校验通过返回null; - 执行策略类业务逻辑 :调用
ApplyInformationFirst.execute(bladeUser, params),执行进件第一步核心业务------调用informationService.applyInfomationFirst保存进件基础信息; - 返回结果 :将业务执行结果封装为
R对象,返回给前端,流程结束。
例子2:调用「保银初审」审核节点
1. 前端/Postman发起请求
- 请求URL:
POST http://你的域名/api/node/audit/bankInsuranceBeginAudit(核心:路径最后拼接节点标识bankInsuranceBeginAudit) - 请求体:传入
NodeDTO类型的JSON参数(与你原有调用/bankInsuranceBeginAudit的入参完全一致)
json
{
"informationId": 888888,
"operatorType": "RESOLVE",
"remark": "资料齐全,符合初审要求,同意通过",
"nodeKey": "BANK_INSURANCE_BEGIN_AUDIT"
}
2. 后端执行流程
- 通用接口接收请求 :
CommonNodeAuditController的audit方法接收请求,解析出handlerName=bankInsuranceBeginAudit,请求体封装为Object params,注入BladeUser; - 工厂类获取策略类 :调用
handlerFactory.getHandler("bankInsuranceBeginAudit"),工厂类从Map中取出BankInsuranceBeginAudit策略类实例; - 执行策略类校验逻辑 :调用
BankInsuranceBeginAudit.validate(bladeUser, params),校验操作类型(仅支持通过/拒单)、审核意见非空,校验通过返回null; - 执行策略类业务逻辑 :调用
BankInsuranceBeginAudit.execute(bladeUser, params),执行保银初审核心业务------设置节点名称/key、查询临时进件材料、同步材料/删除临时材料、调用审批流处理方法informationFlowableService.doReview; - 返回结果 :将审批流处理结果封装为
R对象,返回给前端,流程结束。
核心共性
两个节点的请求流程完全一致 ,唯一区别是工厂类根据不同的handlerName取出了不同的策略类实例,实现了「单接口适配多节点」的核心目标,且所有原有业务逻辑均未修改,保证了业务一致性。
四、重点、难点、亮点解析(面试高频考点,体现专业性)
1. 重点(核心实现要点,必须掌握)
- 策略类的Bean名称唯一性 :所有实现
INodeAuditHandler的策略类,@Component注解必须指定唯一的Bean名称 ,且该名称作为「节点标识」,是工厂类匹配的核心依据,必须与URL中的handlerName完全一致; - Spring自动注入Map的规则 :
@Autowired private Map<String, INodeAuditHandler> auditHandlerMap是核心,Spring容器启动时会自动扫描所有实现INodeAuditHandler的@Component类,以Bean名称为Key,实例为Value注入Map,无需手动维护; - 通用入参的兼容性 :通用接口使用
Object params接收请求体,配合策略接口的Object... params可变参数,完美兼容不同节点的入参DTO(如InformationDTO、NodeDTO),且策略类中已做强制类型转换,无需修改原有逻辑; - 核心流程的复用:所有节点统一复用「工厂获取策略类→校验→执行」的流程,保证了业务逻辑的标准化,避免重复编写相同流程代码。
2. 难点(实现过程中易踩坑点,避坑关键)
- NoUniqueBeanDefinitionException异常 :若策略类未指定
@Component的Bean名称,Spring会默认以「类名首字母小写」为Bean名称,若出现名称冲突(如类名相似),会抛出该异常------解决方式:为所有策略类显式指定唯一的Bean名称; - 策略类未注入Map的问题 :若工厂类的
auditHandlerMap为null或无对应策略类,大概率是策略类未标注@Component(Spring未扫描)、策略类未实现INodeAuditHandler接口、Bean名称拼写错误------解决方式 :检查策略类的注解和接口实现,保证Bean名称与URL中的handlerName完全一致; - 类型转换异常 :若前端传入的入参DTO与策略类中强制转换的类型不一致,会抛出
ClassCastException------解决方式 :前端需保证传入的入参与节点匹配(如进件第一步传InformationDTO),后端可在策略类中增加类型校验,提升健壮性; - 事务失效问题 :若策略类的
execute方法标注了@Transactional(如BankInsuranceBeginAudit),需保证策略类是Spring管理的Bean(已标注@Component),且事务注解的传播行为、隔离级别符合业务要求,避免事务失效。
3. 亮点(设计优势,面试中突出差异化,体现专业性)
- 架构解耦性强 :采用「接口层-工厂层-策略层」三层架构,各司其职:
- 接口层:仅负责接收请求、返回结果,不涉及任何业务逻辑;
- 工厂层:仅负责策略类的获取,不涉及业务执行;
- 策略层:仅负责实现专属业务逻辑,不涉及请求处理和对象创建;
层与层之间通过接口和标识交互,无硬编码依赖,符合「单一职责原则」。
- 极致的可扩展性 :新增审核节点时,仅需新增一个策略类 (实现
INodeAuditHandler,标注@Component("唯一标识")),无需修改工厂类、通用接口等任何原有代码,完全符合「开闭原则」,适配业务快速迭代; - Spring原生能力深度利用:未引入任何第三方框架,仅通过Spring的「依赖注入、Bean管理、自动扫描」等原生能力实现动态调度,降低了项目的依赖复杂度,且易于团队维护;
- 异常处理的标准化 :工厂类中对「未找到策略类」的场景抛出明确的
ServiceException,配合项目全局异常处理器,可返回统一的错误格式,便于前端处理和问题排查,提升了系统的健壮性; - 接口设计的RESTful规范 :通过URL路径(
@PathVariable)传递节点标识,而非请求参数,符合RESTful接口设计规范,URL语义清晰(一眼可看出调用的是哪个审核节点),提升了接口的可读性和可维护性。
五、优化点(后续迭代方向,面试中体现思考深度)
基于当前方案,可根据业务复杂度和实际需求,做以下优化,让架构更健壮、功能更完善,面试中聊这些点,能体现出你对架构的深度思考:
- 增加策略类缓存(性能优化):若系统中审核节点数量多、调用频繁,可在工厂类中增加本地缓存(如Guava Cache、Caffeine),缓存策略类实例,避免多次从Spring Map中获取,提升性能(当前Spring Map是内存级,性能已较好,适合高并发场景优化);
- 节点标识做枚举管理(避免硬编码) :将所有节点标识(如
applyInformationFirst、bankInsuranceBeginAudit)定义为枚举类(如AuditNodeEnum),前端调用时传入枚举值,后端工厂类和接口层使用枚举类获取标识,避免硬编码带来的拼写错误,提升代码可维护性; - 增加入参类型校验(健壮性优化) :在策略类的
validate方法中,增加入参类型的校验(如if (!(params[0] instanceof InformationDTO)) { throw new ServiceException("入参类型错误"); }),避免前端传入错误的入参类型导致类型转换异常; - 支持节点优先级和流程终止 :在策略类中增加
getPriority()方法,定义节点执行优先级,若高优先级节点校验失败/执行异常,可直接终止后续节点执行,适配「多节点串联审核」的业务场景; - 增加策略类执行监控(可观测性优化) :通过AOP切面围绕
INodeAuditHandler的validate和execute方法,记录每个节点的执行耗时、调用次数、成功次数、失败次数、异常次数,对接监控平台(如Prometheus+Grafana),便于后续性能优化和问题排查; - 支持策略类组合(功能扩展) :新增「组合策略类」,实现
INodeAuditHandler接口,内部封装多个基础策略类,支持「与/或」逻辑(如多个节点同时校验通过才允许执行下一步),适配复杂的审核业务场景; - 增加重试机制(容错性优化):对非致命异常的节点执行(如第三方接口调用超时、数据库临时连接失败),增加重试机制(如基于Spring Retry),提升系统的容错性和稳定性;
- 接口版本控制(兼容升级) :若后续业务需要对节点逻辑做兼容升级,可在通用接口中增加版本控制(如
/api/v1/node/audit/{handlerName}、/api/v2/node/audit/{handlerName}),避免版本升级影响原有业务。
六、面试话术参考(直接用,体现专业性)
当面试官问到「你在项目中是如何实现多业务场景的单接口适配?」「谈谈你对策略模式+工厂模式的理解和实践?」时,可按以下话术回答,逻辑清晰、结合业务、突出亮点:
在我们的进件审核项目中,存在多个独立的审核节点(如进件第一步、保银初审、GPS安装审核等),原有方案是一个节点对应一个接口,存在代码冗余、可扩展性差的问题。因此我采用了策略模式+工厂模式的组合方案,基于Spring原生能力实现了「单接口适配多审核节点」的需求。
首先,定义了
INodeAuditHandler作为策略接口 ,规范了所有审核节点的validate和execute方法;然后将每个审核节点的业务逻辑封装为独立的策略类 ,实现该接口并通过@Component标注唯一的Bean名称,保证逻辑的解耦和独立;接着开发了NodeAuditHandlerFactory工厂类 ,利用Spring自动注入的Map管理所有策略类,提供按节点标识获取策略类的方法,封装了对象创建逻辑;最后写了一个通用接口,从URL中获取节点标识,通过工厂类动态获取策略类,统一执行「校验→执行」的流程。这套方案的核心优势是符合开闭原则 ,新增审核节点仅需新增策略类,无需修改原有代码;同时架构解耦性强,接口层、工厂层、策略层各司其职,便于团队维护;另外,完全基于Spring原生能力实现,无第三方依赖,降低了项目复杂度。
在实现过程中,需要注意策略类Bean名称的唯一性、Spring Map的自动注入规则,避免出现Bean冲突和注入失败的问题;后续还可以通过枚举管理节点标识、AOP实现执行监控、增加重试机制等方式,让架构更健壮、功能更完善。
以上内容从设计模式定义、代码实现、请求流程、面试考点四个维度做了全方面解析,你可以直接基于这份内容落地代码,也能快速掌握面试中关于该方案的核心考点,做到全方面理解这种写法。