状态模式--封装状态的变化

关于状态的案例

 

在日常开发中经常会遇到 一个对象有多种状态的情形,并且在不同的状态下需要执行的动作会不同。很多朋友一般会采用if elseif else语句进行判断不同的状态,对匹配到的不同状态进行不同的业务逻辑处理。这样所有的业务逻辑代码都被融合在一起,不符合开闭原则验重影响代码的可读性,以及将来代码的维护(比如新增状态)。

 

下面来看一个笔者遇到的真实案例,在设计一个“页面浏览”的web服务技术架构实现时,由于页面渲染时间较长,为了防止高并发情况下的阻塞,采用了页面异步渲染的方式:每次页面请求都从redis缓存中获取已渲染好的页面内容返回,如果缓存状态过期时,只允许发起一次页面渲染,在页面渲染过程中,如果有其他请求进来 也会直接从redis中获取老缓存内容返回:



 

 

通过这种“异步页面渲染”方式处理,就能保证每次都从redis缓存获取页面内容,减少不必要的页面渲染。但同时页面内容有可能发生变化,这里可以每隔5分钟对页面重新渲染一次。这个过程中 如果把页面看做是对象,就会存在几种状态:

1、初始状态,状态变化:此时页面还没有渲染,如果此时请求页面,会进入渲染中状态。返回内容:如果redis中有缓存(上次渲染的)直接返回,如果没有,返回等待重试

2、渲染中,状态变化:如果渲染完成会进入“渲染成功”(往redis中推送最新页面内容)或者“页面下线”状态。返回内容:如果redis中有缓存(上次渲染的)直接返回,如果没有,返回内容:等待渲染中

3、渲染成功,状态变化:检查举例上次渲染时间是否超过5分钟,如果超过 状态变为初始状态等待重新被渲染。返回内容,最新的页面内容。

4、页面下线,状态变化:如果被重新上线,状态变为初始状态,等待被重新渲染。返回内容:“页面已下线”。

用状态图来表示如下:

 



 

假设4个状态分别用0123表示,最常见的实现方式就是

If(state==0){
    //处理业务逻辑
}else if(state==1){
    //处理业务逻辑
}else if(state==2){
    //处理业务逻辑
}else if(state==3){
    //处理业务逻辑
}

 

 

在一个方法中就搞定,但是缺点也很明显:代码难以阅读和维护,如果要扩展状态又要继续else if,不满足开闭原则很容易引发新的bug

 

其实只要开发中有遇到这种类似状态变化的情况,都可以使用状态模式对各个状态的操作和状态变化进行隔离。

 

状态模式

 

状态模式:允许一个对象在其内部状态改变的时候改变其行为,这个对象看上去就像是改变了它的类一样。从其定义可以看出,状态模式是对各个状态行为和状态改变进行封装。各个状态有一个共同的接口(或抽象类),外部使用者只与这个接口打交道(所谓的面向接口编程)。状态模式的类图:



 

类图很简单,跟策略模式几乎完全一样。但两者的目的不同,导致具体的实现有差异。策略模式在Context中可以动态的改变策略;状态模式在Context中一般不会改变状态,改变状态的动作被封装在每个状态实现内部。

 

示例展示

 

回到文章开头的场景,这里采用状态模式来封装返回内容状态变化,而不是采用一系列的if else

 

首先看来State基类的实现,这里只定义了每个状态的公共动作返回内容方法:

public abstract class State {
 
    //获取页面内容
    public abstract String getPageContent(String id);
 
    //省略其他公共方法
}
 

 

再来看下具体的状态,通过上述状态图分析,这里有4个状态 分别可以用4个状态类表示:InitState(初始状态)RenderIngState(渲染中)RenderSuccessState(渲染成功)OffLineState(页面下线)。下面开始逐个实现:

 

InitState(初始状态) 状态变化:此时页面还没有渲染,如果此时请求页面,会进入渲染中状态。返回内容:如果redis中有缓存(上次渲染的)直接返回,如果没有,返回等待重试

 

public class InitState extends State {
 
    //获取页面内容
    public String getPageContent(String id){
 
        //step 1 根据页面type、id从页面内容从缓存获取
        String pageContent = Redis.pageContent.get(id);
 
        //step 2 根据获取结果 更改状态
        if(pageContent == null){
            pageContent = "页面开始渲染,请再等500ms后重试";
        }
 
        //step 3 状态改为渲染中,并启动一个线程模拟渲染
        Redis.pageSate.put(id, Context.renderIngState);
        Thread renderpage = new Thread(new RenderPage(id));
        renderpage.start();
 
        return pageContent;
    }
}
 
//模拟页面渲染线程
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 {
            //模拟页面渲染需要500ms
            Thread.sleep(500);
            if(rnd.nextBoolean()){//模拟50%的机会渲染失败
                Redis.pageSate.put(id, Context.renderSuccessState);
                Redis.pageContent.put(id,"正常页面内容");
            }else{
                Redis.pageSate.put(id, Context.offLineState);
            }
 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 

 

这里状态变更为渲染中renderIngState。采用一个线程 模拟渲染过程,有一半的几率渲染成功。

 

RenderIngState(渲染中),模拟渲染动作在RenderPage线程里已经做了,这个状态实现只有“返回内容”:

public class RenderIngState extends State {
    @Override
    public String getPageContent(String id) {
        //step 1 根据页面type、id从页面内容从缓存获取,省略实现
        String pageContent = Redis.pageContent.get(id);
 
        //step 2 根据获取结果 更改状态
        if(pageContent == null){
            pageContent = "页面正在渲染中,请再等500ms后重试";
        }
        return pageContent;
    }
}
 

 

RenderSuccessState(渲染成功) 状态变化:检查举例上次渲染时间是否超过5分钟,如果超过 状态变为初始状态等待重新被渲染。返回内容,最新的页面内容。

public class RenderSuccessState extends State {
 
    public String getPageContent(String id){
        //step1 从缓存获取页面内容
        String pageContent = Redis.pageContent.get(id);//页面渲染成功状态,页面
 
        //step2 启动一个线程 模拟页面5分钟 状态变为初始状态
        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();
        }
    }
}
 

 

这里采用一个线程模拟5分钟渲染过期,状态变为“初始状态”。实际开发中,可以使用redis的过期策略。

 

OffLineState(页面下线) 状态变化:如果被重新上线,状态变为初始状态,等待被重新渲染。返回内容:“页面已下线”。

 
public class OffLineState extends State {
    @Override
    public String getPageContent(String id) {
        //新开线程 模拟页面上线
        Thread online  = new Thread(new OnLine(id));
        online.start();
 
        return "页面已下线";
    }
}
 
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();
        }
    }
}

 

这里采用一个线程,模拟在500ms后触发上线操作,状态变更为初始状态

 

到这里,4个状态实现完毕。

 

Context上下文实现

public class Context {
 
    public static State initState = new InitState();//初始状态
    public static State renderIngState = new RenderIngState();//渲染中状态
    public static State offLineState = new OffLineState();//下线状态
    public static State renderSuccessState = new RenderSuccessState();//渲染成功状态
 
    public String getPage(String id){
        //获取当前状态
        State state = pageSate.get(id);
        if (state == null){
            state = initState;
            pageSate.put(id,state);//更新状态到缓存
        }
 
        return state.getPageContent(id);
    }
}

 

getPage实现:首先从redis中获取当前页面的状态,然后调用getPageContent方法获取页面内容即可。具体是执行哪个状态的getPageContent方法,Context本身不知道。假设有一天新增状态或者状态代码有修改,Context不需要做任何改动,这就是基于接口编程的福利。

 

Redis辅助类

public class Redis {
 
    //页面状态缓存
    public static Map<String,State> pageSate = new HashMap<String,State>();
 
    //页面内容缓存
    public static Map<String,String> pageContent = new HashMap<String,String>();
}
 

 

这里使用Hashmap模拟缓存,在多jvm实例部署的系统中 一般使用redis共享缓存。

 

测试代码:

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);
        }
    }
}
 

 

这里的Main类实现是模拟的客户端操作,可以看到客户端只需跟Context类打交道,这个页面内容获取的实现细节都已经被封装起来。

 

执行main方法,结果为:

页面开始渲染,请再等500ms后重试
页面已下线
页面开始渲染,请再等500ms后重试
页面已下线
页面开始渲染,请再等500ms后重试
正常页面内容
正常页面内容
页面已下线
正常页面内容
页面已下线
正常页面内容
页面已下线
正常页面内容
//省略无数行

本次示例实现过程完毕。

 

小结

 

状态模式是对状态变化和行为的封装,一定程度上满足“开闭原则”、面向接口编程原则单一责任原则等。

 

状态模式类图和策略模式相同,区别是策略模式只对行为进行封装;在Context上下文中,策略模式 需要根据具体业务改变“策略”,而状态模式的的Context一般不进行状态变化处理,状态变更操作被封装到每个状态实现中。

 

适用场景:对象存在多个状态,并且多个状态的变化有规律的成环状,此时就可以采用状态模式

猜你喜欢

转载自moon-walker.iteye.com/blog/2399825