基于模板方法+协调者委托对现有冗余代码的改造案例
一、背景
当前有一个应用服务,因为本次涉及到产品需求变更,当完成技术方案后,然后看了一下需要涉及变更的代码,发现有一个service及实现类,该实现类提供不同场景的三要素验证逻辑,总体验证逻辑就是先进行业务规则校验,如果检验不通过,则包装返回业务校验信息;如果校验通过,则进行数据库落库,存储当前身份认证信息。
但是基于当前产品需求,无非就是在service增加一个成员方法,然后实现类实现该方法,拷贝一下其他成员方法的业务逻辑,稍微改造一下,然后就实现了产品需求业务逻辑了。但是呢,看着当前这个service类,看到很多成员方法业务逻辑类似,不忍心将就一下,然后就想着怎么更好的思路重构一下现有的代码。
二、分析
我们先来看一下想要重构的代码原样
1、CustomerRelationService
接口代码
public interface CustomerRelationService { OpenIdRelation getBindedRelationByOpenId(String openId); /** * XXX绑定信息 * @param dto * @return */ RespDTO bindCustomer(BindOpenIdRequestDTO bindOpenIdRequestDTO); /** * XXX绑定信息 * @param dto * @return */ RespDTO releaseBindCustomer(BindOpenIdRequestDTO dto); /** * XXX绑定信息 * @param dto * @return */ RespDTO refundBindOpenId(BindOpenIdRequestDTO dto); List<OpenIdRelation> queryRecordsByIdno(String idno); } 复制代码
2、CustomerRelationServiceImpl
对应实现类:
@Service @Slf4j public class CustomerRelationServiceImpl implements CustomerRelationService { private static final String OPEN_ID = "openId"; private static final String STATUS = "status"; private static final int STATUS_BINDED = 0; static final int UPPER_LIMIT_ERROR_COUNT = 10; @Autowired private OpenIdRelationMapper openIdRelationMapper; @Autowired private CustomerMapper customerMapper; @Autowired private MortgageService mortgageService; @Autowired MsgCacheManager msgCacheManager; @Autowired OrderCenterHttpApi orderCenterHttpApi; @Autowired MortgageHttpApi mortgageHttpApi; @Override public OpenIdRelation getBindedRelationByOpenId(String openId) { Map<String, Object> param = Maps.newHashMap(); param.put(OPEN_ID, openId); param.put(STATUS, STATUS_BINDED); OpenIdRelation openIdRelation = openIdRelationMapper.selectByParam(param); return Optional.ofNullable(openIdRelation).orElse(null); } /** * XX绑定 * @param verifyCode * @param mobile * @param idNo * @param realName * @param openId * @return */ @Override public RespDTO bindCustomer(String verifyCode, String mobile, String idNo, String realName, String openId) { long totalCount = msgCacheManager.getRecordErrorCount(mobile); if(totalCount >= UPPER_LIMIT_ERROR_COUNT){ return RespDTO.fail(String.format("短信验证码错误超过%s次,1分钟后再次重试!",totalCount)); } String verifyCodeFromCache = msgCacheManager.getBindCacheValue(mobile); log.info("[绑定客户关系 从缓存中获取到到验证码为 verifyCode = {}]", verifyCodeFromCache); if (verifyCodeFromCache == null || !Objects.equals(verifyCodeFromCache, verifyCode)) { log.error("[绑定客户关系 验证码匹配失败 openId={}]", openId); msgCacheManager.recordErrorCount(mobile); return RespDTO.fail("短信验证码错误,请重新填写"); } msgCacheManager.clearRecordErrorCount(mobile); List<MortgageDetailDTO> list = mortgageHttpApi.getMortgageDetailByIdNo(idNo); if (CollectionUtils.isEmpty(list)) { return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } MortgageDetailDTO mortgageDetailDTO = list.stream() .filter(mortgageDetail-> Objects.equals(mortgageDetail.getApiCustomerVO().getCustomerMobile(), mobile) && Objects.equals(mortgageDetail.getApiCustomerVO().getCustomerName(), realName)).findFirst().orElse(null); if (ObjectUtils.isEmpty(mortgageDetailDTO)) { log.error("[绑定客户关系 提交信息与系统信息不匹配 openId = {}]", openId); return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } OpenIdRelation isExist = getBindedRelationByOpenId(openId); if (isExist != null) { log.error("[绑定客户关系 用户信息已绑定 openId = {}]", openId); return RespDTO.success(); } Customer customer = Customer.builder().idno(idNo).realName(realName).mobile(mobile).build(); long insertCount = customerMapper.insertSelective(customer); if (insertCount != 0) { OpenIdRelation openIdRelation = OpenIdRelation.builder().customerId(customer.getId()).openId(openId).idNo(idNo).status(OpenIdRelation.BIND_STATUS) .bindTime(new Date()).build(); openIdRelationMapper.insertSelective(openIdRelation); } return RespDTO.success(); } /** * 校验验证码,错误则返回 * @return */ private RespDTO verifyCode(BindOpenIdRequestDTO dto){ String cacheValue = msgCacheManager.getBindCacheValue(dto.getMobile()); return VerifyCodeUtil.verify(cacheValue, dto.getOpenId(), dto.getVerifyCode()); } /** * XX绑定信息 * @param dto * @return */ @Override public RespDTO releaseBindCustomer(BindOpenIdRequestDTO dto) { //1、校验验证码,错误则返回 RespDTO resp = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == resp.getStatus()){ return resp; } //2、从订单中心查询客户信息是否匹配,不存在则返回 String idNo = dto.getIdNo(); String openId = dto.getOpenId(); List<OrderDetailVO> list = orderCenterHttpApi.queryOrderInfoByIdNo(idNo); OrderDetailVO detailVO = list.stream().filter(o -> Objects.nonNull(o.getApplicantDetailVO()) && Objects.equals(dto.getRealName(), o.getApplicantDetailVO().getName()) && (Objects.equals(dto.getMobile(), o.getApplicantDetailVO().getMobile()) || Objects.equals(dto.getMobile(), o.getApplicantDetailVO().getMobile2()))).findAny().orElse(null); if (Objects.isNull(detailVO)){ log.error("[XXXXX 提交信息与系统信息不匹配 openId = {}]", openId); return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } //3、判断是否已经绑定,绑定则返回 if (null != getBindedRelationByOpenId(openId)) { log.error("[XXXXX 用户信息已绑定 openId = {}]", openId); return RespDTO.success(); } //4、未绑定,则进行绑定 bind(dto); return RespDTO.success(); } /** * XXX绑定 * @param dto * @return */ @Override public RespDTO refundBindOpenId(BindOpenIdRequestDTO dto) { //1、校验验证码,错误则返回 RespDTO respDTO = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == respDTO.getStatus()){ return respDTO; } //2、从订单中心查询客户信息是否匹配,不存在则返回 String openId = dto.getOpenId(); RespDTO<TransferRefundOrderRe> resp = mortgageHttpApi.queryRefundOrder(dto.getIdNo()); TransferRefundOrderRe refundVO = resp.getData(); if (Objects.isNull(refundVO)){ log.error("[退款绑定客户关系 提交信息与系统信息不匹配 openId = {}]", openId); return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } //3、判断是否已经绑定,绑定则返回 if (null != getBindedRelationByOpenId(openId)) { log.error("[退款绑定客户关系 用户信息已绑定 openId = {}]", openId); return RespDTO.success(); } //4、未绑定,则进行绑定 bind(dto); return RespDTO.success(); } @Override public List<OpenIdRelation> queryRecordsByIdno(String idno) { List<OpenIdRelation> list = openIdRelationMapper.queryRecordsByIdno(idno); return list; } /** * 保存绑定数据 * @param dto */ private void bind(BindOpenIdRequestDTO dto){ Customer customer = Customer.builder().idno(dto.getIdNo()).realName(dto.getRealName()).mobile(dto.getMobile()).build(); long insertCount = customerMapper.insertSelective(customer); if (insertCount != 0) { openIdRelationMapper.insertSelective( OpenIdRelation.builder() .customerId(customer.getId()).openId(dto.getOpenId()).idNo(dto.getIdNo()) .status(OpenIdRelation.BIND_STATUS).bindTime(new Date()).build() ); } } } 复制代码
上述类有三个重要的方法如下,上面实现类就是基于下面三种方法的不同场景的业务逻辑实现
RespDTO bindCustomer(BindOpenIdRequestDTO bindOpenIdRequestDTO); /** * @param dto * @return */ RespDTO releaseBindCustomer(BindOpenIdRequestDTO dto); /** * @param dto * @return */ RespDTO refundBindOpenId(BindOpenIdRequestDTO dto); 复制代码
总体而言,重构的思路就是通过模板方法定义一个抽象类,然后三个不同的方法就是抽象类的三个不同实现类,相同的代码逻辑(即前置业务校验+绑定)则抽象在抽象类中,对外暴露一个公共方法。
然后通过一个委托代理类,通过上线文对象来封装不同的业务场景类型,然后委托对应的抽象类的实现类来处理业务逻辑,这样以来,如果再增加绑定方法时,只需要增加相应的子类,同事修改某个场景时,也只需要修改对应的子类即可,这样符合开闭原则。
三、重构
1、UML设计
整体设计思路
通过定义抽象类
AbstractBindHandler
,把相关三要素场景校验的步骤封装起来,对于子类只需要实现前置校验抽象方法即可。通过定义
BindContext
上下文对象,封装整个流程所依赖的请求参数和输出。通过定义枚举类Biz,然后实现增加响应的
Handler
,只需要在枚举类增加成员即可。通过定义
BindHandlerDispatcher
,实现对Handler
的封装,对于外部业务调用只需要跟BindHandlerDispatcher
交互即可,屏蔽Handler
类的细节。后续如果增加业务场景,只需要增加
Handler
类即可,如果修改对应业务场景,只需要找到对应Handler
类修改即可,即符合开闭原则。
2、BindContext
该类包含三个常用属性,param输出参数,Handler类所依赖的参数通过param传输,该类型是个泛型,需要Handler类来定义声明,Biz是个枚举类。
/** * @description: 客户绑定上下文对象 * @Date : 2021/10/29 5:52 PM * @Author : 石冬冬-Seig Heil */ @Data @Builder public class BindContext<P> { /** * DTO参数 */ private P param; /** * 业务类型 */ private Biz biz; /** * 响应报文 */ private RespDTO respDTO; /** * 业务类型 */ public enum Biz{ /** * XXX付款客户绑定 */ ESC_PAYMENT_BIND, /** * XXX退款客户绑定 */ ESC_REFUND_BIND, /** * XXX解押客户绑定 */ ESC_RELEASE_BIND, /** * XXX解押客户绑定 */ CRZ_RELEASE_BIND } } 复制代码
3、AbstractBindHandler
Handler类的基类,依然是个泛型抽象类,需要子类来声明输入参数Param需要继承BindOpenIdRequestDTO。
/** * @description: 抽象公众号客户三要素绑定处理器 * @Date : 2021/10/29 5:51 PM * @Author : 石冬冬-Seig Heil */ public abstract class AbstractBindHandler<P extends BindOpenIdRequestDTO> { /** * 进行绑定的标识 */ final String BIND_TAG = "BIND"; @Autowired DiamondConfigProxy diamondConfigProxy; @Autowired OpenIdRelationMapper openIdRelationMapper; @Autowired CustomerMapper customerMapper; @Resource CustomerRelationService customerRelationService; @Autowired MortgageService mortgageService; @Autowired MsgCacheManager msgCacheManager; @Autowired OrderCenterHttpApi orderCenterHttpApi; @Autowired MortgageHttpApi mortgageHttpApi; /** * 前置校验 * @param context 上下文 * @return */ abstract RespDTO verify(BindContext<P> context); /** * 对外暴露方法 * @param context */ public void handle(BindContext<P> context){ RespDTO respDTO = verify(context); context.setRespDTO(respDTO); boolean canBind = respDTO.getStatus() == RespStatusEnum.SUCCESS.getStatus() && BIND_TAG.equals(respDTO.getData()); if(canBind){ bind(context.getParam()); context.setRespDTO(RespDTO.success()); } } /** * 保存绑定数据 * @param dto */ private void bind(BindOpenIdRequestDTO dto){ Customer customer = Customer.builder().idno(dto.getIdNo()).realName(dto.getRealName()).mobile(dto.getMobile()).build(); long insertCount = customerMapper.insertSelective(customer); if (insertCount != 0) { openIdRelationMapper.insertSelective( OpenIdRelation.builder() .customerId(customer.getId()).openId(dto.getOpenId()).idNo(dto.getIdNo()) .status(OpenIdRelation.BIND_STATUS).bindTime(new Date()).build() ); } } /** * 根据openId查询已经绑定的关系 * @param openId * @return */ protected OpenIdRelation getBindedRelationByOpenId(String openId) { return customerRelationService.getBindedRelationByOpenId(openId); } /** * 校验验证码,错误则返回 * @return */ RespDTO verifyCode(BindOpenIdRequestDTO dto){ String cacheValue = msgCacheManager.getBindCacheValue(dto.getMobile()); return VerifyCodeUtil.verify(cacheValue, dto.getOpenId(), dto.getVerifyCode()); } } 复制代码
相关实现类
CrzReleaseBindHandler
@Service @Slf4j public class CrzReleaseBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ @Autowired MortgageHttpApi mortgageHttpApi; @Override RespDTO verify(BindContext<BindOpenIdRequestDTO> context) { BindOpenIdRequestDTO dto = context.getParam(); //1、校验验证码,错误则返回 RespDTO respDTO = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == respDTO.getStatus()){ return respDTO; } MockConfig mockConfig = diamondConfigProxy.mockConfig(); List<String> idnoMockList = mockConfig.getReleaseIdNoMockList(); //身份证白名单 忽略三要素验证 boolean whiteIdno = null != idnoMockList && idnoMockList.contains(dto.getIdNo()); if(!whiteIdno){ //2、身份校验 CrzReleaseBindVerifyDTO bindVerifyDTO = CrzReleaseBindVerifyDTO.builder() .customerIdno(dto.getIdNo()).customerMobile(dto.getMobile()).customerName(dto.getRealName()) .build(); try { RespDTO<String> bindResult = mortgageHttpApi.bindVerify(bindVerifyDTO); } catch (InvokeException e) { log.error("[CrzReleaseBindHandler#verify]",e); return RespDTO.fail(e.getErrMsg()); } } //2、判断是否已经绑定,绑定则返回 String openId = dto.getOpenId(); if (null != getBindedRelationByOpenId(openId)) { log.error("[退款绑定客户关系 用户信息已绑定 openId = {}]", openId); return RespDTO.success(); } return RespDTO.success(BIND_TAG); } } 复制代码
EscPaymentBindHandler
/** * @description: 二手车付款 客户绑定 处理器 * @Date : 2021/10/29 6:01 PM * @Author : 石冬冬-Seig Heil */ @Service @Slf4j public class EscPaymentBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ /** * 短信验证码错误次数阈值上限 */ final int UPPER_LIMIT_ERROR_COUNT = 10; @Override RespDTO verify(BindContext<BindOpenIdRequestDTO> context) { BindOpenIdRequestDTO dto = context.getParam(); String verifyCode = dto.getVerifyCode(), mobile = dto.getMobile(), idNo = dto.getIdNo(), realName = dto.getRealName(), openId = dto.getOpenId(); long totalCount = msgCacheManager.getRecordErrorCount(mobile); if(totalCount >= UPPER_LIMIT_ERROR_COUNT){ return RespDTO.fail(String.format("短信验证码错误超过%s次,1分钟后再次重试!",totalCount)); } String verifyCodeFromCache = msgCacheManager.getBindCacheValue(mobile); log.info("[绑定客户关系 从缓存中获取到到验证码为 verifyCode = {}]", verifyCodeFromCache); if (verifyCodeFromCache == null || !Objects.equals(verifyCodeFromCache, verifyCode)) { log.error("[绑定客户关系 验证码匹配失败 openId={}]", openId); msgCacheManager.recordErrorCount(mobile); return RespDTO.fail("短信验证码错误,请重新填写"); } msgCacheManager.clearRecordErrorCount(mobile); List<MortgageDetailDTO> list = mortgageHttpApi.getMortgageDetailByIdNo(idNo); if (CollectionUtils.isEmpty(list)) { return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } MortgageDetailDTO mortgageDetailDTO = list.stream() .filter(mortgageDetail-> Objects.equals(mortgageDetail.getApiCustomerVO().getCustomerMobile(), mobile) && Objects.equals(mortgageDetail.getApiCustomerVO().getCustomerName(), realName)).findFirst().orElse(null); if (ObjectUtils.isEmpty(mortgageDetailDTO)) { log.error("[绑定客户关系 提交信息与系统信息不匹配 openId = {}]", openId); return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } OpenIdRelation isExist = getBindedRelationByOpenId(openId); if (isExist != null) { log.error("[绑定客户关系 用户信息已绑定 openId = {}]", openId); return RespDTO.success(); } Customer customer = Customer.builder().idno(idNo).realName(realName).mobile(mobile).build(); long insertCount = customerMapper.insertSelective(customer); if (insertCount != 0) { OpenIdRelation openIdRelation = OpenIdRelation.builder().customerId(customer.getId()).openId(openId).idNo(idNo).status(OpenIdRelation.BIND_STATUS) .bindTime(new Date()).build(); openIdRelationMapper.insertSelective(openIdRelation); } return RespDTO.success(BIND_TAG); } } 复制代码
EscRefundBindHandler
/** * @description: 二手车退款客户绑定 处理器 * @Date : 2021/10/29 6:01 PM * @Author : 石冬冬-Seig Heil */ @Service @Slf4j public class EscRefundBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ @Override RespDTO verify(BindContext<BindOpenIdRequestDTO> context) { BindOpenIdRequestDTO dto = context.getParam(); //1、校验验证码,错误则返回 RespDTO respDTO = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == respDTO.getStatus()){ return respDTO; } //2、从订单中心查询客户信息是否匹配,不存在则返回 String openId = dto.getOpenId(); RespDTO<TransferRefundOrderRe> resp = mortgageHttpApi.queryRefundOrder(dto.getIdNo()); TransferRefundOrderRe refundVO = resp.getData(); if (Objects.isNull(refundVO)){ log.error("[退款绑定客户关系 提交信息与系统信息不匹配 openId = {}]", openId); return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } //3、判断是否已经绑定,绑定则返回 if (null != getBindedRelationByOpenId(openId)) { log.error("[退款绑定客户关系 用户信息已绑定 openId = {}]", openId); return RespDTO.success(); } return RespDTO.success(BIND_TAG); } } 复制代码
EscReleaseBindHandler
/** * @description: 二手车解押客户绑定 处理器 * @Date : 2021/10/29 6:01 PM * @Author : 石冬冬-Seig Heil */ @Service @Slf4j public class EscReleaseBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ @Override RespDTO verify(BindContext<BindOpenIdRequestDTO> context) { BindOpenIdRequestDTO dto = context.getParam(); //1、校验验证码,错误则返回 RespDTO resp = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == resp.getStatus()){ return resp; } //2、从订单中心查询客户信息是否匹配,不存在则返回 String idNo = dto.getIdNo(); String openId = dto.getOpenId(); List<OrderDetailVO> list = orderCenterHttpApi.queryOrderInfoByIdNo(idNo); OrderDetailVO detailVO = list.stream().filter(o -> Objects.nonNull(o.getApplicantDetailVO()) && Objects.equals(dto.getRealName(), o.getApplicantDetailVO().getName()) && (Objects.equals(dto.getMobile(), o.getApplicantDetailVO().getMobile()) || Objects.equals(dto.getMobile(), o.getApplicantDetailVO().getMobile2()))).findAny().orElse(null); if (Objects.isNull(detailVO)){ log.error("[解押绑定客户关系 提交信息与系统信息不匹配 openId = {}]", openId); return RespDTO.fail("未查询到您的身份信息,请确认无误后重新提交"); } //3、判断是否已经绑定,绑定则返回 if (null != getBindedRelationByOpenId(openId)) { log.error("[解押绑定客户关系 用户信息已绑定 openId = {}]", openId); return RespDTO.success(); } return RespDTO.success(BIND_TAG); } } 复制代码
4、BindHandlerDispatcher
该类的职责相当于Handler的协调者,它根据context的类型biz,进而指挥对应的Handler类
/** * @description: 客户关系绑定处理分发器 * @Date : 2021/10/29 6:19 PM * @Author : 石冬冬-Seig Heil */ @Component public class BindHandlerDispatcher { @Resource EscPaymentBindHandler escPaymentBindHandler; @Resource EscReleaseBindHandler escReleaseBindHandler; @Resource EscRefundBindHandler escRefundBindHandler; @Resource CrzReleaseBindHandler crzReleaseBindHandler; /** * 根据业务类型分发对应的处理器来处理 * @param context */ public void dispatch(BindContext context){ switch (context.getBiz()){ case ESC_PAYMENT_BIND: escPaymentBindHandler.handle(context); return; case ESC_REFUND_BIND: escRefundBindHandler.handle(context); return; case ESC_RELEASE_BIND: escReleaseBindHandler.handle(context); return; case CRZ_RELEASE_BIND: crzReleaseBindHandler.handle(context); return; } } } 复制代码
5、CustomerRelationServiceImpl
重构后,这里的成员方法只需要构造BindContext实例对象,然后调用Dispatcher.dispatch的成员方法即可,代码这时候看起来清爽许多。
/** * XX绑定 * @param dto * @return */ @Override public RespDTO bindCustomer(BindOpenIdRequestDTO dto) { BindContext<BindOpenIdRequestDTO> context = BindContext.<BindOpenIdRequestDTO>builder().biz(BindContext.Biz.ESC_PAYMENT_BIND).param(dto).build(); bindHandlerDispatcher.dispatch(context); return context.getRespDTO(); } /** * XX绑定信息 * @param dto * @return */ @Override public RespDTO releaseBindCustomer(BindOpenIdRequestDTO dto) { BindContext<BindOpenIdRequestDTO> context = BindContext.<BindOpenIdRequestDTO>builder().biz(BindContext.Biz.ESC_RELEASE_BIND).param(dto).build(); bindHandlerDispatcher.dispatch(context); return context.getRespDTO(); } /** * XX绑定 * @param dto * @return */ @Override public RespDTO releaseBindCustomerForCrz(BindOpenIdRequestDTO dto) { BindContext<BindOpenIdRequestDTO> context = BindContext.<BindOpenIdRequestDTO>builder().biz(BindContext.Biz.CRZ_RELEASE_BIND).param(dto).build(); bindHandlerDispatcher.dispatch(context); return context.getRespDTO(); } /** * XX绑定 * @param dto * @return */ @Override public RespDTO refundBindOpenId(BindOpenIdRequestDTO dto) { BindContext<BindOpenIdRequestDTO> context = BindContext.<BindOpenIdRequestDTO>builder().biz(BindContext.Biz.ESC_REFUND_BIND).param(dto).build(); bindHandlerDispatcher.dispatch(context); return context.getRespDTO(); } 复制代码
四、总结
通过重构前后对比,我们发现整个Service类(CustomerRelationServiceImpl
)的的代码这时候通过Handler
类来封装了,而service类中仅仅只需要委托Dispatcher
整个类,通过构造BindContext实例对象,进而调用其dispatch
方法即可。
整个service对外提供的方法并没有做任何改变,后续如果增加成员方法,只需要增加Handler
类即可。
作者:秋夜无霜
链接:https://juejin.cn/post/7029582156309463048