状态模式--封装状态的变化
关于状态的案例
在日常开发中经常会遇到 一个对象有多种状态的情形,并且在不同的状态下需要执行的动作会不同。很多朋友一般会采用if elseif else语句进行判断不同的状态,对匹配到的不同状态进行不同的业务逻辑处理。这样所有的业务逻辑代码都被融合在一起,不符合“开闭原则”验重影响代码的可读性,以及将来代码的维护(比如新增状态)。
下面来看一个笔者遇到的真实案例,在设计一个“页面浏览”的web服务技术架构实现时,由于页面渲染时间较长,为了防止高并发情况下的阻塞,采用了页面异步渲染的方式:每次页面请求都从redis缓存中获取已渲染好的页面内容返回,如果缓存状态过期时,只允许发起一次页面渲染,在页面渲染过程中,如果有其他请求进来 也会直接从redis中获取老缓存内容返回:
通过这种“异步页面渲染”方式处理,就能保证每次都从redis缓存获取页面内容,减少不必要的页面渲染。但同时页面内容有可能发生变化,这里可以每隔5分钟对页面重新渲染一次。这个过程中 如果把页面看做是对象,就会存在几种状态:
1、初始状态,状态变化:此时页面还没有渲染,如果此时请求页面,会进入”渲染中”状态。返回内容:如果redis中有缓存(上次渲染的)直接返回,如果没有,返回“等待重试”;
2、渲染中,状态变化:如果渲染完成会进入“渲染成功”(往redis中推送最新页面内容)或者“页面下线”状态。返回内容:如果redis中有缓存(上次渲染的)直接返回,如果没有,返回内容:“等待渲染中”;
3、渲染成功,状态变化:检查举例上次渲染时间是否超过5分钟,如果超过 状态变为“初始状态” 等待重新被渲染。返回内容,最新的页面内容。
4、页面下线,状态变化:如果被重新上线,状态变为“初始状态”,等待被重新渲染。返回内容:“页面已下线”。
用状态图来表示如下:
假设4个状态分别用0、1、2、3表示,最常见的实现方式就是
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一般不进行状态变化处理,状态变更操作被封装到每个状态实现中。
适用场景:对象存在多个状态,并且多个状态的变化有规律的成环状,此时就可以采用“状态模式”。
上一篇: jQuery live绑定的事件与解除绑定的实例详解
下一篇: 状态模式——文档编辑模式切换