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
- 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
- It is easy to add new states and transitions by defining new subclasses that better accommodate the open close principle
disadvantages
- When you have too many states you can have too many classes in the system
- 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)