关于状态的案例
在日常开发中经常会遇到 一个对象有多种状态的情形,并且在不同的状态下需要执行的动作会不同。很多朋友一般会采用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一般不进行状态变化处理,状态变更操作被封装到每个状态实现中。
适用场景:对象存在多个状态,并且多个状态的变化有规律的成环状,此时就可以采用“状态模式”。
相关推荐
状态模式将这些状态和行为封装到不同的类中,使得代码更易于维护和扩展。 1. **状态类接口**: 在这个实现中,首先会有一个`State`接口,定义了审批流程中的通用操作,如提交、一级审批、二级审批等。这样,所有...
- 封装了转换规则:状态模式将与特定状态相关的行为封装在各自的状态对象中,使得转换规则被隐藏,降低了系统的复杂性。 - 对象行为的扩展更加灵活:只需添加新的具体状态类和状态转换逻辑,而无需修改现有代码。 - ...
状态模式将与特定状态相关的行为封装到表示该状态的对象中,使得对象可以在运行时改变其行为。 在Python和C++这两种语言中,状态模式的实现方式略有不同。在Python中,由于其动态类型的特性,实现起来相对简洁。在...
- **状态模式**:关注的是对象内部状态的变化,状态的改变会导致行为的变化,且状态之间的转换通常是隐式的。 - **策略模式**:关注的是算法的选择,用户可以根据需要选择不同的算法来解决问题,状态之间的转换是...
状态模式将状态封装成独立的类,并将请求委托给当前状态对象,从而实现状态的切换和状态行为的变化。 状态模式的优点: * 可以将状态和行为分离,使得对象的行为可以随着状态的改变而改变,从而实现更加灵活的设计...
状态模式是软件设计模式中的行为型模式之一,其核心思想是在对象内部状态改变时,允许其改变行为,而看起来像是改变了对象所属的类。状态模式特别适用于以下情况:一个对象的行为取决于其状态,并且该对象必须在运行...
状态模式是一种行为设计模式,它允许对象在内部状态改变时改变其行为,看起来好像对象改变了它的类。在Java中,状态模式通常通过定义不同的状态类和一个上下文类来实现,其中上下文类持有状态对象并调用其方法来响应...
【服务器状态监控-状态模式】 在IT行业中,服务器状态监控是一项至关重要的任务,它确保了系统的稳定运行,能够及时发现并解决潜在问题。状态模式是一种行为设计模式,它允许对象在内部状态改变时改变它的行为,使...
这种情况下,可以使用状态模式将这些分支逻辑封装到不同的状态类中,从而简化主逻辑的实现。 #### 五、效果分析 1. **将与特定状态相关的行为局部化**,并将不同状态的行为分割开来。这有助于保持代码的清晰度,...
状态模式的核心思想是将状态相关的操作封装在状态对象中,使得环境类可以与具体状态类解耦。当环境类的状态发生改变时,只需要改变状态对象,而不需要修改环境类的代码。 在实现状态模式时,通常会有一个状态切换的...
在状态模式中,一个对象的状态变化会导致其行为的变化。这种模式常用于处理具有多种状态的对象,且不同状态下对象的行为差异较大。通过将每种状态封装为一个独立的类,我们可以让对象在其内部状态改变时改变它的行为...
总结起来,"16状态模式.zip"中的内容旨在阐述和演示如何利用状态模式来管理对象的行为,通过实例化不同的状态对象来实现对象在运行时的行为变化。通过对这个模式的深入理解和实践,我们可以更好地处理那些依赖于状态...
这款充电器在充电模式下,通过外部设定电阻器(RPROG)设定充电电流,并在待机模式、停机模式、睡眠模式等状态下具有不同的电流消耗表现。其涓流充电特性、涓流充电门限电压、涓流充电迟滞电压、VCC欠压闭锁门限、...
状态模式是一种行为设计模式,它使对象能够在运行时改变其行为,看起来好像它改变了它的类。在PHP中,状态模式通常用于处理...在实际项目中,尤其是在处理有多种状态变化的对象时,状态模式是一个非常实用的设计模式。
1. **封装状态转换逻辑**:状态模式将与特定状态相关的操作封装在各自的类中,使得代码结构清晰,易于维护。 2. **增加新的状态和转换**:由于状态和转换都定义在独立的类中,因此可以轻松地添加新的状态和转换,而...
- 状态模式:允许对象在其内部状态改变时改变它的行为,看起来好像对象改变了它的类。 - 模板方法模式:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。 - 访问者模式:表示一个作用于某对象结构中的各...
总结来说,状态模式是解决状态变化导致行为变化问题的有效工具,它将状态和行为封装在一起,使得代码更易于理解、扩展和维护。在Android开发中,尤其是在处理复杂逻辑和状态转换时,状态模式的应用能够大大提高代码...
总结来说,状态模式在TCP连接案例中的应用展示了如何利用设计模式来管理复杂对象的行为变化。通过将TCP连接的不同阶段抽象为独立的状态类,我们可以更好地组织代码,提高代码的可读性和可扩展性。`StateTest`文件...
状态模式的核心动机在于解决对象状态变化导致的行为变化问题。当一个对象的行为依赖于其内部状态(如用户的权限级别、账户的状态等),并且这些状态是动态变化的,那么直接在对象中用条件语句(如if-else)来控制...
- 封装了转换规则:状态模式把状态转换的逻辑封装在具体状态对象里,降低了系统的复杂性。 - 使状态转换逻辑独立:每种状态对象都有自己的行为,不会互相影响。 - 易于扩展:添加新的状态或状态转换非常方便,只...