Research on complex logic business layer governance

    • Model background
      • advantages
      • The problem
      • Analysis of the
    • Model development
        • Simple Version: Responsible Chain
        • Responsible Chain + ThreadLocal
        • Context + Processor + Executor + Handler
          • Model of
          • Model initialization
          • Model runtime
    • Model implementation
      • Model structure
      • The model code
        • BaseProcessor
        • ContextHolder
        • Processor
        • Handler
    • The model summary

Model background

In the traditional MVC three-tier architecture mode, the execution path of business request is generally external → Web → Service → DAO → database.

Typically, we handle log printing, interface method performance monitoring, global exception handling, parameter verification, and so on in the outermost view layer. Handle complex business logic at the business layer (Service); The persistence layer (DAO) handles add, delete, change and query operations that interact with the database.

advantages

The classic MVC three-tier architecture is the most common and reasonable architecture design in the development of most single applications. Responsibilities are clearly stripped of the processing of each module, so that the project can be controlled and configured engineering, reduce the coupling degree between modules, can quickly build applications and relatively flexible adjustment of engineering modules and codes.

The problem

In the three-tier architecture mode, it is a typical anaemic model, and it is not difficult to find a problem in coding practice: Once a complex logic business is encountered, the code of the business layer (Service) will expand exponentially. Finally, the business layer is created to encapsulate the business, but the business logic cannot be controlled and cleared. As a result, the code is difficult to organize and maintain, the risk of change will soar, and finally out of control.

// Bad code practice 1: Spoon-feeding declarative and implementation interfaces, not suitable for complex logic, process-oriented, ├─Interface │ Aservice.java │ ├─ Implement │ Aservice.java │ ├─Interface │ Aservice.java │ ├─ Implement │ Aservice.java │ BServiceImpl. Java │ CServiceImpl. JavaCopy the code

Extensive project experience is that if there is no demand for business from the beginning has very good prospective design, just cramming for interface declaration, interface implementation, not to disassemble and the way of governance, behind the parsing logic code later in the process is very painful, maintain code time costs, the understanding of the past messy code logic cost need to pay too much.

Private void method(){// all logics // omit hundreds of lines here}Copy the code

When code is completely organized for business processes, it becomes a tangle of wires. We should try to avoid this kind of lazy behavior in the development process, once the subsequent business logic is complex or continuous change adjustment, this “simple programming” because there is no good foundation, can only be built on the sand, poor maintainability, difficult test, change on the existing business serious impact. A simple code practice is nothing more than a business-oriented method process, where all the functional methods are strung together to complete the business logic.

Private void method(){// all logics method1(); // All logics method1(); // All logics method1(); // All logics method1(); method2(); method3(); } private void method1(){ //logics 1 } private void method2(){ //logics 2 } private void method3(){ //logics 3 }Copy the code

Besides, there are some simple code of practice is continuously in a class to carry on the demand iteration, although can be split through some refactoring technique, but this is just the method level optimization, all business logic is encapsulated in the service in a separate class, eventually lead to class code bloat, a large number of private method although closed the short and the logic of the boundary, But it also expanded. The final requirement iteration reached a point where logic could no longer be sorted out in the code, and eventually had to be redone like cutting flesh.

Private void method(){// all logics method1(); private void method(){// all logics method1(); / / assignment setField (A); method2(); / / arithmetic calculate (B, C, D); method3(); // other logic other(S); } private void method1(){ //logics 1 } private void method2(){ //logics 2 } private void method3(){ //logics 3 }Copy the code

One of my most memorable refactorings was a very complex query-list feature that was used to retrieve data for different types of data, build different types of data, do cache filtering in addition to querying DB, and then encapsulate and return data based on a specific type. Due to the rapid iteration of requirements, the code review and walkthrough were slightly missing, and every time the requirements were adjusted, the business logic had no story line, and the subsequent maintenance personnel did not improve and optimize, ultimately resulting in the poor logical processing of the list and a great risk of change. Refactoring may be short, but the cost and risk of consuming this business logic and logic knowledge background is unimaginable.

Analysis of the

Practical problems To solve the direction
Method body verbosity Class as dimension to the method modular split, plug and plug programming
Class code bloat Disassemble class methods and refine method boundaries into class boundaries to improve class cohesion
Decentralized logic and lack of business process thread Construct service processing information flows and detail logical links

In the beat of a lot of project practices, I has been exploring to solve complex business logic way of cracking, trying to find a better solution to the business layer of the out-of-control governance, abstracts a more universal model, make the code practice more elegant, make the maintenance more flexible, truly support extensible, object-oriented programming.

Model development

Simple Version: Responsible Chain

At first, in a business scenario of commodity commission trial calculation, the demand input is the original price of the commodity, and the price return after the discount or subsidy is calculated through the operation of the configured price strategy. The business logic is not very complex, and the main line of business logic processing is a single. It is only necessary to build the main line of process through the chain of responsibility mode to maintain the control over the flow of business logic data.

The implementation is relatively simple, and it is a typical naive responsibility chain design mode construction, which is summarized as follows:

  1. Construct the execution order of responsibility chain [parameter verification → policy pulling → filter trial calculation → return result]
  2. The input parameter is passed in at the beginning, executed in the execution chain of the responsibility chain, and the output parameter is returned at the end

Responsible Chain + ThreadLocal

After that, I took over a wave of requirements for promoting operation activities, and business requirements and scene processing became complicated, such as task list display, lottery logic, reward settlement, and activity killing and grabbing. Here is an illustration of the business process using the logic of the activity lottery:

As service complexity increases, the following problems occur:

Complexity problem To solve the direction solution
Processing chain is no longer unitary, bifurcation logic appears Choreography processing engine execution points construct different execution chains divide and conquer Handler constructs the Processor node, Processor constructs the execution Chain, and one to multiple Chain constructs the execution environment Context for business methods to invoke
The number of business entities involved in processing increases, and all kinds of exceptions are captured and processed Global exception capture, runtime exception definition Method entrance and exit surround
Business nodes before information silos, messaging and thread safety issues Introduction of thread variables, abstract input parameters, output parameters, temporary parameters and custom parameters thread-level life cycle reside, grab, use, clean up, memory and GC-friendly The thread-level communication carrier is constructed based on ThreadLocal, and the unified management of resident, transfer and clearing is achieved
The more complex the business process, the longer the processing chain The fast-fail mechanism must be supported Perform node Check to end as early as possible

Context + Processor + Executor + Handler

Experienced a simple version, enhanced version of the practice and continuous optimization refining, and gradually have the current version.

Model of
composition describe function
Context Runtime environment Maintenance of the construction and execution of the operating environment, the overall carrier of business methods
ContextHolder The environment variable Store business thread environment variables such as input parameters, output parameters, and intermediate variables
Process The processor Business process nodes, which exist through an execution chain
Executor actuator The kernel of the Process processor that determines and influences and drives the execution of the Handler
Handler The executive body The executor of a specific business process
Model initialization

  • The Processor constructs their respective Handler models, which are nodes in the responsibility chain connected in series to form the service logic processing process. The flow here is one-way, because the service logic processing itself flows in a single direction, rather than the inbound and outbound flows required by Netty to process network requests. Here the Processor execution chain defines the order of business logic.
  • Executor is the Processor kernel that drives the execution of the Handler. Executors, like cpus, direct the Handler to execute in order, in batches, and so on. Executor defines how the driver Handler is executed in the individual Processor. For example, in batch query, parallel driven Handler can be selected. If there is sequential dependence among handlers, the sequential execution is similar to that of Processor’s responsibility chain nested with another layer of Handler level responsibility chain. If there is a multi-level responsibility chain, it should be graded according to the business scenario identification domain scope. The broader semantic domain can refine handlers to the Processor, where suggestions that require specific processing reside.
  • The Handler is the most atomic part of the Processor and is responsible for processing specific service logic. As the smallest processing unit, the Handler is pluggable. In the same way, the Processor is regarded as a collection of handlers
  • Context is the runtime environment that builds the Processor and executes it
  • ContextHolder is the holder of the entire thread variable, and its underlying implementation is ThreadLocal, which is naturally thread-safe, encapsulating a ThreadLocal
    >, and binding a Map to a thread. The key of the Map can be used by the current thread to define a unique index value, and the value of the Map can store a variety of specific value objects. Serialization can be determined according to the specific application scenario

Model runtime
  • Request binds the ContextHolder before entering the Context execution environment
  • Processor is the entire execution chain, from processor-1, processor-2, processor-3, to processor-n. Terminate is a common thread level variable between the execution chains, which is used to control the Processor’s execution judgment. The reason why terminate is introduced is that the execution chain may be very long, but the result may be obtained at the beginning of the execution chain due to abnormal or illegal reasons, and the subsequent Processor does not need to be executed. This is modified with volatile to keep terminate visible so that the signal changes are detected in the first place, and the execution chain next method queries terminate to ensure that it needs to terminate prematurely or continue.
  • Executors execute driver handlers sequentially or concurrently
  • Handlers are driven to execute encapsulated business logic
  • ContextHolder Thread variables are only valid for the current thread execution. Both Handler and Processor can hold them and store and obtain thread variables. After each layer of service method execution, remove will be recycled to prevent memory leakage

Model implementation

Model structure

│ ├ ─ icontext. Java execution environment definition, which can be inherited from the Processor parent class. │ IHandler. Java (0 folders, 0 files, 0 files, 0 files, 0 files) Default is to support in the house and out the cords │ IRequestHandler. Java defines support into the Handler refs │ IRequestResponseContext. Java defines support into the house and out of the execution environment and Context Context │ IRequestResponseFutureHandler. Java defines support into the ginseng, asynchronous Handler refs │ IRequestResponseHandler. Java defines support into the participation, The Handler refs │ IResponseFutureHandler. Java defines support asynchronous out and Handler │ IResponseHandler. Java defines support out and Handler │ ├ ─ Handler All handlers are built into the RequestCheckProcessor, Cohesion all check business logic │ RequestAuthCheckHandler. Java check authorization │ RequestChannelCheckHandler. Java check channel │ RequestParamCheckHandler. Java Check request parameter │ RequestTokenCheckHandler. Java check authorization code │ ├ ─ holder │ ContextHolder. Java environment variables encapsulated │ └ ─ processor RequestCheckProcessor. Java a request into the parameter calibration Processor implementationCopy the code

The model code

BaseProcessor

/** * @author: guanjian * @date: 2020/07/10 13:49 * @description: @scope ("prototype") @Component(" prototype") public class baseProcessor <T> implements IProcessor, Iterable { private final static Logger LOGGER = LoggerFactory.getLogger(BaseProcessor.class); /** * thread variable */ @resource protected ContextHolder ContextHolder; @Resource protected ApplicationContext springContext; private final static String TERMINATE = "terminate"; /** * protected IProcessor processor; /** * protected IProcessor antecedent; Handlers = Lists. NewLinkedList (); /** * protected LinkedList<T> Handlers = Lists. public IProcessor getProcessor() { LOGGER.debug("[BaseProcessor] processor is {}.", processor.getClass().getName()); return processor; } public void setProcessor(IProcessor processor) { this.processor = processor; } public IProcessor getSuccessor() { LOGGER.debug("[BaseProcessor] successor is {}.", successor.getClass().getName()); return successor; } public void setSuccessor(IProcessor successor) { this.successor = successor; } public boolean hasSuccessor() { return null ! = successor; } public void processSuccessor() { if (isTerminated()) { LOGGER.debug("[BaseProcessor] terminate is stop status , it will stop all processors."); return; } if (! hasSuccessor()) return; getSuccessor().process(); } public void terminate() { LOGGER.debug("[BaseProcessor] terminate works , it will stop all processors."); contextHolder.bindLocal(TERMINATE, Boolean.TRUE); } public T getHandler() { Assert.notEmpty(handlers, "handlers cant not be null."); LOGGER.debug("[BaseProcessor] handler is {}.", handlers.get(0).getClass().getName()); return handlers.get(0); } public LinkedList<T> getHandlers() { return handlers; } public void setHandlers(LinkedList<T> handlers) { this.handlers = handlers; } @Override public void configurate() { } @Override public void process() { } protected void attach(T handler) { handlers.add(handler); } protected void append(T handler) { handlers.addLast(handler); } @Override public Iterator iterator() { return new BaseProcessorIterator(); } private class BaseProcessorIterator implements Iterator { int index; @Override public boolean hasNext() { if (index < handlers.size()) { return true; } return false; } @Override public Object next() { if (hasNext()) { LOGGER.debug("[BaseProcessor] handler is {}.", handlers.get(index).getClass().getName()); return handlers.get(index++); } return null; } } protected boolean isTerminated() { if (null == contextHolder.getLocal(TERMINATE)) return false; return (boolean) contextHolder.getLocal(TERMINATE); }}Copy the code

ContextHolder

/** * @author: guanjian * @date: 2020/07/08 9:31 * @description: Component("contextHolder") public class contextHolder <T, R> { private final static Logger LOGGER = LoggerFactory.getLogger(ContextHolder.class); Public final static String REQUEST_PARAM = "REQUEST_PARAM "; Public final static String RESPONSE_PARAM = "RESPONSE_PARAM "; /** * public final static String TRANSMIT_PARAM = "TRANSMIT_PARAM "; Private final static ThreadLocal<Map<Object, Object>> localVariable = ThreadLocal.withInitial(() -> Maps.newHashMap()); public void bindLocal(Object key, Object value) { Objects.requireNonNull(key, "key can not be null"); Map holder = localVariable.get(); holder.put(key, value); localVariable.set(holder); LOGGER.debug("[ContextHolder] key={},value={} binded.", key, JSON.toJSONString(value)); } public Object getLocal(Object key) { if (CollectionUtils.isEmpty(localVariable.get())) return null; Object value = localVariable.get().get(key); LOGGER.debug("[ContextHolder] key={},value={} getted.", key, JSON.toJSONString(value)); return value; } public void bindRequest(T value) { bindLocal(REQUEST_PARAM, value); } public T getRequest() { return (T) localVariable.get().get(REQUEST_PARAM); } public void bindResponse(R value) { bindLocal(RESPONSE_PARAM, value); } public R getResponse() { return (R) localVariable.get().get(RESPONSE_PARAM); } public void bindTransmit(Object value) { bindLocal(TRANSMIT_PARAM, value); } public Object getTransmit() { return getLocal(TRANSMIT_PARAM); } public void clear() { localVariable.remove(); }}Copy the code

Processor

One of the Processor implementations is listed here

/** * @author: guanjian * @date: 2020/07/10 13:57 * @description: */ @scope ("prototype") @Component(" Component ") public class requestCheckProcessor extends BaseProcessor { @PostConstruct @Override public void configurate() { configurateHandlers(); } @Override public void process() { Iterator iterator = iterator(); while (iterator.hasNext()) { IRequestResponseHandler<Request, Result> handler = (IRequestResponseHandler<Request, Result>) iterator.next(); if (! parseHandler(handler)) { //force terminate all processorRequestAuthCheckHandler terminate(); break; } } processSuccessor(); } private boolean parseHandler(IRequestResponseHandler<Request, Result> handler) { boolean isContinue = true; Result result = null; try { result = handler.execute((Request) contextHolder.getRequest()); if (! Result.isSuccess(result)) { contextHolder.bindResponse( Response.build( Result.build(result.getCode(), result.getInfo()) ) ); isContinue = false; } } catch (Exception e) { e.printStackTrace(); contextHolder.bindResponse(Response.unknowError()); isContinue = false; } return isContinue; } /** * +-----------------+ +-------------------+ +-----------------+ +---------------+ * |param check| ----> |channel check | ----> | token check | ----> | auth check | * +-----------------+ +------------------+ +-----------------+ +---------------+ */ private void configurateHandlers() { append(springContext.getBean(RequestParamCheckHandler.class));  append(springContext.getBean(RequestChannelCheckHandler.class)); append(springContext.getBean(RequestTokenCheckHandler.class)); append(springContext.getBean(RequestAuthCheckHandler.class)); }}Copy the code

Handler

One of the Handler implementations is listed here

/** * @author: guanjian * @date: 2020/07/10 13:41 * @description: Check channel * / @ Component (" requestChannelCheckHandler ") public class requestChannelCheckHandler implements IRequestResponseHandler<Request, Result> { private final static String REGION = CacheKeyConstants.ChannelConfig.CHANNEL_TOKEN_KEY; private final static LocalCache LOCAL_CACHE = CaffeineCache.getInstance(REGION); @Resource private RedisCache redisCache; @Override public Result execute(Request request) { if (LOCAL_CACHE.hasKey(request.getSource())) { return Result.success(); } Map<String, String> channelToken = redisCache.hGetAll(REGION); if (channelToken.containsKey(request.getSource())) { return Result.success(); } return Result.build( ResponseEnum.ResponseCode.A001.getCode(), ResponseEnum.ResponseCode.A001.getInfo() ); }}Copy the code

The model summary

Finally, the solutions will be put into production and tested in actual combat. Through practical feedback, the functions will be continuously optimized and improved to form a floatable scaffold. Local transaction compatibility and extension processing will be added later.