The application scenarios of state mode are very wide, such as online shopping orders, mobile payment, music players, games, workflow engines and so on. The original intention of state pattern design is to deal with the changes of different behaviors when different states change in the same object. So how do we use state patterns when we encounter similar scenarios?
Not much to say, let's start today's study.
Mode principle analysis
The original definition of state pattern is to allow an object to change its behavior when its internal state changes, and the object seems to modify its own class.
This definition is really a little abstract. In short, state pattern is to let an object control the change of behavior by defining a series of state changes. For example, define several package delivery statuses for purchased items, such as ordered, in transit, signed in, etc. when the "ordered" status changes to the "in transit" status, the logistics truck will transport the packaged package to the specified address, that is, when the package status changes, the corresponding external operation will be triggered.
Let's first look at the standard UML diagram of the state pattern:
From this UML diagram, we can see that the state pattern contains three key roles.
-
Context: in fact, it is a class that stores the current state and provides external operations to update the state.
-
Abstract State class: it can be an interface or abstract class to define the operation methods for declaring State updates.
-
Concrete state class (StateA, etc.): implement the method defined by the abstract state class, and specify the code implementation logic after the corresponding state change according to the specific scenario.
Let's take a direct look at the code implementation corresponding to the UML:
public interface State { void handle(Context context); } public class StateA implements State{ private static StateA instance = new StateA(); public StateA() { } public static StateA instance(){ return instance; } @Override public void handle(Context context) { System.out.println("=== get into the state A"); context.setCurrentState(StateB.instance()); } } public class StateB implements State { private static StateB instance = new StateB(); public StateB() { } public static StateB instance(){ return instance; } @Override public void handle(Context context) { System.out.println("=== get into the state B"); context.setCurrentState(this); } } public class Context { private State currentState; public Context(State currentState) { this.currentState = currentState; if (null == currentState) { this.currentState = StateA.instance(); } } public State getCurrentState() { return currentState; } public void setCurrentState(State currentState) { this.currentState = currentState; } public void request(){ currentState.handle(this); } }
In the code implementation, the State A and State B defined by us are encapsulated into specific State classes StateA and StateB, and the two specific State classes implement the interface of the same abstract State class State. The Context information class Context stores A global variable currentState to save the current State object. The specific State class can obtain the current State of accessing the global by inputting the Context object as A parameter to complete the State switching.
Therefore, the core of state pattern design is to find the appropriate abstract state and the transfer relationship between States, and change the behavior by changing the state.
Usage scenario analysis
Generally speaking, there are several common usage scenarios of state mode.
-
When an object performs operations with different behaviors according to its own state changes, for example, shopping order status.
-
The object needs to change its behavior according to the current value of its own variable. It is not expected to use a large number of if else statements, such as commodity inventory status.
-
For certain certain states and behaviors, when you don't want to use duplicate codes, such as the shopping browsing record of a member on the same day.
For a real example, we can change the display state of the TV by pressing the button on the TV remote control. If the TV is on, we can turn it off, mute it, or change channels. But if the TV is off, it won't work when we press the switch channel button. Because at this time, for the TV in the off state, it is only effective when it is set to the on state.
Another example of state mode in a program is Java thread state. There are five states in the life cycle of a thread. The next state can be determined only after obtaining the current state.
In order to help you better understand the applicable scenarios of state mode, let's demonstrate it through a simple example. In the process of online shopping, when we select the goods and submit the order, the package containing the goods will begin to be transported. Here we define six simple package transportation statuses: ordered, warehouse processing, transportation, dispatch, to be picked up and signed in. As shown in the figure below:
First, we define the package status PackageState, and declare a method updateState() to update the status in the interface, which receives the package context information class PackageContext as a parameter.
public interface PackageState { /** * Six states are defined * 1 - Order placed * 2 - Warehouse processing * 3 - In transit * 4 - Delivery in progress * 5 - Parts to be taken * 6 - Signed in * @param ctx */ void updateState(PackageContext ctx); }
Then we will define the context information class PackageContext in detail, which contains a current state PackageState and a package id.
public class PackageContext { private PackageState currentState; private String packageId; public PackageContext(PackageState currentState, String packageId) { this.currentState = currentState; this.packageId = packageId; if(currentState == null) { this.currentState = Acknowledged.instance(); } } public PackageState getCurrentState() { return currentState; } public void setCurrentState(PackageState currentState) { this.currentState = currentState; } public String getPackageId() { return packageId; } public void setPackageId(String packageId) { this.packageId = packageId; } public void update() { currentState.updateState(this); } }
Next, we define specific status classes in turn: Acknowledged, warehouse processing, in transit, Delivering, WaitForPickUp, and Received. Each class implements the updateState() method and uses the singleton pattern to simulate the uniqueness of the state.
1 - Order placed public class Acknowledged implements PackageState { //Singleton private static Acknowledged instance = new Acknowledged(); private Acknowledged() {} public static Acknowledged instance() { return instance; } @Override public void updateState(PackageContext ctx) { System.out.println("=== state start..."); System.out.println("1 - Package is acknowledged !!"); ctx.setCurrentState(WarehouseProcessing.instance()); } } public class WarehouseProcessing implements PackageState { //Singleton private static WarehouseProcessing instance = new WarehouseProcessing(); private WarehouseProcessing() {} public static WarehouseProcessing instance() { return instance; } @Override public void updateState(PackageContext ctx) { System.out.println("2 - Package is WarehouseProcessing"); ctx.setCurrentState(InTransition.instance()); } } public class InTransition implements PackageState { //Singleton private static InTransition instance = new InTransition(); private InTransition() {} public static InTransition instance() { return instance; } //Business logic and state transition @Override public void updateState(PackageContext ctx) { System.out.println("3 - Package is in transition !!"); ctx.setCurrentState(Delivering.instance()); } } public class Delivering implements PackageState { //Singleton private static Delivering instance = new Delivering(); private Delivering() { } public static Delivering instance() { return instance; } //Business logic @Override public void updateState(PackageContext ctx) { System.out.println("4 - Package is Delivering !!"); ctx.setCurrentState(WaitForPickUp.instance()); } } public class WaitForPickUp implements PackageState { //Singleton private static WaitForPickUp instance = new WaitForPickUp(); private WaitForPickUp() {} public static WaitForPickUp instance() { return instance; } //Business logic and state transition @Override public void updateState(PackageContext ctx) { System.out.println("5 - Package is waiting for pick up !!"); ctx.setCurrentState(Received.instance()); } } public class Received implements PackageState { //Singleton private static Received instance = new Received(); private Received() {} public static Received instance() { return instance; } //Business logic and state transition @Override public void updateState(PackageContext ctx) { System.out.println("6 - Package is Received !!"); System.out.println("=== state end "); } }
Finally, we run a unit test and change the state by performing the update operation of the context information class.
public class Client { public static void main(String[] args) { PackageContext ctx = new PackageContext(null, "Test123"); ctx.update(); ctx.update(); ctx.update(); ctx.update(); ctx.update(); ctx.update(); } } //Output results === state start... 1 - Package is acknowledged !! 2 - Package is WarehouseProcessing 3 - Package is in transition !! 4 - Package is Delivering !! 5 - Package is waiting for pick up !! 6 - Package is Received !! === state end
From the results of the unit test, we can see intuitively that when a status update is performed, the status will change to the next status until the status ends.
Why use state mode?
After analyzing the principle and usage scenarios of state mode, let's talk about the reasons for using state mode, mainly including the following two.
First, when the business to be designed has complex state changes, we expect to quickly change operations through state changes and reduce code coupling. In the example of the above usage scenario, we can roughly see the state change process of a package. In fact, the state change of a shopping order is much more complex than this. The behavior change caused by state change can be well solved by using state pattern. On the one hand, because the status is analyzed and sorted in advance, it can reduce the difficulty of code implementation. On the other hand, the natural isolation between states can aggregate related behaviors, improve class cohesion and reduce coupling.
Second, avoid increasing the complexity of the code. In normal programming, a large number of conditional judgment statements need to be added every time a new state is added. The most typical is the continuous nesting of if else. When such code develops to the later stage, the logic will become extremely complex, resulting in poor maintainability and flexibility of the code. The use of state mode can well carry out logical association from the dimension of state. There is only switching action between states. As for how to deal with the complex state itself, it doesn't care about the other state, which can well avoid the complexity of the call relationship between objects.
What are the benefits? What did you lose?
Through the above analysis, we can conclude that the use state mode has the following advantages.
-
Determine the possible states in advance to reduce the complexity of code implementation. The state pattern usually needs to design the state transition in advance, so it needs to design the transition relationship between states in advance, which makes it much easier to implement the code.
-
Quickly understand the relationship between state and behavior. Because operations are associated with States, all objects related to a state will be aggregated. In this way, operations can be easily added and deleted without affecting the functions of other states.
-
Avoid writing a lot of if else conditional statements. For example, to judge the status of the order goods arriving at the distribution station, you need to judge whether the goods are in transit or have been delivered, and then judge where to send them. Such if else conditional statements will increase with the increase of scenarios. If the transfer relationship between States is established, the order goods arriving at the distribution station will trigger the state transformation, and then carry out the corresponding operation in the corresponding state, which can effectively reduce the direct conditional sentence judgment.
-
Multiple environment objects can share a state object to reduce duplicate code. State mode is usually used for overall process control and state change. In this way, for multi environment applications, only one whole state needs to be shared, and each environment does not need to implement its own state object.
Of course, state patterns have some disadvantages.
-
Resulting in many scattered classes. Because the state pattern needs to define a specific state class for each state, it is bound to increase the number of system classes and objects.
-
The more complex the state switching relationship, the higher the difficulty of code implementation. With the continuous expansion of the state, the structure and implementation of the state will become complex. For example, a state switches to B, B switches to C, C exception returns to a, a goes to D exception state, and so on. If it is not used properly, the person maintaining the code will spend a lot of time sorting out the state transition relationship.
-
Does not meet the opening and closing principle. Although the state mode reduces the coupling between States, adding and modifying States will involve the code modification of the previous state and the latter state, which increases the probability of introducing code problems.
summary
The state pattern describes the changes of object states and how objects behave differently in each state. The suitable scene is that the object itself has many state changes, and different changes need different behaviors to deal with.
Although the state mode can make our code clear and easy to read, it is actually not friendly to support the opening and closing principle. The new state may affect the original state, so we should pay attention to it when using it.
To make good use of the state pattern, the key point is to find a good state and the relationship between States, rather than rush to implement the state pattern. After the state is determined, the code implementation of the state pattern itself is actually very easy.
After class thinking
In the past development experience, have you ever customized different states and implemented the code with state patterns? Welcome to share with me in the message area.
In the next lecture, I will share with you the relevant contents of "observer mode and sending notification of message change". Remember to attend the class on time!