1, Background
At present, there is an application service because it involves product demand change. After completing the technical scheme, I looked at the code that needs to be changed and found that there is a service and implementation class. The implementation class provides three element verification logic for different scenarios. The overall verification logic is to verify the business rules first. If the verification fails, Then the package returns the business verification information; If the verification is passed, the database will be dropped and the current identity authentication information will be stored.
However, based on the current product requirements, it is nothing more than adding a member method to the service, then implementing the method by the class, copying the business logic of other member methods, making a slight transformation, and then implementing the product requirements business logic. However, looking at the current service class, I see that many member methods have similar business logic. I can't bear to make do with it, and then I think about how to better reconstruct the existing code.
2, Analysis
Let's take a look at the original code we want to refactor
1,CustomerRelationService
Interface code
public interface CustomerRelationService { OpenIdRelation getBindedRelationByOpenId(String openId); /** * XXX Binding information * @param dto * @return */ RespDTO bindCustomer(BindOpenIdRequestDTO bindOpenIdRequestDTO); /** * XXX Binding information * @param dto * @return */ RespDTO releaseBindCustomer(BindOpenIdRequestDTO dto); /** * XXX Binding information * @param dto * @return */ RespDTO refundBindOpenId(BindOpenIdRequestDTO dto); List<OpenIdRelation> queryRecordsByIdno(String idno); }
2,CustomerRelationServiceImpl
Corresponding implementation class:
@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 binding * @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("SMS verification code error exceeds%s Times, try again in 1 minute!",totalCount)); } String verifyCodeFromCache = msgCacheManager.getBindCacheValue(mobile); log.info("[The binding customer relationship obtains the verification code from the cache verifyCode = {}]", verifyCodeFromCache); if (verifyCodeFromCache == null || !Objects.equals(verifyCodeFromCache, verifyCode)) { log.error("[Binding customer relationship verification code matching failed openId={}]", openId); msgCacheManager.recordErrorCount(mobile); return RespDTO.fail("SMS verification code error, please fill in again"); } msgCacheManager.clearRecordErrorCount(mobile); List<MortgageDetailDTO> list = mortgageHttpApi.getMortgageDetailByIdNo(idNo); if (CollectionUtils.isEmpty(list)) { return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } 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("[The binding customer relationship submission information does not match the system information openId = {}]", openId); return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } OpenIdRelation isExist = getBindedRelationByOpenId(openId); if (isExist != null) { log.error("[Binding customer relationship user information bound 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(); } /** * Verify the verification code, and return if there is an error * @return */ private RespDTO verifyCode(BindOpenIdRequestDTO dto){ String cacheValue = msgCacheManager.getBindCacheValue(dto.getMobile()); return VerifyCodeUtil.verify(cacheValue, dto.getOpenId(), dto.getVerifyCode()); } /** * XX Binding information * @param dto * @return */ @Override public RespDTO releaseBindCustomer(BindOpenIdRequestDTO dto) { //1. Verify the verification code, and return if there is an error RespDTO resp = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == resp.getStatus()){ return resp; } //2. Query whether the customer information matches from the order center. If it does not exist, return 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 The submitted information does not match the system information openId = {}]", openId); return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } //3. Judge whether it has been bound, and return if (null != getBindedRelationByOpenId(openId)) { log.error("[XXXXX User information bound openId = {}]", openId); return RespDTO.success(); } //4. If not bound, bind bind(dto); return RespDTO.success(); } /** * XXX binding * @param dto * @return */ @Override public RespDTO refundBindOpenId(BindOpenIdRequestDTO dto) { //1. Verify the verification code, and return if there is an error RespDTO respDTO = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == respDTO.getStatus()){ return respDTO; } //2. Query whether the customer information matches from the order center. If it does not exist, return String openId = dto.getOpenId(); RespDTO<TransferRefundOrderRe> resp = mortgageHttpApi.queryRefundOrder(dto.getIdNo()); TransferRefundOrderRe refundVO = resp.getData(); if (Objects.isNull(refundVO)){ log.error("[The refund binding customer relationship submission information does not match the system information openId = {}]", openId); return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } //3. Judge whether it has been bound, and return if (null != getBindedRelationByOpenId(openId)) { log.error("[Refund binding customer relationship user information bound openId = {}]", openId); return RespDTO.success(); } //4. If not bound, bind bind(dto); return RespDTO.success(); } @Override public List<OpenIdRelation> queryRecordsByIdno(String idno) { List<OpenIdRelation> list = openIdRelationMapper.queryRecordsByIdno(idno); return list; } /** * Save binding data * @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() ); } } }
The above classes have three important methods as follows. The above implementation class is the business logic implementation based on different scenarios of the following three methods
RespDTO bindCustomer(BindOpenIdRequestDTO bindOpenIdRequestDTO); /** * @param dto * @return */ RespDTO releaseBindCustomer(BindOpenIdRequestDTO dto); /** * @param dto * @return */ RespDTO refundBindOpenId(BindOpenIdRequestDTO dto);
Generally speaking, the idea of refactoring is to define an abstract class through the template method, and then three different methods are three different implementation classes of the abstract class. The same code logic (i.e. pre business verification + binding) is abstracted in the abstract class to expose a public method.
Then, a principal-agent class is used to encapsulate different business scenario types through online text objects, and then the implementation class of the corresponding abstract class is entrusted to process business logic. In this way, if you add binding methods, you only need to add corresponding subclasses. When colleagues modify a scenario, you only need to modify the corresponding subclasses, which is in line with the opening and closing principle.
3, Reconstruction
1. UML design
Overall design idea
- By defining the abstract class AbstractBindHandler, the steps of relevant three element scenario verification are encapsulated. For subclasses, only the pre verification abstract method needs to be implemented.
- The BindContext context object is defined to encapsulate the request parameters and output that the whole process depends on.
- Define the enumeration class Biz, and then implement the Handler to add the response. You only need to add members in the enumeration class.
- The Handler is encapsulated by defining BindHandlerDispatcher. For external business calls, you only need to interact with BindHandlerDispatcher to shield the details of the Handler class.
- If you add a business scenario later, you only need to add a Handler class. If you modify the corresponding business scenario, you only need to find the corresponding Handler class and modify it, that is, it complies with the opening and closing principle.
2,BindContext
This class contains three common attributes, param output parameters. The parameters that the Handler class depends on are transmitted through param. This type is a generic type and requires the Handler class to define the declaration. Biz is an enumeration class.
/** * @description: Client binding context object * @Date : 2021/10/29 5:52 PM * @Author : Shi Dongdong Seig Heil */ @Data @Builder public class BindContext<P> { /** * DTO parameter */ private P param; /** * Business type */ private Biz biz; /** * response message */ private RespDTO respDTO; /** * Business type */ public enum Biz{ /** * XXX Payment customer binding */ ESC_PAYMENT_BIND, /** * XXX Refund customer binding */ ESC_REFUND_BIND, /** * XXX Debarred customer binding */ ESC_RELEASE_BIND, /** * XXX Debarred customer binding */ CRZ_RELEASE_BIND } }
3,AbstractBindHandler
The base class of the Handler class is still a generic abstract class. Subclasses are required to declare the input parameter Param and inherit BindOpenIdRequestDTO.
/** * @description: Abstract official account customer three factor binding processor * @Date : 2021/10/29 5:51 PM * @Author : Shi Dongdong Seig Heil */ public abstract class AbstractBindHandler<P extends BindOpenIdRequestDTO> { /** * Identification of the binding */ 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; /** * Pre check * @param context context * @return */ abstract RespDTO verify(BindContext<P> context); /** * External exposure method * @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()); } } /** * Save binding data * @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() ); } } /** * Query the bound relationship according to openId * @param openId * @return */ protected OpenIdRelation getBindedRelationByOpenId(String openId) { return customerRelationService.getBindedRelationByOpenId(openId); } /** * Verify the verification code, and return if there is an error * @return */ RespDTO verifyCode(BindOpenIdRequestDTO dto){ String cacheValue = msgCacheManager.getBindCacheValue(dto.getMobile()); return VerifyCodeUtil.verify(cacheValue, dto.getOpenId(), dto.getVerifyCode()); } }
Related implementation classes
CrzReleaseBindHandler
@Service @Slf4j public class CrzReleaseBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ @Autowired MortgageHttpApi mortgageHttpApi; @Override RespDTO verify(BindContext<BindOpenIdRequestDTO> context) { BindOpenIdRequestDTO dto = context.getParam(); //1. Verify the verification code, and return if there is an error RespDTO respDTO = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == respDTO.getStatus()){ return respDTO; } MockConfig mockConfig = diamondConfigProxy.mockConfig(); List<String> idnoMockList = mockConfig.getReleaseIdNoMockList(); //ID card white list ignores three elements verification boolean whiteIdno = null != idnoMockList && idnoMockList.contains(dto.getIdNo()); if(!whiteIdno){ //2. Identity verification 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. Judge whether it has been bound, and return String openId = dto.getOpenId(); if (null != getBindedRelationByOpenId(openId)) { log.error("[Refund binding customer relationship user information bound openId = {}]", openId); return RespDTO.success(); } return RespDTO.success(BIND_TAG); } }
EscPaymentBindHandler
/** * @description: Used car payment customer binding processor * @Date : 2021/10/29 6:01 PM * @Author : Shi Dongdong Seig Heil */ @Service @Slf4j public class EscPaymentBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ /** * Upper threshold limit of error times of SMS verification code */ 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("SMS verification code error exceeds%s Times, try again in 1 minute!",totalCount)); } String verifyCodeFromCache = msgCacheManager.getBindCacheValue(mobile); log.info("[The binding customer relationship obtains the verification code from the cache verifyCode = {}]", verifyCodeFromCache); if (verifyCodeFromCache == null || !Objects.equals(verifyCodeFromCache, verifyCode)) { log.error("[Binding customer relationship verification code matching failed openId={}]", openId); msgCacheManager.recordErrorCount(mobile); return RespDTO.fail("SMS verification code error, please fill in again"); } msgCacheManager.clearRecordErrorCount(mobile); List<MortgageDetailDTO> list = mortgageHttpApi.getMortgageDetailByIdNo(idNo); if (CollectionUtils.isEmpty(list)) { return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } 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("[The binding customer relationship submission information does not match the system information openId = {}]", openId); return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } OpenIdRelation isExist = getBindedRelationByOpenId(openId); if (isExist != null) { log.error("[Binding customer relationship user information bound 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: Used car refund customer binding processor * @Date : 2021/10/29 6:01 PM * @Author : Shi Dongdong Seig Heil */ @Service @Slf4j public class EscRefundBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ @Override RespDTO verify(BindContext<BindOpenIdRequestDTO> context) { BindOpenIdRequestDTO dto = context.getParam(); //1. Verify the verification code, and return if there is an error RespDTO respDTO = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == respDTO.getStatus()){ return respDTO; } //2. Query whether the customer information matches from the order center. If it does not exist, return String openId = dto.getOpenId(); RespDTO<TransferRefundOrderRe> resp = mortgageHttpApi.queryRefundOrder(dto.getIdNo()); TransferRefundOrderRe refundVO = resp.getData(); if (Objects.isNull(refundVO)){ log.error("[The refund binding customer relationship submission information does not match the system information openId = {}]", openId); return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } //3. Judge whether it has been bound, and return if (null != getBindedRelationByOpenId(openId)) { log.error("[Refund binding customer relationship user information bound openId = {}]", openId); return RespDTO.success(); } return RespDTO.success(BIND_TAG); } }
EscReleaseBindHandler
/** * @description: Used car release customer binding processor * @Date : 2021/10/29 6:01 PM * @Author : Shi Dongdong Seig Heil */ @Service @Slf4j public class EscReleaseBindHandler extends AbstractBindHandler<BindOpenIdRequestDTO>{ @Override RespDTO verify(BindContext<BindOpenIdRequestDTO> context) { BindOpenIdRequestDTO dto = context.getParam(); //1. Verify the verification code, and return if there is an error RespDTO resp = verifyCode(dto); if (RespStatusEnum.FAIL.getStatus() == resp.getStatus()){ return resp; } //2. Query whether the customer information matches from the order center. If it does not exist, return 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("[The submitted information of the customer relationship for de pledge binding does not match the system information openId = {}]", openId); return RespDTO.fail("Your identity information was not found. Please re submit it after confirmation"); } //3. Judge whether it has been bound, and return if (null != getBindedRelationByOpenId(openId)) { log.error("[The customer relationship user information has been bound openId = {}]", openId); return RespDTO.success(); } return RespDTO.success(BIND_TAG); } }
4,BindHandlerDispatcher
The responsibility of this class is equivalent to the coordinator of the Handler. It commands the corresponding Handler class according to the type biz of the context
/** * @description: Customer relationship binding processing distributor * @Date : 2021/10/29 6:19 PM * @Author : Shi Dongdong Seig Heil */ @Component public class BindHandlerDispatcher { @Resource EscPaymentBindHandler escPaymentBindHandler; @Resource EscReleaseBindHandler escReleaseBindHandler; @Resource EscRefundBindHandler escRefundBindHandler; @Resource CrzReleaseBindHandler crzReleaseBindHandler; /** * Distribute the corresponding processor according to the service type for processing * @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
After refactoring, the member method here only needs to construct the BindContext instance object and then call the member method of Dispatcher.dispatch. The code at this time looks refreshing.
/** * XX binding * @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 Binding information * @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 binding * @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 binding * @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(); }
4, Summary
Through the comparison before and after reconstruction, we find that the code of the whole service class (CustomerRelationServiceImpl) is encapsulated by the Handler class, while the service class only needs to delegate the whole Dispatcher class, construct the BindContext instance object, and then call its dispatch method.
The external methods provided by the whole service have not been changed. If you add member methods later, you only need to add the Handler class.