欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-03 21:39:38
...

关于状态的案例

 

在日常开发中经常会遇到 一个对象有多种状态的情形,并且在不同的状态下需要执行的动作会不同。很多朋友一般会采用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一般不进行状态变化处理,状态变更操作被封装到每个状态实现中。

 

 

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

  • 状态模式--封装状态的变化
            
    
    博客分类: 设计模式 状态模式 
  • 大小: 41.2 KB
  • 状态模式--封装状态的变化
            
    
    博客分类: 设计模式 状态模式 
  • 大小: 50.2 KB
  • 状态模式--封装状态的变化
            
    
    博客分类: 设计模式 状态模式 
  • 大小: 6.7 KB
相关标签: 状态模式