introduction
State mode may be strange to you at first. What's the use of this mode? I am a CRUD BOY. In the face of different states, I judge one state by one, if else, if else Doesn't it smell good to keep writing different logic?
Sweet! But as an outstanding representative of houlang, how can I satisfy my desire for knowledge!
We know that object-oriented design patterns have seven basic principles:
- Open Closed Principle (OCP)
- Single responsibility principle (SRP)
- Liskov Substitution Principle (LSP)
- Dependency Inversion Principle (DIP)
- Interface aggregation principle (ISP)
- Composite/Aggregate Reuse Principle (CARP)
- Least Knowledge Principle (LKP) or Law of Demeter (LOD)
The simple understanding is:
- The opening and closing principle is the general program, which guides us to open to expansion and close to modification;
- The single responsibility principle guides us to realize the single responsibility of the class; Richter's substitution principle guides us not to destroy the inheritance system;
- The dependency inversion principle guides us to interface oriented programming; The principle of interface isolation guides us to simplify and single when designing interfaces;
- Dimitri's law directs us to reduce coupling.
Design pattern is to guide us how to do a good design through these seven principles. However, design pattern is not a set of "tricks", it is a set of methodology, a design idea of high cohesion and low coupling. We can play freely on this basis, and even design our own set of design patterns.
Of course, learning design patterns or practicing design patterns in engineering must go deep into a specific business scenario, combined with the understanding of business scenarios and the establishment of domain models, in order to realize the essence of design patterns. It is extremely empty to learn or use design patterns without specific business logic.
Next, we will discuss how to use state design pattern to reduce if else and realize reusable and maintainable code through business practice.
State mode
I wonder if you often encounter this situation in your business:
Product: brother developer, you see, I want to add an intermediate process. How to deal with this process, I also want to distinguish users who have added these operations. Other users who do not meet this condition should not be affected. Can they be realized!
Me: Yes, just add a state! Therefore, the original process is added with a state. What happens when the user is in this state, So I changed it and went online a few days later.
Product: brother developer, come again. You see, I want to add an intermediate process. How to deal with this process, I also want to distinguish users who have added these operations. Other users who do not meet this condition should not be affected. Can they be realized!
Me: Yes! Inner OS: eh, deja vu Yan returns. I added one before. I still added it! So the sound was added again. I thought it was over, but after a few days, I asked again! So we constantly judge if else and if else, and install and modify the original process! Finally, I accidentally moved the code in the previous state. A tragedy occurred and the production environment reported an error!
This is a problem that every development brother will encounter. With the continuous development of business, the state of our definition table will continue to expand, and the flow between States will become more and more complex. The original small piece of if else code will be more and more complex, which is really confusing.
Is there a model that can decouple these businesses, involving the generation of events and the consequent impact (state flow). You can bind the event and the state change after the event. The state flow generated by different events is also different. We can configure it from a global perspective.
yes , we have! Of course, it's our protagonist today - state mode
definition
In State Pattern, the behavior of a class changes based on its state. This type of design pattern belongs to behavioral pattern.
In the state mode, we create objects representing various states and a context object whose behavior changes with the change of the state object.
intention
Allows an object to change its behavior when its internal state changes, and the object looks as if it has modified its class.
Main solution
The behavior of an object depends on its state (properties), and its related behavior can be changed according to its state change.
When to use
The code contains a large number of conditional statements related to the object state.
How to solve
Abstract various concrete state classes.
critical code
There is usually only one method in the command mode interface. There are one or more methods in the interface of state mode. Moreover, the method of the implementation class of the state mode generally returns the value or changes the value of the instance variable. That is, the state pattern is generally related to the state of the object. The methods of implementing classes have different functions, covering the methods in the interface. Like command mode, state mode can also be used to eliminate if Else and other conditional selection statements.
advantage
1. Encapsulates the transformation rules. 2. Enumerate possible states. Before enumerating States, you need to determine the state type. 3. Put all the behaviors related to a state into one class, and you can easily add new states. You can change the behavior of the object only by changing the state of the object. 4. Allow state transition logic to be integrated with state objects rather than a huge conditional statement block. 5. Multiple environment objects can share a state object, thus reducing the number of objects in the system.
shortcoming
1. The use of state mode will inevitably increase the number of system classes and objects. 2. 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. 3. The state mode does not support the "opening and closing principle" very well. For the state mode that can switch states, adding a new state class requires modifying the source code responsible for state transition, otherwise it cannot switch to the new state, and modifying the behavior of a state class also requires modifying the source code of the corresponding class.
Usage scenario
1. A scene in which behavior changes with state changes. 2. Substitutes for conditions and branch statements.
Actual use code
After talking about a lot of concepts, everyone must still be vague. Let's take a look at this scene
scene
As a small up, my biggest wish is that what I write can be seen by more people. You should be familiar with the operations of coin, praise, collection and one click triple. Your enthusiasm directly affects the update frequency of up. Then the event and status appear:
- Event: coin, like, collect
- Status: SOMETIME (update whenever you think of it), OFTEN (update OFTEN), USUALLY (update when something happens), ALWAYS (liver that hasn't stopped)
We can get a relationship:
- Coin: upsometimestate - > upoftenstate
- Like: upoftenstate - > upusuallystate
- Favorites: upusuallystate - > upalwaysstate
- English frequency from low to high: sometime - > often - > normally - > always
After knowing the basic information, let's do object-oriented development based on design pattern principles!
code
We first define a state abstract class to represent the update frequency of up
package cn.guess.statemachine.one; import lombok.Data; /** * @program: guess * @description: up Master update frequency status interface * @author: xingcheng * @create: 2020-05-10 12:18 **/ @Data public abstract class UpState { /** * Context in current up state */ protected BlogContext blogContext; /** * Operation in this state */ protected abstract void doAction(); /** * Switching state */ protected abstract void switchState(); }
Next, we define subclasses to represent each different state:
package cn.guess.statemachine.one; /** * @program: guess * @description: up Master Sometime update status * @author: xingcheng * @create: 2020-05-10 12:22 **/ public class UpSometimeState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // Switching state action: coin state flow: upsometimestate - > upoftenstate blogContext.setUpState(new UpOftenState()); // Execute action blogContext.getUpState().doAction(); } @Override public String toString() { return "UpSometimeState"; } }
package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up Master Often update status * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpOftenState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // Switching state action: coin state flow: upoftenstate - > upusuallystate blogContext.setUpState(BlogContext.UP_USUALLY_STATE); // Execute action blogContext.getUpState().doAction(); } @Override public String toString() { return "UpOftenState"; } }
package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up Master update status * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpUsuallyState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // Switching state action: coin state flow: upusuallystate - > upalwaysstate blogContext.setUpState(BlogContext.UP_ALWAYS_STATE); // Execute action blogContext.getUpState().doAction(); } @Override public String toString() { return "UpUsuallyState"; } }
package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up Master Always update status * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpAlwaysState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // Final state, no switching state // Execute action blogContext.getUpState().doAction(); } @Override public String toString() { return "UpAlwaysState"; } }
We also need a context to associate the flow of states
package cn.guess.statemachine.one; import lombok.Data; import lombok.NoArgsConstructor; /** * @program: guess * @description: Blog context sensitive information packaging * Coin: upsometimestate - > upoftenstate * Like: upoftenstate - > upusuallystate * Favorites: upusuallystate - > upalwaysstate * English frequency from low to high: sometime - > often - > normally - > always * @author: xingcheng * @create: 2020-05-10 12:17 **/ @Data @NoArgsConstructor public class BlogContext { public final static UpSometimeState UP_SOMETIME_STATE = new UpSometimeState(); public final static UpOftenState UP_OFTEN_STATE = new UpOftenState(); public final static UpUsuallyState UP_USUALLY_STATE = new UpUsuallyState(); public final static UpAlwaysState UP_ALWAYS_STATE = new UpAlwaysState(); /** * Current up master status */ private UpState upState; public BlogContext(UpState upState) { this.upState = upState; this.upState.setBlogContext(this); } /** * User action on blog content - Coin */ public static void throwCoin() { new BlogContext(BlogContext.UP_SOMETIME_STATE).getUpState().switchState(); } /** * User action on blog content - like */ public static void like() { new BlogContext(BlogContext.UP_OFTEN_STATE).getUpState().switchState(); } /** * User action on blog content - Collection */ public static void collect() { new BlogContext(BlogContext.UP_USUALLY_STATE).getUpState().switchState(); } }
Next, we write a client to simulate the call process:
package cn.guess.statemachine.one; /** * @program: guess * @description: State switching actuator * @author: xingcheng * @create: 2020-05-10 15:36 **/ public class UpStateClient { public static void main(String[] args) { // Start simulating each action event - state transition will be performed automatically // coin-operated System.out.println("Coin action"); BlogContext.throwCoin(); System.out.println("-----------------------------------------------------------------------"); // give the thumbs-up System.out.println("Like action"); BlogContext.like(); System.out.println("-----------------------------------------------------------------------"); // Collection System.out.println("Collection action"); BlogContext.collect(); } }
At this point, the state mode is completed. You can see that we have completed the judgment without using if else.
Each state is also replaced by a class. The changes we make to one state will not affect the other state logic
In this way, the principle of opening to extension and closing to modification is well realized.
Let's look at the output:

The first mock exam is brother brother. We are almost springboot in the development environment. Can we combine this powerful ecosystem with spring to achieve this model?
Yes! There is no doubt that a state automata can be realized by combining spring's powerful IOC and AOP!!!
SpringBoot state automata
Still the scene just now, let's implement it through Spring StateMachine.
code
Package introduction:
<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>${spring-boot-statemachine.version}</version> </dependency>
I use < spring boot Statemachine version>2.2.0. RELEASE</spring-boot-statemachine. Version > version
Define status and event enumerations
package cn.guess.statemachine.tow.enums; import java.util.Objects; /** * up The English frequency of status event enumeration is from low to high: sometime - > often - > normally - > always * @program: guess * @author: xingcheng * @create: 2020-05-10 16:12 **/ public enum UpStateEnum { UP_SOMETIME_STATE(0, "SOMETIME"), UP_OFTEN_STATE(10, "OFTEN"), UP_USUALLY_STATE(20, "USUALLY"), UP_ALWAYS_STATE(30, "ALWAYS"), ; /** * Enumeration encoding */ private final int code; /** * Enumeration description */ private final String value; public int getCode() { return code; } public String getValue() { return value; } UpStateEnum(int code, String value) { this.code = code; this.value = value; } /** * Convert to enumeration object according to enumeration key value * * @param key enum * @return enumerable object */ public static UpStateEnum keyOf(int key) { UpStateEnum[] values = values(); for (UpStateEnum stateEnum : values) { if (Objects.equals(stateEnum.getCode(), key)) { return stateEnum; } } return null; } }
package cn.guess.statemachine.tow.enums; import java.util.Objects; /** * Blog event enumeration * @program: guess * @author: xingcheng * @create: 2020-05-10 16:08 **/ public enum BlobEventEnum { THROW_COIN(0, "coin-operated"), LIKE(10, "give the thumbs-up"), COLLECT(20, "Collection"), ; /** * Enumeration encoding */ private final int code; /** * Enumeration description */ private final String value; public int getCode() { return code; } public String getValue() { return value; } BlobEventEnum(int code, String value) { this.code = code; this.value = value; } /** * Convert to enumeration object according to enumeration key value * * @param key enum * @return enumerable object */ public static BlobEventEnum keyOf(int key) { BlobEventEnum[] values = values(); for (BlobEventEnum stateEnum : values) { if (Objects.equals(stateEnum.getCode(), key)) { return stateEnum; } } return null; } }
Create state machine configuration class
package cn.guess.statemachine.tow.config; import cn.guess.statemachine.tow.enums.BlobEventEnum; import cn.guess.statemachine.tow.enums.UpStateEnum; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.transition.Transition; import java.util.EnumSet; /** * @program: guess * @description: This annotation is used to enable the Spring StateMachine state machine feature * @author: xingcheng * @create: 2020-05-10 16:14 **/ @EnableStateMachine @Configuration public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<UpStateEnum, BlobEventEnum> { /** * configure Used to initialize which states the current state machine has * * @param states * @throws Exception */ @Override public void configure(StateMachineStateConfigurer<UpStateEnum, BlobEventEnum> states) throws Exception { states .withStates() // The initial state is defined as UP_SOMETIME_STATE .initial(UpStateEnum.UP_SOMETIME_STATE) //Specify all States in UpStateEnum as the state definition of the state machine .states(EnumSet.allOf(UpStateEnum.class)); } /** * configure It is used to initialize the state migration actions of the current state machine * From the naming, we can easily understand that each migration action has a source state source, a target state target and a trigger event event * Event and state flow relationship binding: similar to the throwCoin of BlogContext and the switchState process under UpSometimeState * * @param transitions * @throws Exception */ @Override public void configure(StateMachineTransitionConfigurer<UpStateEnum, BlobEventEnum> transitions) throws Exception { transitions .withExternal() // Coin: upsometimestate - > upoftenstate .source(UpStateEnum.UP_SOMETIME_STATE).target(UpStateEnum.UP_OFTEN_STATE) .event(BlobEventEnum.THROW_COIN) .and() .withExternal() // Like: upoftenstate - > upusuallystate .source(UpStateEnum.UP_OFTEN_STATE).target(UpStateEnum.UP_USUALLY_STATE) .event(BlobEventEnum.LIKE) .and() .withExternal() // Favorites: upusuallystate - > upalwaysstate .source(UpStateEnum.UP_USUALLY_STATE).target(UpStateEnum.UP_ALWAYS_STATE) .event(BlobEventEnum.COLLECT); } /** * configure A state listener is specified for the current state machine, and listener() is the listener instance created by calling the next function to handle the state migration events. * Note here because we have other better methods to replace it */ // @Override // public void configure(StateMachineConfigurationConfigurer<UpStateEnum, BlobEventEnum> config) throws Exception { // config // .withConfiguration() // //Specifies the processing listener for the state machine // .listener(listener()); // } /** * listener()Method is used to create an instance of StateMachineListener state listener, * In this instance, specific state migration processing logic will be defined. In the above implementation, only some outputs are made, * The actual business scenario will have more rigorous logic, so generally, we can put the definition of the instance into an independent class definition and load it by injection. * Note here because we have other better methods to replace it */ // @Bean // public StateMachineListener<UpStateEnum, BlobEventEnum> listener() { // return new StateMachineListenerAdapter<UpStateEnum, BlobEventEnum>() { // // @Override // public void transition(Transition<UpStateEnum, BlobEventEnum> transition) { // if (transition.getTarget().getId() == UpStateEnum.UP_SOMETIME_STATE) { // System.out.println("up sometime update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_SOMETIME_STATE // && transition.getTarget().getId() == UpStateEnum.UP_OFTEN_STATE) { // System.out.println("user throw coin, up sometime update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_OFTEN_STATE // && transition.getTarget().getId() == UpStateEnum.UP_USUALLY_STATE) { // System.out.println("user like blob, up usually update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_USUALLY_STATE // && transition.getTarget().getId() == UpStateEnum.UP_ALWAYS_STATE) { // System.out.println("user collect blob, up always update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_ALWAYS_STATE) { // System.out.println("up always update blob"); // return; // } // } // // }; // } }
Annotation listener
package cn.guess.statemachine.tow.config; import org.springframework.statemachine.annotation.OnTransition; import org.springframework.statemachine.annotation.OnTransitionEnd; import org.springframework.statemachine.annotation.OnTransitionStart; import org.springframework.statemachine.annotation.WithStateMachine; /** * @program: guess * @description: This configuration implements CN guess. statemachine. tow. config. Implementation of state machine listener defined in statemachineconfig class * @author: xingcheng * @create: 2020-05-10 16:31 **/ @WithStateMachine public class EventConfig { @OnTransition(target = "UP_SOMETIME_STATE") public void initState() { System.out.println("up sometime update blob"); } @OnTransition(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoin() { System.out.println("up sometime update blob"); } @OnTransitionStart(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoinStart() { System.out.println("up sometime update blob start"); } @OnTransitionEnd(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoinEnd() { System.out.println("up sometime update blob end"); } @OnTransition(source = "UP_OFTEN_STATE", target = "UP_USUALLY_STATE") public void like() { System.out.println("user like blob, up usually update blob"); } @OnTransition(source = "UP_USUALLY_STATE", target = "UP_ALWAYS_STATE") public void collect() { System.out.println("user collect blob, up always update blob"); } }
Create an application Controller to complete the process
package cn.guess.statemachine.tow.controller; import cn.guess.common.api.ApiResult; import cn.guess.common.web.controller.BaseController; import cn.guess.statemachine.tow.enums.BlobEventEnum; import cn.guess.statemachine.tow.enums.UpStateEnum; import cn.guess.system.web.res.UserSelfCenterInfoRes; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.statemachine.StateMachine; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @program: guess * @description: state Test related interfaces * @author: xingcheng * @create: 2020-05-10 16:38 **/ @Slf4j @RestController @RequestMapping("/api/state") @Api(value = "state Test related interfaces API", description = "03.state Test related interfaces") public class StateController extends BaseController { @Autowired private StateMachine<UpStateEnum, BlobEventEnum> stateMachine; @GetMapping("/v1/run") @ApiOperation(value = "state Detection request") public String stateRun() { // start() is the blogging process of creating the up master. According to the previous definition, the up will be in the state of infrequent update (SOMETIME) stateMachine.start(); // Perform coin operation by calling sendEvent(Events.THROW_COIN) stateMachine.sendEvent(BlobEventEnum.THROW_COIN); // Perform the like operation by calling sendEvent(Events.THROW_COIN) stateMachine.sendEvent(BlobEventEnum.LIKE); // Perform the collection operation by calling SendEvent (events. Thread_coin) stateMachine.sendEvent(BlobEventEnum.COLLECT); return "OK"; } }
Call result

explain
We can summarize how to use Spring StateMachine as follows:
- Define status and event enumerations
- Define all States used and initial states for the state machine
- Define the state migration action for the state machine
- Specifies the listening processor for the state machine
Status listener
Through the above introductory example and the final summary, we can see that when using Spring StateMachine to implement the state machine, the code logic becomes very simple and hierarchical.
The scheduling logic of the whole state mainly depends on the definition of configuration mode, and all business logic operations are defined in the state listener.
In fact, the functions of the state listener are far more than those described above. It also has more event capture. We can understand all its event definitions by viewing the StateMachineListener interface:


summary
The core of the state pattern is encapsulation, which encapsulates the state and state transition logic into the interior of the class, and also well reflects the "opening and closing principle" and "single responsibility principle". Each state is a subclass. Whether modifying or adding a state, you only need to modify or add a subclass. In our application scenario, the number of States and state transitions are much more complex than the above examples. A large number of if else codes are avoided through "state mode", which makes our logic clearer. At the same time, due to the good encapsulation of the state pattern and the design principles followed, we can easily manage various states in complex business scenarios.