background

Take the order business as an example, there are a variety of business operations, order creation, order payment, shipping, receiving and so on. These operations correspond to different order states. The specified order business can only be performed in the specified order state, for example, the following order state machine:

If the buyer can apply for a refund only in the state of waiting for delivery, the following code is obtained

The original code

Order status enumeration

@Getter
@AllArgsConstructor
public enum OrderStateEnum {
    WAIT_PAY(0."To be paid"),
    WAIT_DELIVER(1."To be shipped"),
    WAIT_RECEIVE(2."To be received"),
    REFUNDING(3."Refund in progress"),

    FINISH(10."Done"),
    REFUNDED(11."Refunded"),;private Integer code;
    private String desc;

    public static OrderStateEnum getEnumByCode(Integer code) {
        for (OrderStateEnum stateEnum : values()) {
            if (stateEnum.getCode().equals(code)) {
                returnstateEnum; }}throw new RuntimeException("Code of illegal"); }}Copy the code

The business process Service class

import java.util.Objects;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    // Get the order number, which is a globally unique distributed ID
    private static Long orderSn = 1L;
    public static String generateOrderSn(a) {
        return String.valueOf(orderSn++);
    }

    /** * create order *@param buyerId
     * @param skuId
     * @return* /
    public String create(Long buyerId, Long skuId) {
        Order order = new Order();
        order.setOrderSn(generateOrderSn());
        order.setBuyerId(buyerId);
        order.setSkuId(skuId);
        order.setStatus(OrderStateEnum.WAIT_PAY.getCode());
        orderRepository.insert(order);
        return order.getOrderSn();
    }

    /** ** initiate payment * order delivery * order receipt * * /	

    /** * After-sale application *@param orderSn
     */
    void refund(String orderSn) {
        Order order = orderRepository.get(orderSn);
        // Check whether the goods are waiting for receipt
        if(! Objects.equals(order.getStatus(), OrderStateEnum.WAIT_DELIVER.getCode())) {throw new RuntimeException("This operation is not supported in this state");
        }
        OrderStateEnum newState = OrderStateEnum.REFUNDING;
        // Update the databaseorder.setStatus(newState.getCode()); orderRepository.update(order); }}Copy the code

Iteration code

With the iteration of business, not only the state of goods to be delivered can apply for after-sales service, but also the state of goods to be received can apply for after-sales service, that is, the state machine is changed to:

The judgment of the corresponding state in refund method also needs to be changed, and even the processing process may be different. The assumption is the same here.

import java.util.Objects;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    // Get the order number, which is a globally unique distributed ID
    private static Long orderSn = 1L;
    public static String generateOrderSn(a) {
        return String.valueOf(orderSn++);
    }

    /** * create order *@param buyerId
     * @param skuId
     * @return* /
    public String create(Long buyerId, Long skuId) {
        Order order = new Order();
        order.setOrderSn(generateOrderSn());
        order.setBuyerId(buyerId);
        order.setSkuId(skuId);
        order.setStatus(OrderStateEnum.WAIT_PAY.getCode());
        orderRepository.insert(order);
        return order.getOrderSn();
    }

    /** ** initiate payment * order delivery * order receipt * * /	

    /** * After-sale application *@param orderSn
     */
    void refund(String orderSn) {
        Order order = orderRepository.get(orderSn);
        // Check whether the goods are waiting for receipt
        if(! Objects.equals(order.getStatus(), OrderStateEnum.WAIT_DELIVER.getCode()) && ! Objects.equals(order.getStatus(), OrderStateEnum.WAIT_RECEIVE.getCode())) {throw new RuntimeException("This operation is not supported in this state");
        }
        OrderStateEnum newState = OrderStateEnum.REFUNDING;
        // Update the databaseorder.setStatus(newState.getCode()); orderRepository.update(order); }}Copy the code

As you can see, here we violate the open close principle by directly changing previously rigorously tested code, resulting in subsequent regression testing. While the current example doesn’t look too bad, the state machine only gets more complex as the business iterates, and if you have to change the original code every time you add new logic, it becomes risky to go live and the code becomes less readable (not too much if-else). Therefore, we try to optimize the code logic writing of the order business with the state pattern.

define

The definition of State mode: For stateful objects, the complex “judgment logic” is extracted into different State objects, allowing the State object to change its behavior when its internal State changes. By looking at the implementation of each state object, we can clearly understand the set of operations that can be performed in that state that have been transferred to the next state.

Pattern structure

The state pattern contains the following primary roles.

  • Context role: Also known as Context, it defines the interface required by the client, maintains a current state internally, and is responsible for switching the specific state.

  • Abstract State role: Defines an interface that encapsulates the behavior of a particular State in an environment object, which can have one or more behaviors.

  • Concrete State role: Implements the behavior of the abstract State and switches the State if necessary.

UML diagrams

Basic pattern implementation

Context class

public class Context {
    private State state;

    public Context(State state) {
        this.state = state;
    }

    public State getState(a) {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public void handle(a) {
        state.handle(this); }}Copy the code

Abstract state class

public abstract class State {

    public abstract void handle(Context context);
}
Copy the code

Specific state class A

public class AState extends State{

    public void handle(Context context) {
        System.out.println(this.getClass().getSimpleName() + "Stream to BState");
        context.setState(newBState()); }}Copy the code

Specific state B

public class BState extends State{

    public void handle(Context context) {
        System.out.println(this.getClass().getSimpleName() + "Will flow to AState");
        context.setState(newAState()); }}Copy the code

The test Client class

public class ClientTest {

    public static void main(String[] args) {
        Context context = new Context(newAState()); context.handle(); context.handle(); }}Copy the code

The execution results show that the first execution of Handle is performed by AState, and the state is transferred to B after the execution; the second execution of Handle is performed by BState, and the state is transferred to A after the execution

AState is going to flow to BState and BState is going to flow to AStateCopy the code

One problem with the basic code above is that each time AState and BState switch states, a new instance is created. This is not necessary. You can save the instance here, for example in Context, all threads share the state instance during the lifetime of the program execution

public class ShareContext {

    private static Map<String, State> shareStateMap = new HashMap<>();

    private State state;

    static {
        shareStateMap.put(AState.class.getSimpleName(), new AState());
        shareStateMap.put(BState.class.getSimpleName(), new BState());
    }
    
    public ShareContext(a) {}

    public State getState(a) {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    // Read the state
    public static State getState(String key) {
        return shareStateMap.get(key);
    }

    public void handle(a) {
        state.handle(this); }}Copy the code

The implementation of state A becomes,

public class AState extends State{
    public void handle(ShareContext context) {
        System.out.println(this.getClass().getSimpleName() + "Stream to BState");
        context.setState(ShareContext.getState("AState")); }}Copy the code

In the same way as in the test Client class, the operation of a new State is changed to fetch from the map.

Optimize order status flow

Pattern base code

The above template code as an example can be easily written to optimize the order state flow of basic code, but now we are generally in the Framework of SpringBoot code, so here is the state mode in SpringBoot code implementation.

Pattern combines code with SpringBoot

See the complete code:…

Define an order status enumeration

Same as before

Define abstract state classes

All methods are inoperable by default at the beginning, and each state implements its own operable method so that state flow is clear at a glance

public abstract class AbstractOrderState {

    public abstract Enum type(a);

    /** * initiate payment **@param context
     * @param order
     */
    public void pay(OrderStateContext context, Order order) {
        throw new RuntimeException("This operation is not supported in this state");
    }

    /** ** Order shipping **@param context
     * @param order
     */
    public void deliver(OrderStateContext context, Order order) {
        throw new RuntimeException("This operation is not supported in this state");
    }

    /** ** Order receipt **@param context
     * @param order
     */
    public void receive(OrderStateContext context, Order order) {
        throw new RuntimeException("This operation is not supported in this state");
    }

    /** * After-sale application **@param context
     * @param order
     */
    public void applyRefund(OrderStateContext context, Order order) {
        throw new RuntimeException("This operation is not supported in this state");
    }

    /** * The refund is completed **@param context
     * @param order
     */
    public void finishRefund(OrderStateContext context, Order order) {
        throw new RuntimeException("This operation is not supported in this state"); }}Copy the code

Define concrete state classes

For example, in the state of waiting for delivery, the delivery method and after-sale application method need to be implemented according to the description of the state machine

@Component
public class WaitPayOrderState extends AbstractOrderState {

    @Autowired
    private OrderRepository orderRepository;

    @Override
    public Enum type(a) {
        return OrderStateEnum.WAIT_PAY;
    }

    /** * Shipping *@param context
     * @param order
     */
    public void deliver(OrderStateContext context, Order order) {
        OrderStateEnum newState = OrderStateEnum.WAIT_RECEIVE;
        // Handle the shipment and update the database
        order.setStatus(newState.getCode());
        orderRepository.update(order);
        // Update the context state
        context.setOrderState(OrderStateFactory.getState(newState));
        System.out.println(Order No. :+ order.getOrderSn() + "Successful delivery! State flow to:" + newState.getDesc());
    }

    /** * Apply for after-sale *@param context
     * @param order
     */
    public void applyRefund(OrderStateContext context, Order order) {
        OrderStateEnum newState = OrderStateEnum.REFUNDING;
        // Handle the shipment and update the database
        order.setStatus(newState.getCode());
        orderRepository.update(order);
        // Update the context state
        context.setOrderState(OrderStateFactory.getState(newState));
        System.out.println(Order No. :+ order.getOrderSn() + "Apply after sale! State flow to:"+ newState.getDesc()); }}Copy the code

Defining context classes

public class OrderStateContext {

    private AbstractOrderState orderState;

    public OrderStateContext(a) {}

    public AbstractOrderState getOrderState(a) {
        return orderState;
    }

    public void setOrderState(AbstractOrderState orderState) {
        this.orderState = orderState;
    }

    /** * initiate payment **@param order
     */
    void pay(Order order) {
        orderState.pay(this, order);
    }

    /** ** Order shipping **@param order
     */
    void deliver(Order order) {
        orderState.deliver(this, order);
    }

    /** ** Order receipt **@param order
     */
    void receive(Order order) {
        orderState.receive(this, order);
    }

    /** * apply for after-sale **@param order
     */
    void applyRefund(Order order) {
        orderState.applyRefund(this, order);
    }

    /** * The refund is completed **@param order
     */
    void finishRefund(Order order) {
        orderState.finishRefund(this, order); }}Copy the code

Encapsulate the state instance factory

Encapsulate state instance factories to share specific state instances and avoid wasting memory

@Component
public class OrderStateFactory implements ApplicationContextAware {

    private static final Map<Enum, AbstractOrderState> stateMap = new HashMap<>(OrderStateEnum.values().length);

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, AbstractOrderState> beans = applicationContext.getBeansOfType(AbstractOrderState.class);
        beans.values().forEach(item -> stateMap.put(item.type(), item));
    }

    public static AbstractOrderState getState(Enum orderStateEnum) {
        returnstateMap.get(orderStateEnum); }}Copy the code

The test code

Here we simulate writing an OrderService, which also corresponds to different operations, but it is simplified here. In general, for example, parameters need to be verified before payment operation, and messages may need to be sent to the downstream after payment, etc.

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    /** * create the order as before */
    public String create(Long buyerId, Long skuId) {
        / /...
    }

    /** * initiate payment **@param orderSn
     */
    void pay(String orderSn) {
        Order order = orderRepository.get(orderSn);
        OrderStateContext context = new OrderStateContext();
        AbstractOrderState currentState = OrderStateFactory.getState(OrderStateEnum.getEnumByCode(order.getStatus()));
        context.setOrderState(currentState);
        context.pay(order);
    }

    /** ** Order shipping **@param orderSn
     */
    void deliver(String orderSn) {
        Order order = orderRepository.get(orderSn);
        OrderStateContext context = new OrderStateContext();
        AbstractOrderState currentState = OrderStateFactory.getState(OrderStateEnum.getEnumByCode(order.getStatus()));
        context.setOrderState(currentState);
        context.deliver(order);
    }

    /** * Order received * apply for after-sales * refund completed * Basically the same code as above, see github */ for complete code
}
Copy the code

Client test, simulate external button operation

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
public class StateClientTest {

    public static void main(String[] args) {
        SpringApplication.run(StateClientTest.class, args);
        OrderService orderService = SpringContextUtil.getBean(OrderService.class);

        1. Create an order
        String orderSn = orderService.create(1L.1L);

        2. Perform the payment operation
        orderService.pay(orderSn);

        //3. Deliver the goods
        orderService.deliver(orderSn);

        //4. Perform the receiving operation
        orderService.receive(orderSn);

        //5. The payment operation failsorderService.pay(orderSn); }}Copy the code

The execution result

The advantages and disadvantages

advantages

  1. For each specific state, you can intuitively see the operations that can be performed in the current state and the state that will flow to
  2. It is easy to add new states and transitions by defining new subclasses that better accommodate the open close principle

disadvantages

  1. When you have too many states you can have too many classes in the system
  2. When there are too many states, there will also be too many operations, resulting in a lot of method definitions in the abstract state class and context. In fact, there can be a lower layer, the operations can be divided into forward and reverse, and cancel and after-sales operations can be defined into reverse

conclusion

When state determination is rare and will not be extended later, it is not necessary to over-design with state patterns in general, and a little if-else looks clear. In addition, the state pattern for state transfer, will directly for part of the operation, such as in the above code will update the database, sometimes actually is not very good, because usually in the operation of an order for database update is definitely more than one table, in order to ensure the transaction, whether they are in the specific state method to update the sometimes is a headache. Another way to implement state management is not to do operations directly, but only a getNextState method, passing in the current state and action, and returning to the next state. The database and other operations are implemented externally, which will be introduced in a separate article later.


reference

State mode (detailed version)

Deconstructing E-commerce Products — Order System (I)