Framework design evolution for channel services

preface

As we all know, and the tripartite system interaction, often because of the tripartite interface design of our system caused certain intrusion. This intrusion refers to the system incompatibility caused by the upgrade of the three-party interface/improper design of the three-party interface. In this case, the system gradually evolves into a patching mode. As the number of patches increased, much of the original design was obscured, and the code was so full of If and else that it turned out to be difficult to maintain. A very simple logic was hidden in the judgment, because the details of the patch were obscured. Therefore, the design of channel service with high scalability is the key to consider. In order to illustrate the evolution process of this system, I plan to step by step from the first version, and finally give a design that I think is reasonable. Of course, this design is not perfect, I hope readers can come to their own thinking in the process of evolution. This article will be in the form of a story, the following is the body (pure fiction).

V1 Simple Factory + Policy mode

May 20 that day the weather is sunny, small hope to the company found that the receptionist sister to him a lot of gentle, think of usually little sister to their own are love. Single for many years small hope heart joy opened flower, there is a moment of the child surname who want to. Come back to god, with just finished eating fried dough sticks hand touched “I became bald, but not strong” head, heart there is so a little dark. Unfortunately, Xiaoxi was a very traditional person, and she couldn’t help wondering: “Something important must have happened today.” Then he walked to the office lobby on the second floor. “Well, the early bird gets the worm!” Lao Wang said with a smile, “Recently our company is preparing to make a payment system, you are responsible for the connection of the three-way channel part, ok?” . “How can a man say no?” Small hope hurriedly agreed to come down, turn to think “I have never made, install X for a while cool, calculate hard make, originally this is today’s big event, god arranged as expected the biggest!” . Happy time is always so short, usually silent love paddling reading novels small xi gradually become irritable. “CIAO, what ghost, what SHIT, this thing write like SHIT” from his station. Take a closer look, the original small xi is traveling in wechat and pay the official website documents can not extricate themselves. “Fierce small hope, all whole top pay” eldest brother light say. “Ah, not too good, wechat pay to write the general, I redesign absolutely better than him one hundred times” blowing cattle, small xi and buried in the official website to go. After three days and three nights of continuous hard work, small hope seems to have known the knack of channel system design. “Well, it’s just a simple factory with an interface service. What’s so hard! I look down on the best boss in the world “. Then, a design draft is drawn.


After looking at it for a long time, he said, “This is a wonderful design. All the channel interfaces are encapsulated in one class, so that different channels only need to implement this interface. The channel service is obtained by passing in the channel number through the factory method and then invoking the specified method. By the way, is this factory method plus strategy mode? High, very high!” . “Of course, I’m a hot guy who knows design patterns. Why not?” Small hope light say to eldest brother, but in fact heart excitement already can’t suppress, finally catch an opportunity to let me dazzle a wave of skill. “But I have a question, the foremost NetPayServiceImpl and JsPayServiceImpl is in front of the entrance to this I can understand that, but ChannerlCenterServiceImpl do?” “Asked eldest brother modestly. “Well, the unified entrance to all the channels, you can do a lot of things in it. I’m just logging in it right now, but it’s for future expansion, you know what I mean?” . Listen, the eldest brother nodded and carried his medlar tea quietly back to his station, as expected I was too vegetables ah, the heart murmural way.

V2 Adapter mode

Time flies like a song. Unexpectedly, the business development is getting better and better, and this payment system has become the core project of the company. Lao Wang has the idea of job-hopping with Xiao Xi to set up a payment company alone. With the increasing number of three-way channels, the methods in IChannelService have inevitably ballooned, and Hickey has gradually become aware of this problem. Let’s take a look at the interface we envisioned at the beginning of the design:

public interface IChannelService {
    // Online bank payment
    ResultNetDTO netPay(NetPayDTO netpayDto);
    // Get the qr code
    ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto);
 // Order query  ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto); } Copy the code

Yes, according to the previous expectation, we will not have too many payment methods, but thousands of thousands of calculations unexpectedly, now there are more than ten three channels, each of the payment methods are different. Especially quick payment, PM can not distinguish the difference between different channels quick payment, simply called 1,2,3. Although she was reluctant to do so, she wrote this code:

// Don't scold me, that's the name of the product
ResultQuickPayB2cChannelMsgDTO quickPay1(QuickPay1DTO payDto);
ResultQuickPayB2cChannelMsgDTO quickPay2(QuickPay2DTO payDto);
ResultQuickPayB2cChannelMsgDTO quickPay3(QuickPay3DTO payDto);
Copy the code

That’s not the worst part. A bad name doesn’t matter. The key is that wechat and Alipay before, there is no such quick payment, but it is unreasonable to ask me to implement these methods. Let’s look at what wechat looks like now:

public class WechatChannelServiceImpl implements IChannelService{

    @Override
    public ResultNetDTO netPay(NetPayDTO netpayDto){
        // omit implementation....
 };   @Override  public ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto){  // omit implementation....  };   @Override  public ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto){  // omit implementation....  };   @Override  public ResultQuickPayB2cChannelMsgDTO quickPay1(QuickPay1DTO payDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  };   @Override  public ResultQuickPayB2cChannelMsgDTO quickPay2(QuickPay2DTO payDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  };   @Override  public ResultQuickPayB2cChannelMsgDTO quickPay3(QuickPay3DTO payDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  }; } Copy the code

How to do this, smart small hope suddenly thought of adapter mode, or use JDK8 default syntax.

public class ChannelServiceAdapter implements IChannelService{

    @Override
    public ResultNetDTO netPay(NetPayDTO netpayDto){
       throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
 };   @Override  public ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  };   @Override  public ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  };   @Override  public ResultQuickPayB2cChannelMsgDTO quickPay1(QuickPay1DTO payDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  };   @Override  public ResultQuickPayB2cChannelMsgDTO quickPay2(QuickPay2DTO payDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  };   @Override  public ResultQuickPayB2cChannelMsgDTO quickPay3(QuickPay3DTO payDto){  throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);  }; } Copy the code

Then let the channel inherit the class and override the method it needs to implement:

public class WechatChannelServiceImpl extends ChannelServiceAdapter{

    @Override
    public ResultNetDTO netPay(NetPayDTO netpayDto){
        // omit implementation....
 };   @Override  public ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto){  // omit implementation....  };   @Override  public ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto){  // omit implementation....  }; } Copy the code

Is that ok? It temporarily glots over the nasty aspects of subclasses forcing superclass methods to be implemented without actually solving the problem.

V3 service plug-in + service closure + generic design

Move forward. Knowing the drawbacks of lumping all payment methods together in one excuse, the way to improve is clear, in one word. How do I do it? Small xi heart again made a whisper.

  1. Do you need to divide services into groups? For example, wechat related to get together, such as wechat group contains H5, WAP, scan code, query, etc. If so, a channel may implement multiple groups. Theoretically yes, but there are group problems, and changes to H5 may affect scanning because the code is in a class.
  1. I’m just going to go one class per method, so I’m going to try to make it so that the program can call this method directly. Design a top-level interface, use generic input parameter generic return, use factory method to select specific implementation?

After consideration, Heather chose the second option. Let’s take a look at his code implementation: First, the top-level interface design:

/ * *Abstract channel service interface* /
public interface IChannelService<T extends AbstractReqModel.R extends AbstractRspModel> {

 R invoke(T request) throws ChannelServiceException; } Copy the code

AbstractReqModel and AbstractRspModel are nothing to write about, except for the subclass that defines entry and exit parameters and has some common variables in it. With this top-level interface, the key becomes how to get the different IChannelService implementations. Note that the implementation is different here. Let’s look at a few implementation classes.

// Wechat scan code
public class WechatScanCode implements IChannelService<PaymentDTO.PaymentResultDTO> {

    @Override
    public PaymentResultDTO invoke(PaymentDTO request) throws ChannelServiceException {
 // Omit the implementation  return null;  } }  // Wechat mobile website payment public class WechatMobileH5 implements IChannelService<PaymentDTO.PaymentResultDTO> {   @Override  public PaymentResultDTO invoke(PaymentDTO request) throws ChannelServiceException {  // Omit the implementation  return null;  } }  // Alipay APP payment public class AlipayApp implements IChannelService<PaymentDTO.PaymentResultDTO> {   @Override  public PaymentResultDTO invoke(PaymentDTO request) throws ChannelServiceException {  // Omit the implementation  return null;  } }  // Alipay payment result query public class AlipayPayQuery implements IChannelService<PayQueryDTO.PayQueryResultDTO> {   @Override  public PayQueryResultDTO invoke(PayQueryDTO request) throws ChannelServiceException {  // Omit the implementation  return null;  } } Copy the code

OK, I believe that readers have a clear understanding of xiaoxi’s design now. That is, each class that needs to interact with three parties is created to implement, and each class has a different input and output parameter. Some children may ask: “Xiao Xi, will this kind of explosion, it is also a trouble to write? . Yes, there will be more classes, but if you think about it, the chances that each interface will not be affected by bugs will be greatly reduced, and it will be easier to find. A comprehensive choice, I think it is worth it! With this top-level interface, we can also use factory methods to identify specific service implementation classes, as in the previous design. The only difference is that the previous factory method had only one input parameter: the channel number. This time we need to add a new partner:

public enum ServiceIdEnum {

    // Scan to pay
    SCAN_CODE("scan_code"),
    / / APP
 APP("app"),  // Pay by payment code  BRUSH_CARD("brush_card"),  // Public account payment  GZ("gz"),  // Applet payment  MINI_PROGRAM("mini_program"),  // Mobile website payment  MOBILE_H5("mobile_h5"),  // Computer website payment  PC_WEB("pc_web"),   // Payment query  PAY_QUERY("pay_query"),  // Refund enquiry  REFUND_QUERY("refund_query"),  // Payment notification  PAY_NOTIFY("pay_notify"),   // omit the follow-up Copy the code

The main purpose of this enumeration is to determine which class is called (forget about the class, it’s just implementation needs).

The factory method finds the corresponding class by channel and service ID:

public interface IChannelServiceFactory {

    / * ** The channel service is obtained by channel and service category. Null is returned if the channel service is not obtained* /
 IChannelService getChannelService(ChannelEnum channel, ServiceIdEnum serviceId); } Copy the code

In this way, we can call:

@Component
@Slf4j
public class ServiceDispatcher {

    @Autowired
 private IChannelServiceFactory channelServiceFactory;   private IChannelLifeCycleListener lifeCycleListener = new IChannelLifeCycleListener.Adapter();   public <R extends AbstractRspModel> R doDispatch(AbstractReqModel reqModel) throws ChannelServiceException {  final ChannelEnum channel = reqModel.getChannel();  final ServiceIdEnum serviceId = reqModel.getServiceId();  IChannelService channelService = channelServiceFactory.getChannelService(channel, serviceId);   if (channelService == null) {  log.error(Failed to obtain channel service: channel={}, serviceId={}", JSON.toJSONString(channel), JSON.toJSONString(serviceId));  throw new ChannelServiceException(ReturnCodeEnum.ERROR, "Obtaining channel service failed");  }  log.info("Obtaining channel service succeeded. ChannelService = {}, serviceId = {}", channelService.getClass().getSimpleName(),  JSON.toJSONString(serviceId));    lifeCycleListener.beforeRequest(reqModel);   StopWatch watch = StopWatch.createStarted();  R rspModel = null;  try {  rspModel = (R) channelService.invoke(reqModel);  } catch (ChannelServiceException e) {  lifeCycleListener.exceptionCaught(e);  throw e;  }  watch.stop();   lifeCycleListener.afterRequest(reqModel, rspModel);   log.info("The channel service was successfully invoked in {} (ms). ReqModel = {}, rspModel = {}". watch.getTime(TimeUnit.MILLISECONDS),  JSON.toJSONString(reqModel),  JSON.toJSONString(rspModel));   return rspModel;  }  } Copy the code

As can be seen, these parameters are all passed by the upstream caller, in addition, here added a listener, small hope code is not finished, is reserved for the future statistics service QOS and so on. Many children may ask, how is the specific channel service implementation done? Those of you familiar with Spring probably know that the Spring container already has the ability to scan packages, and dynamically registering beans makes this easy. All you need to do is make a custom annotation and set an alias during automatic scan registration to retrieve the Bean later. However, the disadvantage of this method is that it is difficult to maintain, because each time you need to think about the class in your head, which is not friendly to new friends. So The most common way to do this is to configure it manually in XML and divide it by channel.


Channel-alipay. XML looks like this:


So the new partner can also quickly find the corresponding class, maintainability up to 9 9, think of here small xi for his wit to buy a cup of fat house happy water. A key point here is that save the mapping relationship between classes, you know we need to pass number, service ID to get the specific implementation class, the realization of the conventional is Map < ChannelEnum, Map < ServiceIdEnum, IChannelService > >. Is there an elegant data structure that directly encapsulates this relationship? The answer is Yes. It’s time to bring out our big brother Guava and get straight to the code:

// Channel, service category, implementation class
privateTable<ChannelEnum, ServiceIdEnum, Class<? >> services = Tables.newCustomTable(new ConcurrentHashMap<>(), ConcurrentHashMap::new);
Copy the code

This is an Excel like data structure, called a triple, provided in Guava. Bits 1, 2, and 3 represent rows, columns, and values respectively. I think that makes sense. With this implementation class, it’s not a matter of minutes to get it into Spring.

public void afterPropertiesSet(a) {
    BeanDefinitionRegistry beanRegistry = (BeanDefinitionRegistry) applicationContext;
    channelServicePlugin.getChannels().forEach((ChannelEnum channel) -> {  // Traverses the service plug-in, which parses XML, provides channels, service categories, and implements class mappings
channelServicePlugin.getServiceMap(channel).forEach((ServiceIdEnum serviceId, Class<? > service) -> {            String serviceName = channel.name() + "_" + service.getSimpleName();
 BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.genericBeanDefinition(service);  beanRegistry.registerBeanDefinition(serviceName, beanBuilder.getBeanDefinition()); // Register the corresponding implementation class in the container  channelTransServiceTable.put(channel, serviceId, (IChannelService) applicationContext.getBean(serviceName)); // Add the newly registered Bean instance to the triplet  });  }); } Copy the code

In this way, all the design is complete. Xiao Xi is happy to look at these designs, a satisfied smile on the corners of her mouth.

After the language

The service design of the payment channel also needs to consider the request parameters and file fetching of each channel, which is not discussed in this article. These parameter fetching should also be done independently by the service and should not be passed by the upper level caller. For example, the version of some tripartite interfaces could not be clearly explained in one sentence or two sentences, so the paper did not consider the interface design of Alipay V1 and Alipay V2, for example. In addition, this article is some summary of my design experience, it is inevitable that there are shortcomings, if there is unreasonable, inadequate, can be improved, but also hope to correct, looking forward to discussion, thank you.