The author | ZhangYe Ming

Dr. Almond CTO. Middle aged programmer, focus on various technologies and team management.

An overview of the

We often encounter complicated state transitions when developing business modules. For example, the user may be in the state of new registration, real-name authentication, real-name authentication or disabled, and the payment may be in the state of waiting for payment, payment in progress or paid. There’s more state handling in OA. Faced with these processes, many people will probably use the most intuitive if/else or switch methods to determine the state without thinking. But there are better ways to handle complex state transitions than this crude approach.

State judgment

Let’s take payment as an example. An order may be waiting for payment, in payment, paid, etc. For orders awaiting payment, users may make payment through third-party payment such as wechat Pay or Alipay, and the third-party payment will call back to inform the payment result after the payment is completed. We might deal with it like this:

public void pay(Order order) { if (order.status == UNPAID) { order.status = PAYING; } else throw IllegalStateException(" can't pay "); } public void paySuccess(Order Order) {if (Order. Status == PAYING) { } else throw IllegalStateException(" cannot pay "); }Copy the code

That seems to be all right. But suppose we allow the user to make multiple payments to complete an order, so we need to add a partial payment state. When the order is in the partial payment state, the next payment can be made; The order may be converted to paid or partially paid status depending on the payment amount upon receipt of successful payment notification. Now we have to deal with this state in pay and paySuccess.

public void pay(Order order) { if (order.status == UNPAID || order.status == PARTIAL_PAID) { order.status = PAYING; } else throw IllegalStateException(" can't pay "); } public void paySuccess(Order Order) {if (Order. Status == PAYING) {// paySuccess(Order. PaidFee == Order. order.status = PAID; else order.status = PARTIAL_PAID; } else throw IllegalStateException(" cannot pay "); }Copy the code

With payment, we must also be able to support refund, which requires the addition of refund status and refund status, as well as the corresponding refund operation and refund success callback processing.

public void refund(Order order) { if (order.status == PAID || order.status == PARTIAL_PAID) { order.status = REFUNDING; } else throw IllegalStateException(" No refunds "); }public void refundSuccess(Order Order) {if (order.status == REFUNDING) {// Order. status = REFUNDING; } else throw IllegalStateException(" Non-refundable "); }Copy the code

If a finite state machine (FSM) was used to represent the current state transition, it would look something like this:

For the few states, the transition is not very complex, using state judgment to deal with the situation is also concise. But once there are more states and operations become more complex, the business code becomes riddled with conditional judgments and state processing logic scattered all over the place. Adding a new state, or adjusting some processing logic, can be cumbersome and error-prone.

In this case, for example, exists in the actual processing may also cancel the order, payment failure/timeout, refund/timeout, and so on and so forth, if coupled with logistics and some internal state, the handle up is extremely complex, and there will be easy to pay failed can also give the user a refund, or have a refund to the customers delivery should not be the case. This is a bad taste and makes code difficult to maintain and extend.

State patterns for design patterns

The next thing many people might think about is the GOF state model. For processing involving complex state logic, the use of state patterns can be more easily maintained and extended by abstracting the specific state rather than scattering it among the conditional judgment processing of various methods.

State patterns generally contain three roles, Context, State, and ConcreteState. State is the State interface, which defines the operation of the State. ConcreteState is an implementation of concrete states. The relation of other gate is shown in the figure below:

Let’s try to implement the previous order state transition using a state pattern. First we need to define the state interface, which should contain all the required operations and the corresponding implementation for each state.

abstract class OrderState { public abstract OrderState pay(Order order); public abstract OrderState paySuccess(Order order); public abstract OrderState refund(Order order); public abstract OrderState refundSuccess(Order order); }public class PayingOrderState implement OrderState { public OrderState pay(Order order) { throw IllegalStateException(" Already in payment "); } public OrderState paySuccess(Order order, long fee) { doPaySuccess(Order order, long fee); if (order.paidFee < order.totalFee) { order.setState(new PartialPaidOrderState()); } else { order.setState(new PaidOrderState()); }} public OrderState refund(Order Order) {throw IllegalStateException(" not paid yet "); } public OrderState refundSuccess(Order Order) {throw IllegalStateException(" not completed payment "); }}public class UnpaidOrder implement OrderState { ... }public class PartialPaidOrderState implement OrderState { ... }public class PaidOrderState implement OrderState { ... }public class RefundingOrderState implement OrderState { ... }public class RefundedOrderState implement OrderState { ... }Copy the code

You may notice that not every state supports every operation. For example, in the implementation above, PayingOrderState cannot be refunded and PaidOrderState cannot be paid. Here we throw an IllegalStateException. Of course, instead of throwing an exception, you can put an empty implementation. Alternatively, we can define an Abstract Class that contains the default implementation of the operation, and each state Class only needs to override its own supported methods.

Then we implement the Context, our Order entity, which contains a state field, state, through which all the state transition logic is implemented. With these defined, the implementation of the payment service is simple.

public class Order {    OrderState state = new UnpaidOrder();
    public void pay(long fee) {
        state.pay(fee);
    }
    public void paySuccess(long fee) {
        state.paySuccess(this, fee);        
    }
    public void refund() { ... }
    public void refundSuccess() { ... }}public class PaymentService {
    public void payOrder(long orderId) {
        Order order = OrderRepository.find(orderId) 
        order.pay();
        OrderRepository.save(order);
    }}Copy the code

With state mode, we avoid a lot of state judgments in the code, and the implementation of state transition rules is clearer. However, it should be noted that the actual state mode does not conform to the Open/Close Principle very well. When adding a state, it is possible to modify the logic of other existing states. But compared with the method of state judgment, it is clear and convenient.

State modeling for domain-driven design

Another problem with the state pattern, as mentioned earlier, is that there are many operations in the real business that are only valid for some states, and the state pattern requires that each state implement all operations, sometimes unnecessarily.

In this case, explicit state modeling is recommended in domain-driven design. That is, modeling entities in different states into different entity classes; Or each entity class represents a set of states.

For example, we could define an entity class for each state of the order. However, because it is possible for orders in multiple states to support the same operation, we need to define some interfaces to abstract such operations.

public interface CanPayOrder { Order pay(); }public interface CanPaySuccessOrder { ... }public interface CanRefundOrder { ... }public interface CanRefundSuccessOrder { ... }public class UnpaidOrder implements CanPayOrder { ... }public class PayingOrder implements CanPaySuccessOrder { ... }public class PartialPaidOrder implements CanPayOrder, CanRefundOrder { ... }public class PaidOrder implements CanRefundOrder { ... }public class RefundingOrder implements CanRefundSuccessOrder { ... }public class PaymentService {public void pay(long orderId) {Order Order = orderRepository.find (orderId) // convert to CanPayOrder orderToPay = order.ascanpayOrder (); Order payingOrder = orderToPay.pay(); OrderRepository.save(payingOrder); }}Copy the code

The operations that entities in each state can support are explicitly defined. In this way, when there are many operations and many operations are only valid for part of the state, the disadvantages of the state mode can be effectively avoided and the code is more concise and clear.

State transitions in dynamic languages

In the above example, both UnpaidOrder and PartialPaidOrder can perform pay operations. Actually, when processing the payment operation, we do not need to know whether it is UnpaidOrder or PartialPaidOrder, I just need to know that the current order entity supports the Pay operation. In statically typed languages like Java, we can only handle this by defining some Interface or Abstract class, which is a bit of a hassle.

In dynamically typed languages such as Python, Ruby, or JavaScript, Duck Typing can be used to further simplify things. Duck Typing is: if there is a bird that walks like a Duck, swims like a Duck and quacks like a Duck, we call it a Duck. This means that we can determine at run time whether an object supports a behavior regardless of its type.

For example, in JavaScript, once we get the order entity, we can simply call the pay method by determining whether it has been defined, without knowing what type the object is.

let orderToPay = order.asOrderStateEntity(); if (typeof orderToPay['pay'] === 'function') { orderToPay.pay(); } else {throw new ServiceError(" This order cannot be paid "); }Copy the code

Of course, not many of these business systems are actually developed in dynamic languages, because dynamic languages cause other problems as well.

FSM

Whether it’s state patterns or state entities, transitions between states, or scattered implementations of states. In fact, all state transitions can be summarized as: F(S, E) -> (A, S’), that is, if the current state is S and an event E is received, the action A is executed and the state transitions to S’ at the same time.

Akka provides a framework for finite state machines called FSM, which can separate state transitions from business processing logic through Scala’s pattern matching and other powerful features. I won’t go into the details, and we haven’t used it in actual development. But we can get a feel for it:

Class OrderFSM extends FSM[State, Data] {California when(boss) {case Event(New Jersey, New Jersey, New Jersey, New Jersey, New Jersey, New Jersey) If it is in the position of Paying doPay(data) goto(Paying) {if it is in the position of Paying If the event PaySuccess is received, the payment is processed successfully by Case Event(PaySuccess(fee), data) fee) if (fee+data.paidFee == data.totalFee) goto(Paid) else goto(PartialPaid) } // ... }Copy the code

Of course, THERE is more to FSM than that, and the actual implementation can be more complex. There is also an implementation of finite state machine in Java called Squirrel, but due to the limitations of the Java language, it is not as elegant to use as Akka FSM. I’m not going to go into that, but if you’re interested in it, you can look at it.

conclusion

This paper briefly introduces several methods to deal with complex state logic in business system. Except in very simple cases, you should avoid using state judgment. Using state patterns or state modeling can greatly improve the maintainability and extensibility of your code. Finally, some optimization of state modeling by dynamic language and FSM framework are briefly introduced.

The full text after


You may also be interested in the following articles:

  • Lego micro Service Transformation (I)

  • Lego micro service Transformation (II)

  • A startup’s Path to containerization (I) – Containerization before

  • A startup’s path to containerization (II) – Containerization

  • The containerization of a startup (iii) – The container is the future

  • Four-dimensional Reading: My Secret Technique for Efficient Study

  • The skills necessary for an engineer to grow

  • IOS screen adaptation

  • Principle and implementation of data interaction between Web and App

  • Responsive programming (PART 1) : Overview

  • Responsive programming (part 2) : Spring 5

  • Apple’s three kits in health care

  • Talk about the mobile cross-platform database Realm

We are looking for a Java engineer. Please send your resume to [email protected].

Almond technology station

Long press the left QR code to pay attention to us, here is a group of passionate young people looking forward to meeting with you.