State Mode - Changes in Encapsulated State

The case for status

 

In daily development, it is often encountered that an object has multiple states, and the actions that need to be performed in different states will be different. Many friends generally use the if elseif else statement to judge different states, and perform different business logic processing on the matched different states. In this way, all business logic codes are merged together, and the non-compliance with the " open and closed principle " will affect the readability of the code and the maintenance of the code in the future (such as adding new status).

 

Let's take a look at a real case that the author encountered. When designing a "page browsing" web service technology architecture implementation, due to the long page rendering time, in order to prevent blocking under high concurrency, the page asynchronous rendering method is adopted: Each page request obtains the rendered page content from the redis cache and returns it. If the cache status expires, only one page rendering is allowed to be initiated. During the page rendering process, if other requests come in , the old page will be obtained directly from redis . Cached content returns:



 

 

Through this "asynchronous page rendering" method, you can ensure that the page content is obtained from the redis cache every time , reducing unnecessary page rendering. But at the same time, the content of the page may change. Here, the page can be re-rendered every 5 minutes. In this process, if the page is regarded as an object, there will be several states:

1. Initial state, state change: The page has not been rendered at this time. If the page is requested at this time, it will enter the " rendering " state. Return content: If there is a cache (last rendered) in redis , return it directly, if not, return " waiting for retry " ;

2. During rendering, the state changes: if the rendering is completed, it will enter the "rendering successful" (push the latest page content to redis ) or "page offline" state. Return content: If there is a cache (last rendered) in redis , return it directly, if not, return content: " Waiting for rendering " ;

3. The rendering is successful, and the state changes: Check whether the last rendering time for example exceeds 5 minutes. If it exceeds, the state changes to " initial state " and waits to be re-rendered. Returns content, the latest page content.

4. The page goes offline and the state changes: If it is re-launched, the state becomes the " initial state " , waiting to be re-rendered. Return content: "The page is offline".

It is represented by a state diagram as follows:

 



 

Assuming that the four states are represented by 0 , 1 , 2 , and 3 , the most common implementation is

If(state==0){
    // handle business logic
}else if(state==1){
    // handle business logic
}else if(state==2){
    // handle business logic
}else if(state==3){
    // handle business logic
}

 

 

It can be done in one method, but the disadvantage is also obvious: the code is difficult to read and maintain. If you want to extend the state and continue to else if , it is easy to cause new bugs if the " open-closed principle " is not satisfied .

 

In fact, as long as you encounter such similar state changes in development, you can use the " state mode " to isolate the operation and state changes of each state.

 

state mode

 

State Pattern: Allows an object to change its behavior when its internal state changes, the object appears to have changed its class. As can be seen from its definition, the state pattern is to encapsulate each state behavior and state change. Each state has a common interface ( or abstract class ) , and external users only deal with this interface ( so-called " interface-oriented " programming ) . Class diagram of the state pattern:



 

The class diagram is simple, almost identical to the " Strategy Pattern " . However, the purpose of the two is different, resulting in different implementations. The strategy pattern can dynamically change the " strategy " in the Context ; the state pattern generally does not change the state in the Context, and the action of changing the state is encapsulated within each state implementation .

 

Example display

 

Going back to the scene at the beginning of the article, the " state pattern " is used to encapsulate the " return content " and " state change " instead of a series of if else .

 

First look at the implementation of the State base class, here only the public action " return content " method of each state is defined:

public abstract class State {
 
    //get page content
    public abstract String getPageContent(String id);
 
    //Omit other public methods
}
 

 

Let's take a look at the specific state. Through the above state diagram analysis, there are 4 states that can be represented by 4 state classes: InitState ( initial state ) , RenderIngState ( rendering ) , RenderSuccessState ( rendering successful ) , OffLineState ( page offline ) ) . Let's start implementing one by one:

 

InitState ( initial state ) state change: the page has not been rendered at this time, if the page is requested at this time, it will enter the " rendering " state. Return content: If there is a cache (last rendered) in redis , return it directly, if not, return " waiting for retry " .

 

public class InitState extends State {
 
    //get page content
    public String getPageContent(String id){
 
        //step 1 Obtain from the cache from the page content according to the page type and id
        String pageContent = Redis.pageContent.get(id);
 
        //step 2 Change the state according to the obtained result
        if(pageContent == null){
            pageContent = "The page starts rendering, please wait another 500ms and try again";
        }
 
        //Step 3 The state is changed to rendering, and a thread is started to simulate rendering
        Redis.pageSate.put(id, Context.renderIngState);
        Thread renderpage = new Thread(new RenderPage(id));
        renderpage.start();
 
        return pageContent;
    }
}
 
//Simulate the page rendering thread
class RenderPage implements Runnable{
    private static final Random rnd = new Random();
    private String id;
    public RenderPage(String id) {
        this.id = id;
    }
 
    public void run() {
        try {
            //Simulated page rendering takes 500ms
            Thread.sleep(500);
            if(rnd.nextBoolean()){//Simulate 50% chance of rendering failure
                Redis.pageSate.put(id, Context.renderSuccessState);
                Redis.pageContent.put(id,"normal page content");
            }else{
                Redis.pageSate.put(id, Context.offLineState);
            }
 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 

 

This state changes to renderIngState in rendering . Using a thread to simulate the rendering process, there is a half chance of successful rendering.

 

RenderIngState ( rendering ) , the simulated rendering action has been done in the RenderPage thread, this state implementation only has "return content":

public class RenderIngState extends State {
    @Override
    public String getPageContent(String id) {
        //step 1 Obtain from the page content from the cache according to the page type and id, omitting the implementation
        String pageContent = Redis.pageContent.get(id);
 
        //step 2 Change the state according to the obtained result
        if(pageContent == null){
            pageContent = "The page is being rendered, please wait another 500ms and try again";
        }
        return pageContent;
    }
}
 

 

RenderSuccessState ( rendering success ) state change: check whether the last rendering time for example exceeds 5 minutes, if it exceeds the state changes to " initial state " and waits to be re-rendered. Returns content, the latest page content.

public class RenderSuccessState extends State {
 
    public String getPageContent(String id){
        //step1 Get page content from cache
        String pageContent = Redis.pageContent.get(id);//Page rendering success status, page
 
        //step2 start a thread to simulate the page's 5-minute state to the initial state
        Thread reRender = new Thread (new ReRender (id));
        reRender.start();
 
        return pageContent;
    }
}
 
class ReRender implements Runnable{
    private String id;
 
    public ReRender (String id) {
        this.id = id;
    }
 
    public void run() {
        try {
            Thread.sleep(5*60);
            Redis.pageSate.put(id, Context.initState);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 

 

Here, a thread is used to simulate the 5 -minute rendering expiration, and the state becomes the "initial state". In actual development, the expiration strategy of redis can be used.

 

OffLineState ( page offline ) state change: If it is brought back online, the state becomes the " initial state " , waiting to be re-rendered. Return content: "The page is offline".

 
public class OffLineState extends State {
    @Override
    public String getPageContent(String id) {
        //The newly opened thread simulation page goes online
        Thread online  = new Thread(new OnLine(id));
        online.start();
 
        return "Page is offline";
    }
}
 
class OnLine implements Runnable{
    private String id;
 
    public OnLine(String id) {
        this.id = id;
    }
 
    public void run() {
        try {
            Thread.sleep(500);
            Redis.pageSate.put(id, Context.initState);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

Here, a thread is used to simulate triggering the " online " operation after 500ms , and the state changes to " initial state " .

 

At this point, 4 states have been realized.

 

Context context implementation :

public class Context {
 
    public static State initState = new InitState();//Initial state
    public static State renderIngState = new RenderIngState();//Rendering state
    public static State offLineState = new OffLineState();//Offline state
    public static State renderSuccessState = new RenderSuccessState();//Rendering success state
 
    public String getPage(String id){
        //get current state
        State state = pageSate.get(id);
        if (state == null){
            state = initState;
            pageSate.put(id,state);//Update the state to the cache
        }
 
        return state.getPageContent(id);
    }
}

 

getPage implementation: First get the status of the current page from redis , and then call the getPageContent method to get the page content. Specifically, the getPageContent method of which state is executed , the Context itself does not know. Assuming that one day a new state is added or the state code is modified, the Context does not need to make any changes, which is the benefit of interface-based programming.

 

Redis helper class

public class Redis {
 
    // page state cache
    public static Map<String,State> pageSate = new HashMap<String,State>();
 
    // page content cache
    public static Map<String,String> pageContent = new HashMap<String,String>();
}
 

 

Here , Hashmap is used to simulate cache, and redis shared cache is generally used in systems deployed with multiple JVM instances.

 

Test code:

public class Main {
 
    public static void main(String[] args) throws Exception{
        Context context = new Context();
        String pageContent = context.getPage("123");
        System.out.println(pageContent);
       
        while(true){
            Thread.sleep(501);
            pageContent = context.getPage("123");
            System.out.println(pageContent);
        }
    }
}
 

 

The Main class implementation here is a simulated client operation. You can see that the client only needs to deal with the Context class, and the implementation details of the page content acquisition have been encapsulated.

 

Execute the main method, the result is:

The page starts to render, please wait another 500ms and try again
Page is offline
The page starts to render, please wait another 500ms and try again
Page is offline
The page starts to render, please wait another 500ms and try again
normal page content
normal page content
Page is offline
normal page content
Page is offline
normal page content
Page is offline
normal page content
// omit countless lines

The implementation of this example is complete.

 

summary

 

The state pattern is the encapsulation of state changes and behaviors, and to a certain extent satisfies the "open-closed principle", " interface-oriented programming principle " , " single responsibility principle " and so on.

 

The state mode class diagram is the same as the strategy mode, the difference is that the strategy mode only encapsulates the behavior; in the context context, the strategy mode needs to change the "strategy" according to the specific business, and the Context of the state mode generally does not process the state change, the state change Actions are encapsulated into each state implementation.

 

 

Applicable scenarios: The object has multiple states, and the changes of the multiple states are regularly formed in a ring. At this time, the " state mode " can be used .

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326558572&siteId=291194637