State Machine Oriented Programming: A Solution to Complex Business Logic

Author: Jingdong Retail Fan Siguo

1. Background

In R&D projects, complex business scenarios such as state transitions are often encountered, such as NPC state changes such as jumping, moving forward, and turning in game programming, and order state changes in the e-commerce field. In fact, there is an elegant way to implement this kind of situation: state machine. The following figure is the state machine of the operating system for process scheduling:

Figure Operating system process scheduling state machine

2. Implementation method

In the face of the above scenarios, there are usually the following implementations. The following compares their scope of application and advantages and disadvantages:

2.1 if/else

Advantages: Simple and intuitive to implement.

Disadvantages: The state has more code readability, the business and state judgment are deeply coupled, and maintenance and expansion are difficult.

2.2 State Mode

The state mode class diagram and usage are as follows:

public class StatePatternDemo {
   public static void main(String[] args) {
      Context context = new Context();
 
      StartState startState = new StartState();
      startState.doAction(context);
 
      System.out.println(context.getState().toString());
 
      StopState stopState = new StopState();
      stopState.doAction(context);
 
      System.out.println(context.getState().toString());
   }
}

Advantages: The state is implemented separately, and the readability is better than if/else.

Disadvantages: Extending the status requires adding status classes. If there are too many status classes, there will be many status classes; the decoupling of status and business is not fully realized, which is not conducive to maintaining and understanding the overall status of the entire system.

2.3 Finite state machine

Advantages: Rigorous mathematical model, state transition and business logic are completely decoupled based on events, and the whole system state can be seen for easy maintenance and expansion.

Disadvantages: It is necessary to introduce a state machine implementation method, which has a certain understanding cost.

3. Finite state machine

3.1 Definition

Finite state machine (English: finite-state machine, abbreviation: FSM), also known as finite state automaton (English: finite-state automaton, abbreviation: FSA), referred to as a state machine, is to represent a finite number of states and between these states Mathematical computational models of behaviors such as transfer and action.

3.2 Key concepts

  • State State: generally represented by a circle in the state transition diagram.
  • Event Event: Indicates the trigger mechanism for transitioning from one state to another. Corresponds to the arrow part in the state transition diagram.
  • Action: Indicates the action to be executed after the state transition, but it is not necessary, and no action can be executed after the state transition.
  • Transition Transition: Indicates a state transition, a process of migrating from the original state to the destination state.
  • Condition Guard: Indicates the conditions that must be met for state transition to occur.

3.3 Technology selection

In Java projects, Spring Statemachine and Squirrel-foundation are commonly used.

frameadvantageshortcoming
Spring StatemachineBased on the Spring ecology, the community is strong. It has complete functions and supports multiple state machine configurations and persistence methods.It is heavyweight and has many extra functions. The singleton mode state machine does not guarantee thread safety, and can only be realized by creating a new state machine instance through the factory mode, which has a certain impact on performance.
Squirrel-foundationLightweight implementation, the creation overhead of the state machine is small. It is convenient for secondary transformation and realizes customized business.The community is not as active as spring. There are many special agreements.

To sum up, in the following projects, because the team uses SpringBoot as the development framework, and the project does not involve high concurrency scenarios, Spring Statemachine is chosen.

4. Project actual combat

In real projects, when encountering complex business processes with multiple state transitions, the following steps can be used to classify and gradually realize product requirements clearly:

4.1 Requirement Background

When maintaining SKU logistics attributes (length, width, height, and weight) in retail purchasing and marketing, there will be inconsistencies with the actual attributes on the logistics warehouse side, resulting in deviations in logistics costs. In order to solve this problem, it is necessary to design a system for procurement and marketing to send the attributes to the logistics side for review by operating the SKU, so as to obtain the final accurate logistics attributes. In the process of reviewing the SKU, there are 6 states: inactive, task delivery, delivery failure, audited, self-contacting the supplier, and pending (see 4.2 for details of the status transition). Considering these status transitions The conditions are distributed in different scenarios. In consideration of maintainability and scalability, a state machine is used to realize this requirement.

4.2 State Transition Diagram

By sorting out the state transition relationship, draw the state transition diagram as follows:


SKU attribute review status transition diagram

4.3 Configure state machine

4.3.1 Defining the state enumeration

public enum SkuStateEnum {

    /**
     * not operated
     */
    INIT(0, "not operated"),
    /**
     * The task is being issued
     */
    TASK_DELIVERY(1, "The task is being issued"),
    /**
     * Delivery failed
     */
    DELIVERY_FAIL(2, "Delivery failed"),
    /**
     * Reviewing
     */
    RECHECKING(3, "Reviewing"),
    /**
     * reviewed
     */
    RECHECKED(4, "reviewed"),
    /**
     * Contact the supplier yourself
     */
    CONCAT_SUPPLIER(5, "Contact the supplier yourself"),
    /**
     * hang up
     */
    SUSPEND(6, "hang up");

    /**
     * status code
     */
    private Integer state;
    /**
     * Description
     */
    private String desc;

    SkuStateEnum(Integer state, String desc) {
        this.state = state;
        this.desc = desc;
    }

    public static SkuStateEnum getByState(Integer state) {
        for (SkuStateEnum skuStateEnum : SkuStateEnum.values()) {
            if (skuStateEnum.getState().equals(state)) {
                return skuStateEnum;
            }
        }
        return null;
    }

    public Integer getState() {
        return state;
    }

    public String getDesc() {
        return desc;
    }
}

4.3.2 Define event enumeration

public enum SkuAttrEventEnum {
    /**
     * The call to the OMC attribute collection interface is successful
     */
    INVOKE_OMC_ATTR_COLLECT_API_SUCCESS,
    /**
     * Failed to call the OMC attribute collection interface
     */
    INVOKE_OMC_ATTR_COLLECT_API_FAIL,
    /**
     * Call the OMC to issue the query interface and a collection order has been generated
     */
    INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH,
    /**
     * Failed to call the OMC delivery query interface
     */
    INVOKE_OMC_SKU_DELIVERY_API_FAIL,
    /**
     * OMC The MQ returned SKU property has changed
     */
    MQ_OMC_SKU_ATTR_CHANGED,
    /**
     * Call the platform jsf interface of the product, and return the SKU attribute has been changed
     */
    INVOKE_SKU_ATTR_API_CHANGED,
    /**
     * Jingdong has stock
     */
    HAS_JD_STOCK,
    /**
     * JD has no stock, VMI has stock
     */
    NO_JD_STOCK_HAS_VMI_STOCK,
    /**
     * JD.com and VMI are out of stock
     */
    NO_JD_STOCK_NO_VMI_STOCK,
    /**
     * upload and review
     */
    UPLOAD_AND_RECHECK;
}

4.3.3 Configure state machine

@Configuration
@EnableStateMachineFactory
@Slf4j
public class SkuAttrStateMachineConfig extends StateMachineConfigurerAdapter<SkuStateEnum, SkuAttrEventEnum> {

    /**
     * configuration status
     *
     * @param states
     * @throws Exception
     */
    @Override
    public void configure(StateMachineStateConfigurer<SkuStateEnum, SkuAttrEventEnum> states) throws Exception {
        states.withStates().initial(SkuStateEnum.INIT)
            .states(EnumSet.allOf(SkuStateEnum.class));
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<SkuStateEnum, SkuAttrEventEnum> config) throws Exception {
        config.withConfiguration().listener(listener()).autoStartup(false);
    }

    /**
     * Configure the relationship between state transitions and events
     *
     * @param transitions
     * @throws Exception
     */
    @Override
    public void configure(StateMachineTransitionConfigurer<SkuStateEnum, SkuAttrEventEnum> transitions)
        throws Exception {
        transitions.withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.TASK_DELIVERY)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS)
            .action(ctx -> {
                log.info("[transfer OMC The attribute collection interface is successful],status change:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.TASK_DELIVERY.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.DELIVERY_FAIL)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL)
            .action(ctx -> {
                log.info("[transfer OMC The attribute collection interface failed],status change:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.DELIVERY_FAIL.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.CONCAT_SUPPLIER)
            .event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK)
            .action(ctx -> {
                log.info("[Jingdong has no stock,VMI in stock],status change:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.CONCAT_SUPPLIER.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.SUSPEND)
            .event(SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK)
            .action(ctx -> {
                log.info("[Jingdong and VMI None in stock],status change:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.SUSPEND.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.TASK_DELIVERY)
            .event(SkuAttrEventEnum.HAS_JD_STOCK)
            .action(ctx -> {
                log.info("[Jingdong has stock],status change:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),
                    SkuStateEnum.TASK_DELIVERY.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.RECHECKING)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS)
            .action(ctx -> {
                log.info("[transfer OMC The attribute collection interface is successful],status change:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),
                    SkuStateEnum.RECHECKING.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.CONCAT_SUPPLIER)
            .event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK)
            .action(ctx -> {
                log.info("[Jingdong has no stock,VMI in stock]:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),
                    SkuStateEnum.CONCAT_SUPPLIER.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKING)
            .event(SkuAttrEventEnum.INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH)
            .action(ctx -> {
                log.info("[transfer OMC The query interface has been issued and a collection order has been generated]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(),
                    SkuStateEnum.RECHECKING.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKED)
            .event(SkuAttrEventEnum.MQ_OMC_SKU_ATTR_CHANGED)
            .action(ctx -> {
                log.info("[OMC of MQ return SKU property changed]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(),
                    SkuStateEnum.RECHECKED.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.DELIVERY_FAIL).target(SkuStateEnum.TASK_DELIVERY)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS)
            .action(ctx -> {
                log.info("[transfer OMC The attribute collection interface is successful]:{} -> {}.", SkuStateEnum.DELIVERY_FAIL.getDesc(),
                    SkuStateEnum.TASK_DELIVERY.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.CONCAT_SUPPLIER).target(SkuStateEnum.RECHECKED)
            .event(SkuAttrEventEnum.INVOKE_SKU_ATTR_API_CHANGED)
            .action(ctx -> {
                log.info("[Call the product center jsf interface, return SKU property changed]:{} -> {}.", SkuStateEnum.CONCAT_SUPPLIER.getDesc(),
                    SkuStateEnum.RECHECKED.getDesc());
            });
    }

    /**
     * global listener
     *
     * @return
     */
    private StateMachineListener<SkuStateEnum, SkuAttrEventEnum> listener() {
        return new StateMachineListenerAdapter<SkuStateEnum, SkuAttrEventEnum>() {
            @Override
            public void transition(Transition<SkuStateEnum, SkuAttrEventEnum> transition) {
                //When the state transfer is in the configure method configuration, it will go to this method.
                log.info("[{}]status change:{} -> {}", transition.getKind().name(),
                    transition.getSource() == null ? "NULL" : ofNullableState(transition.getSource().getId()),
                    transition.getTarget() == null ? "NULL" : ofNullableState(transition.getTarget().getId()));
            }

            @Override
            public void eventNotAccepted(Message<SkuAttrEventEnum> event) {
                //When the state transfer that occurs is not in the configuration of the configure method, it will go to this method, where the error log is printed to facilitate troubleshooting of state transfer problems
                log.error("event not received: {}", event);
            }

            private Object ofNullableState(SkuStateEnum s) {
                return Optional.ofNullable(s)
                    .map(SkuStateEnum::getDesc)
                    .orElse(null);
            }
        };
    }
}

4.4 Business logic processing

4.4.1 Building a state machine

For the operation of each sku, through the state machine factory stateMachineFactory.getStateMachine

//Inject state machine factory instance
@Autowired
private StateMachineFactory<SkuStateEnum, SkuAttrEventEnum> stateMachineFactory;
//build state machine
public StateMachine<SkuStateEnum, SkuAttrEventEnum> buildStateMachine(String skuId) throws BusinessException {
        if (StringUtils.isEmpty(skuId)) {
            return null;
        }
        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine = null;
        try {
            //Get the status corresponding to the current skuId from the DB
            LambdaQueryWrapper<SkuAttrRecheckState> query = Wrappers.lambdaQuery(SkuAttrRecheckState.class);
            query.eq(SkuAttrRecheckState::getSkuId, skuId);
            SkuAttrRecheckState skuAttrRecheckState = this.baseMapper.selectOne(query);
            SkuStateEnum skuStateEnum = SkuStateEnum.getByState(
                skuAttrRecheckState == null ? SkuStateEnum.INIT.getState() : skuAttrRecheckState.getState());
            //Get a state machine from the state machine factory
            stateMachine = stateMachineFactory.getStateMachine(skuId);
            stateMachine.stop();
            //Configure state machine parameters
            stateMachine.getStateMachineAccessor().doWithAllRegions(sma -> {
                //Configure the state machine interceptor, when the state is transferred, it will go to the interceptor
                sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<SkuStateEnum, SkuAttrEventEnum>() {
                    @Override
                    public void preStateChange(State<SkuStateEnum, SkuAttrEventEnum> state,
                        Message<SkuAttrEventEnum> message,
                        Transition<SkuStateEnum, SkuAttrEventEnum> transition,
                        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
                        StateMachine<SkuStateEnum, SkuAttrEventEnum> rootStateMachine) {
                        //Get the detailed information of the corresponding SKU when the status is transferred
                        SkuAttrRecheckState result = JSON.parseObject(
                            String.class.cast(message.getHeaders().get(JSON_STR)), SkuAttrRecheckState.class);
                        //Update the state after the state machine transfer (from the configuration in 4.3.3)
                        result.setState(state.getId().getState());
                        //Write the state after the state machine transfer to DB
                        LambdaQueryWrapper<SkuAttrRecheckState> query = Wrappers.lambdaQuery(SkuAttrRecheckState.class);
                        query.eq(SkuAttrRecheckState::getSkuId, result.getSkuId());
                        if (baseMapper.exists(query)) {
                            UpdateWrapper<SkuAttrRecheckState> updateQuery = new UpdateWrapper<>();
                            updateQuery.eq("sku_id",result.getSkuId());
                            log.info("update status information:{}", JSON.toJSONString(result));
                            baseMapper.update(result, updateQuery);
                        } else {
                            log.info("write status information:{}", JSON.toJSONString(result));
                            baseMapper.insert(result);
                        }
                    }
                });
                //Configure the initial state of the state machine as the state corresponding to the skuId in the DB
                sma.resetStateMachine(new DefaultStateMachineContext<SkuStateEnum, SkuAttrEventEnum>(
                    skuStateEnum, null, null, null));
            });
            //start state machine
            stateMachine.start();
        } catch (Exception e) {
            log.error("skuId={},Build state machine failed.", skuId, e);
            throw new BusinessException("State machine build failed", e);
        }
        return stateMachine;
    }

4.4.2 Encapsulating events

public synchronized Boolean sendEvent(StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
        SkuAttrEventEnum skuAttrEventEnum, SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {
        try {
            //Send an event and write the information that needs to be passed into the header
            stateMachine.sendEvent(MessageBuilder.withPayload(skuAttrEventEnum)
                .setHeader(SKU_ID, skuAttrRecheckState.getSkuId())
                .setHeader(STATE, skuAttrRecheckState.getState())
                .setHeader(JSON_STR, JSON.toJSONString(skuAttrRecheckState))
                .build());
        } catch (Exception e) {
            log.error("Failed to send event", e);
            throw new BusinessException("Failed to send event", e);
        }
        return true;
    }

4.4.3 Business Logic Application

When the user clicks the review button on the SKU in the "unoperated" state on the interface, the logistics OMC interface will be called to send the SKU attributes to the logistics side. If the interface fails, the status will be changed to "delivery failure". The core code is as follows:

public Boolean recheck(List<String> skuIds) throws BusinessException {
        if (CollectionUtils.isEmpty(skuIds)) {
            log.error("Parameter error, sku list is empty");
            throw new BusinessException("Parameter error, sku list is empty");
        }
        List<SkuAttrExceptionDetail> skuDetails = skuAttrExceptionDetailMapper.queryBySkuIdList(skuIds);
        if (CollectionUtils.isEmpty(skuDetails)) {
            log.error("Inquire sku Exception detail result set is empty,skuIds={}", JSON.toJSONString(skuIds));
            return false;
        }
        for (SkuAttrExceptionDetail detail : skuDetails) {
            if (detail.getState() != SkuStateEnum.INIT.getState()) {
                log.info("{}not inactive sku no review", detail.getSkuId());
                continue;
            }
            //Build the state machine corresponding to SKU
            StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine = buildStateMachine(detail.getSkuId());
            SkuAttrRecheckState skuAttrRecheckState = DomainBuilderUtil.buildSkuAttrRecheckState(detail);
            //Determine stock and send event
            adjustAndSendEvents(detail, stateMachine, skuAttrRecheckState);
        }
        return true;
    }

public void adjustAndSendEvents(SkuAttrExceptionDetail detail,
        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
        SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {
        //1. JD.com has inventory, calls the logistics attribute interface, and issues SKU attributes
        if (detail.getSpotInventoryQtty() > 0) {
            invokeOmcSkuAttrCollectApiAndSendEvent(detail, stateMachine, skuAttrRecheckState);
            return;
        }
        //2. JD.com has no inventory, but VMI has inventory
        if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() > 0) {
            sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK, skuAttrRecheckState);
            return;
        }
        //3. Both JD.com and VMI have no inventory
        if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() <= 0) {
            sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK, skuAttrRecheckState);
            return;
        }
    }

private void invokeOmcSkuAttrCollectApiAndSendEvent(SkuAttrExceptionDetail detail,
        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
        SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {
        DistrustAttributeGatherRequest request = RequestUtil.buildOmcAttrCollectRequest(detail, reqSource);
        try {
            if (jsfInvokeService.invokeSkuAttrCollectApi(request)) {
                sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS,
                    skuAttrRecheckState);
            } else {
                sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState);
            }
        } catch (Exception e) {
            log.error("call logistics Sku Attribute collection interface error,request={}", JSON.toJSONString(request), e);
            sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState);
        }
    }

V. Summary

This article introduces the finite state machine, combined with specific projects, and decouples the state and business logic through the application of the state machine, which facilitates the simplification of complex business logic and reduces the cost of understanding. In addition, the application scenario of the state machine is also suitable for that kind of complex process. In actual projects, the implicit state transition relationship can be sorted out according to the core elements of the state machine, so as to convert a complex process problem into a state machine mode problem. , and then use the state machine model to realize it, which can help us solve a wider range of complex business problems elegantly.

Tags: Java Programming Operating System

Posted by smurl on Fri, 10 Mar 2023 12:12:00 +1030