`

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

阅读更多

关于状态的案例

 

在日常开发中经常会遇到 一个对象有多种状态的情形,并且在不同的状态下需要执行的动作会不同。很多朋友一般会采用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
0
1
分享到:
评论

相关推荐

    设计模式--C++

    5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型模式 208 5.10 TEMPLATE METHOD(模板方法)—类行为型模式 214 5.11 VISITOR(访问者)—对象行为型模式 218 5.12 行为模式的讨论 228 ...

    design-pattern-java.pdf

    处理对象的多种状态及其相互转换——状态模式(五) 处理对象的多种状态及其相互转换——状态模式(六) 策略模式-Strategy Pattern 算法的封装与切换——策略模式(一) 算法的封装与切换——策略模式(二) 算法的...

    java设计模式

    26.3.4 状态模式的注意事项 26.4 最佳实践 第27章 解释器模式 27.1 四则运算你会吗 27.2 解释器模式的定义 27.3 解释器模式的应用 27.3.1 解释器模式的优点 27.3.2 解释器模式的缺点 27.3.3 解释器模式使用的场景 ...

    ActionScript 3.0设计模式扫描版_方红琴译

    第10章 状态模式 用来创建一个状态机器的设计模式 状态设计模式用到的主要(30P概念 最小抽象状态设计模式 视频播放器具体状态应用程序 扩展状态设计:添加状态 添加更多的状态和流媒体播放能力 小结 第11章 ...

    23种设计模式入门到精通详解.txt

    状态模式:允许一个对象在其对象内部状态改变时改变它的行为。 观察者模式:对象间的一对多的依赖关系。 备忘录模式:在不破坏封装的前提下,保持对象的内部状态。 中介者模式:用一个中介对象来封装一系列的对象...

    学习php设计模式 php实现状态模式

    状态模式变化的位置在于对象的状态 二、状态模式结构图   三、状态模式中主要角色 抽象状态(State)角色:定义一个接口,用以封装环境对象的一个特定的状态所对应的行为 具体状态(ConcreteState)角色:每一个具体...

    设计模式可复用面向对象软件的基础.zip

    5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型 模式 208 5.10 TEMPLATE METHOD(模板方法) —类行为型模式 214 5.11 VISITOR(访问者)—对象行为型 模式 218 5.12 行为模式的讨论 228 ...

    设计模式:可复用面向对象软件的基础--详细书签版

    5.8 state(状态)—对象行为型模式 201 5.9 strategy(策略)—对象行为型 模式 208 5.10 template method(模板方法) —类行为型模式 214 5.11 visitor(访问者)—对象行为型 模式 218 5.12 行为模式的...

    javascript设计模式 – 状态模式原理与用法实例分析

    介绍:状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。状态模式将一个对象的状态从该对象中分离出来,使得对象状态可以灵活变化。 定义:允许一个对象在其内部状态改变时改变它的行为,...

    Java设计模式 版本2

    对象间的联动——观察者模式,处理对象的多种状态及其相互转换——状态模式,算法的封装与切换——策略模式,模板方法模式深度解析,操作复杂对象结构——访问者模式,设计模式与足球,多人联机射击游戏中的设计模式...

    黑马程序员 安卓学院 万元哥项目经理 分享220个代码实例

    |--Menu之不同模式下显示不同菜单 |--openGL-ES上绘制文字 |--openGL-ES纹理贴图 |--openGL-ES获取帧率 |--openGL-ES雾化 |--PopupWindow的使用 |--PopupWindow的返回健关闭 |--RadioGroup的用法(里面的成员可以是...

    《设计模式》中文版(23个设计模式的介绍与运用)

    5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型 模式 208 5.10 TEMPLATE METHOD(模板方法) —类行为型模式 214 5.11 VISITOR(访问者)—对象行为型 模式 218 5.12 行为模式的讨论 228 ...

    软件设计师必读的书-设计模式

    5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型 模式 208 5.10 TEMPLATE METHOD(模板方法) —类行为型模式 214 5.11 VISITOR(访问者)—对象行为型 模式 218 5.12 行为模式的讨论 228 ...

    design_patterns:我最常见的设计模式示例

    状态模式-对象的内部状态更改时,其行为也会更改。 因此,似乎对象类已更改。 我们通过为每个状态创建具体对象来实现此模式。 它们都实现相同的接口。 因此,当行为需要更改时,我们只需将一个具体实例(状态)与另...

    C#23种设计模式_示例源代码及PDF

    桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关 桥梁模式 联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是 继承关系,从而使两者可以独立的...

    Delphi模式编程第一分卷

    1.3.3 借助模式封装多个变化 1.3.4 模式帮助我们解决问题 第2章 Delphi的模式编程机制 2.1 对象模型机制 2.1.1 对象模型 2.1.2 对象建模和模式编程 2.1.3 对象关系与复用 2.2 动态绑定机制 2.2.1 方法绑定 ...

    设计模式(.PDF)

    5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型 模式 208 5.10 TEMPLATE METHOD(模板方法) —类行为型模式 214 5.11 VISITOR(访问者)—对象行为型 模式 218 5.12 行为模式的讨论 228 ...

    GOLF设计模式(C++语言版)

    5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型 模式 208 5.10 TEMPLATE METHOD(模板方法) —类行为型模式 214 5.11 VISITOR(访问者)—对象行为型 模式 218 5.12 行为模式的...

    Delphi模式编程第二分卷

    1.3.3 借助模式封装多个变化 1.3.4 模式帮助我们解决问题 第2章 Delphi的模式编程机制 2.1 对象模型机制 2.1.1 对象模型 2.1.2 对象建模和模式编程 2.1.3 对象关系与复用 2.2 动态绑定机制 2.2.1 ...

    [2010.10.14][封装工具][天空作品] Easy Sysprep v3 RC3(+ SkySRS3.00)

    [2010.10.14][封装工具][天空作品] Easy Sysprep v3 RC3(+ SkySRS3.00) 来源:自由天空技术论坛,原文链接:http://sky123.org/thread-26640-1-1.html Easy Sysprep v3 简介 1、欢迎使用 (1)ES3目前支持WinXP ...

Global site tag (gtag.js) - Google Analytics