Architect's mental skill, detailed explanation of the state mode of participating in e-commerce order business development

Keywords: Programming Spring Redis

State mode is also common in life scenes. For example, we usually change the order status of online shopping, as well as the status of taking the elevator.

In the process of software development, there may be different situations for a certain operation. Generally, the most direct way to deal with multi situation problems is to use if...else or switch...case conditional statements for judgment. This method has its own disadvantages in judging complex states: the judgment condition statements are too bloated, readability is poor, expansibility is not available, and dimension is very difficult. If we change our thinking, we can separate these different states and express them with different classes. In which case does the system deal with, we can directly use the corresponding state classes to deal with them, eliminate the condition judgment statements, and the code is more hierarchical and has good extension ability.

State Pattern, also known as State Machine Pattern, allows an object to change its behavior when its internal state changes, and the object appears to modify its class. The behavior of a class in a State Pattern is determined by the state, and there are different behaviors in different states. Its intention is that when an object changes within it, its behavior will change accordingly. The core of State Pattern is the binding of state and behavior. Different states correspond to different behaviors.

1, Application scenario of state mode

State mode is applicable to the following scenarios:

  • The behavior changes the scene as the state changes;
  • There are many branches in an operation, and these branches depend on the state of the object.

There are three main roles in state mode:

  • Environment class role (Context): define the interface required by the client, internally maintain a current state instance, and be responsible for the specific state switching;
  • Abstract State: defines the behavior in this State, which can have one or more behaviors;
  • Concrete state: implement the behavior corresponding to the state and switch the state when needed.

1.1 application of state mode in business scenarios

When we read an article in a community, if we think it is well written, we will forward, collect and comment on it. If the user is logged in, we can comment, forward and collect these behaviors. Otherwise, you need to jump to the login page before you can perform the previous action. There are two states involved here: logged in and not logged in, and three behaviors: comment, forward, and favorite. The following uses code to implement these logic. First, create the abstract state role class UserState:

public abstract class UserState {

    private AppContext appContext;

    public void setAppContext(AppContext appContext) {
        this.appContext = appContext;
    }

    public abstract void forward();

    public abstract void collect();

    public abstract void comment(String comment);

}

To create a login state LoginState class:

public class LoginState extends UserState {

    @Override
    public void forward() {
        System.out.println("Forwarding succeeded!");
    }

    @Override
    public void collect() {
        System.out.println("Collection succeeded!");
    }

    @Override
    public void comment(String comment) {
        System.out.println("Comment success,The content is:" + comment);
    }
}

Next, create the unregistered state UnLoginState class:

public class UnLoginState extends UserState {

    @Override
    public void forward() {
        forward2Login();
        this.appContext.forward();
    }

    @Override
    public void collect() {
        forward2Login();
        this.appContext.collect();
    }

    @Override
    public void comment(String comment) {
        forward2Login();
        this.appContext.comment(comment);
    }

    private void forward2Login() {
        System.out.println("Jump to the login page!");
        this.appContext.setState(this.appContext.LOGIN_STATE);
    }
}

To create a context role AppContext class:

public class AppContext {

    public static final UserState LOGIN_STATE = new LoginState();

    public static final UserState UNLOGIN_STATE = new UnLoginState();

    private UserState currentState = UNLOGIN_STATE;

    {
        UNLOGIN_STATE.setAppContext(this);
        LOGIN_STATE.setAppContext(this);
    }

    public void setState(UserState state) {
        this.currentState = state;
        this.currentState.setAppContext(this);
    }

    public UserState getState() {
        return this.currentState;
    }

    public void forward() {
        this.currentState.forward();
    }

    public void collect() {
        this.currentState.collect();
    }

    public void comment(String comment) {
        this.currentState.comment(comment);
    }

}

Test the main method:

 public static void main(String[] args) {
    AppContext context = new AppContext();
    context.forward();
    context.collect();
    context.comment("That's great. Give me a compliment with both hands and feet👍");
}

Operation result:

1.2 using state machine to realize order state flow control

State machine is an application of state mode, which is equivalent to an upgraded version of context role. It is widely used in various systems such as workflow or game, such as various workflow engines. It is almost a subset and implementation of state machine, encapsulating the change rules of state. Spring provides a good solution. The component name of Spring is called StateMachine. State machine helps developers simplify the development process of state control and make state machine structure more hierarchical. Next, use the spring state machine to simulate an order state flow process.

1. pom dependency

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>2.0.1.RELEASE</version>
</dependency>

2. Create order entity class

public class Order {
    private int id;
    private OrderStatus status;
    public void setStatus(OrderStatus status) {
        this.status = status;
    }

    public OrderStatus getStatus() {
        return status;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    @Override
    public String toString() {
        return "Order number:" + id + ", Order status:" + status;
    }
}

3. Create order status enumeration class and status transformation enumeration class

/**
 * Order status
 */
public enum OrderStatus {
    // To be paid, to be shipped, to be received, order closed
    WAIT_PAYMENT, WAIT_DELIVER, WAIT_RECEIVE, FINISH;
}
/**
 * Order status change event
 */
public enum OrderStatusChangeEvent {
    // Pay, deliver, confirm receipt
    PAYED, DELIVERY, RECEIVED;
}

4. Add state flow configuration

/**
 * Order state machine configuration
 */
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatus, OrderStatusChangeEvent> {
 
    /**
     * Configuration state
     * @param states
     * @throws Exception
     */
    public void configure(StateMachineStateConfigurer<OrderStatus, OrderStatusChangeEvent> states) throws Exception {
        states
                .withStates()
                .initial(OrderStatus.WAIT_PAYMENT)
                .states(EnumSet.allOf(OrderStatus.class));
    }
 
    /**
     * Configure state transition event relationships
     * @param transitions
     * @throws Exception
     */
    public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderStatusChangeEvent> transitions) throws Exception {
        transitions
                .withExternal().source(OrderStatus.WAIT_PAYMENT).target(OrderStatus.WAIT_DELIVER).event(OrderStatusChangeEvent.PAYED)
                .and()
                .withExternal().source(OrderStatus.WAIT_DELIVER).target(OrderStatus.WAIT_RECEIVE).event(OrderStatusChangeEvent.DELIVERY)
                .and()
                .withExternal().source(OrderStatus.WAIT_RECEIVE).target(OrderStatus.FINISH).event(OrderStatusChangeEvent.RECEIVED);
    }
 
    /**
     * Persistent configuration
     * In actual use, you can cooperate with redis to perform persistent operations
     * @return
     */
    @Bean
    public DefaultStateMachinePersister persister(){
        return new DefaultStateMachinePersister<>(new StateMachinePersist<Object, Object, Order>() {
            @Override
            public void write(StateMachineContext<Object, Object> context, Order order) throws Exception {
                //There is no persistence here
            }
 
            @Override
            public StateMachineContext<Object, Object> read(Order order) throws Exception {
                //The status in the order is obtained directly here, but there is no persistent read operation
                return new DefaultStateMachineContext(order.getStatus(), null, null, null);
            }
        });
    }
}

5. Add order status listener

@Component("orderStateListener")
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListenerImpl{
 
    @OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
    public boolean payTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.WAIT_DELIVER);
        System.out.println("Payment, state machine feedback:" + message.getHeaders().toString());
        return true;
    }
 
    @OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
    public boolean deliverTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.WAIT_RECEIVE);
        System.out.println("Delivery, state machine feedback:" + message.getHeaders().toString());
        return true;
    }
 
    @OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
    public boolean receiveTransition(Message<OrderStatusChangeEvent> message){
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.FINISH);
        System.out.println("Receiving, state machine feedback:" + message.getHeaders().toString());
        return true;
    }
}

6. Create the IOrderService interface

public interface IOrderService {
    //Create new order
    Order create();
    //Initiate payment
    Order pay(int id);
    //Order shipment
    Order deliver(int id);
    //Order collection
    Order receive(int id);
    //Get all order information
    Map<Integer, Order> getOrders();
}

7. Add business logic to Service

@Service("orderService")
public class OrderServiceImpl implements IOrderService {

    @Autowired
    private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;
 
    @Autowired
    private StateMachinePersister<OrderStatus, OrderStatusChangeEvent, Order> persister;
 
    private int id = 1;
    private Map<Integer, Order> orders = new HashMap<>();

    public Order create() {
        Order order = new Order();
        order.setStatus(OrderStatus.WAIT_PAYMENT);
        order.setId(id++);
        orders.put(order.getId(), order);
        return order;
    }

    public Order pay(int id) {
        Order order = orders.get(id);
        System.out.println("Thread Name:" + Thread.currentThread().getName() + " Attempt to pay, order No.:" + id);
        Message message = MessageBuilder.withPayload(OrderStatusChangeEvent.PAYED).setHeader("order", order).build();
        if (!sendEvent(message, order)) {
            System.out.println("Thread Name:" + Thread.currentThread().getName() + " Failure to pay, Abnormal status, order No.:" + id);
        }
        return orders.get(id);
    }

    public Order deliver(int id) {
        Order order = orders.get(id);
        System.out.println("Thread Name:" + Thread.currentThread().getName() + " Try shipping, order No.:" + id);
        if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.DELIVERY).setHeader("order", order).build(), orders.get(id))) {
            System.out.println("Thread Name:" + Thread.currentThread().getName() + " Shipment failed, status abnormal, order No.:" + id);
        }
        return orders.get(id);
    }

    public Order receive(int id) {
        Order order = orders.get(id);
        System.out.println("Thread Name:" + Thread.currentThread().getName() + " Try receiving, order No.:" + id);
        if (!sendEvent(MessageBuilder.withPayload(OrderStatusChangeEvent.RECEIVED).setHeader("order", order).build(), orders.get(id))) {
            System.out.println("Thread Name:" + Thread.currentThread().getName() + " Failed to receive goods, abnormal status, order No.:" + id);
        }
        return orders.get(id);
    }
 

    public Map<Integer, Order> getOrders() {
        return orders;
    }
 
 
    /**
     * Send order status conversion event
     *
     * @param message
     * @param order
     * @return
     */
    private synchronized boolean sendEvent(Message<OrderStatusChangeEvent> message, Order order) {
        boolean result = false;
        try {
            orderStateMachine.start();
            //Attempt to restore state machine state
            persister.restore(orderStateMachine, order);
            //Add latency for thread safety testing
            Thread.sleep(1000);
            result = orderStateMachine.sendEvent(message);
            //Persistent state machine state
            persister.persist(orderStateMachine, order);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            orderStateMachine.stop();
        }
        return result;
    }
}

Test the main method:

public static void main(String[] args) {

    Thread.currentThread().setName("Main thread");

    ConfigurableApplicationContext context = SpringApplication.run(Test.class,args);

    IOrderService orderService = (IOrderService)context.getBean("orderService");

    orderService.create();
    orderService.create();

    orderService.pay(1);

    new Thread("Client thread"){
        @Override
        public void run() {
            orderService.deliver(1);
            orderService.receive(1);
        }
    }.start();

    orderService.pay(2);
    orderService.deliver(2);
    orderService.receive(2);

    System.out.println("All order status:" + orderService.getOrders());

}

2, Source code representation in state mode

The specific application of state pattern is very rare in the source code, which generally only provides a general solution. If you have to find it, you can find it. After a lot of hard work and continuous brain burning, let's take a look at a Lifecycle class in JSF source code. JSF is also a classic front-end framework, so it doesn't matter if you haven't used a small partner. We are just analyzing its design idea. In JSF, the processing of all pages is divided into six stages, which are defined in the phase class to represent the life cycle stage with different constants. The source code is as follows:

public class PhaseId implements Comparable {
    private final int ordinal;
    private String phaseName;
    private static int nextOrdinal = 0;
    private static final String ANY_PHASE_NAME = "ANY";
    public static final PhaseId ANY_PHASE = new PhaseId("ANY");
    private static final String RESTORE_VIEW_NAME = "RESTORE_VIEW";
    public static final PhaseId RESTORE_VIEW = new PhaseId("RESTORE_VIEW");
    private static final String APPLY_REQUEST_VALUES_NAME = "APPLY_REQUEST_VALUES";
    public static final PhaseId APPLY_REQUEST_VALUES = new PhaseId("APPLY_REQUEST_VALUES");
    private static final String PROCESS_VALIDATIONS_NAME = "PROCESS_VALIDATIONS";
    public static final PhaseId PROCESS_VALIDATIONS = new PhaseId("PROCESS_VALIDATIONS");
    private static final String UPDATE_MODEL_VALUES_NAME = "UPDATE_MODEL_VALUES";
    public static final PhaseId UPDATE_MODEL_VALUES = new PhaseId("UPDATE_MODEL_VALUES");
    private static final String INVOKE_APPLICATION_NAME = "INVOKE_APPLICATION";
    public static final PhaseId INVOKE_APPLICATION = new PhaseId("INVOKE_APPLICATION");
    private static final String RENDER_RESPONSE_NAME = "RENDER_RESPONSE";
    public static final PhaseId RENDER_RESPONSE = new PhaseId("RENDER_RESPONSE");
    private static final PhaseId[] values;
    public static final List VALUES;

    private PhaseId(String newPhaseName) {
        this.ordinal = nextOrdinal++;
        this.phaseName = null;
        this.phaseName = newPhaseName;
    }

    public int compareTo(Object other) {
        return this.ordinal - ((PhaseId)other).ordinal;
    }

    public int getOrdinal() {
        return this.ordinal;
    }

    public String toString() {
        return null == this.phaseName ? String.valueOf(this.ordinal) : this.phaseName + ' ' + this.ordinal;
    }

    static {
        values = new PhaseId[]{ANY_PHASE, RESTORE_VIEW, APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES, INVOKE_APPLICATION, RENDER_RESPONSE};
        VALUES = Collections.unmodifiableList(Arrays.asList(values));
    }
}

Then these state switches are all performed in the execute() side and the cancel side of Lifecycle. A parameter FacesContext object will be passed, and finally all the States will be saved by FacesContext. In this case, we will not make further analysis.

3, Related modes of state mode

3.1 state mode and responsibility chain mode

Both state mode and responsibility chain mode can eliminate the problem of too many if branches. But in some cases, the state in the state mode can be understood as responsibility. In this case, both modes can be used.

From the definition point of view, the state pattern emphasizes the change of internal state of an object, while the responsibility chain pattern emphasizes the change of external node objects.

In terms of code implementation, the biggest difference between them is that each state object of state mode knows its next state object to enter, while the responsibility chain mode does not know its next node processing object, because the client is responsible for the chain assembly.

3.2 state mode and strategy mode

The UML class diagram architecture of state pattern and policy pattern is almost the same, but their application scenarios are different. There are many kinds of algorithm behaviors in the policy mode, one of which can be satisfied. The independent users can change the policy algorithm by themselves, while the states in the state mode are related to each other. Under certain conditions, there is an automatic state switching effect between them, and the user cannot specify the state, so he can only set the initial state.

4, Advantages and disadvantages of state mode

Advantage:

  • Clear structure: the state is independent of the class, eliminating redundant if...else or switch...case statements, making the code more concise and improving the maintainability of the system;
  • To display the state transition, the common object uses the numerical type to define the state. The state transition is represented by assignment, which is not intuitive. When using the state class, it is represented by different classes, and the purpose of transition is more clear;
  • State responsibilities are clear and extensible.

Disadvantages:

  • Class expansion: if a thing has many states, it will cause too many state classes;
  • The structure and implementation of state mode are complex. If it is not used properly, it will lead to the confusion of program structure and code;
  • The state mode does not support the opening and closing principle very well. To add a new state class to a state mode that can switch state, you need to modify the source code of those responsible for state conversion. Otherwise, you cannot switch to the new state, and to modify the behavior of a state class, you need to modify the source code of the corresponding class.

Posted by fuji on Sat, 21 Mar 2020 00:23:03 -0700