【行为型模式二十】状态模式-2(State)

3.4 模拟工作流##

做企业应用的朋友,大多数都接触过工作流,至少处理过业务流程。当然对于工作流,复杂的应用可能会使用工作流中间件,用工作流引擎来负责流程处理,这个会比较复杂,其实工作流引擎的实现也可以应用上状态模式,这里不去讨论。

简单点的,把流程数据存放在数据库里面,然后在程序里面自己来进行流程控制。对于简单点的业务流程控制,可以使用状态模式来辅助进行流程控制,因为大部分这种流程都是状态驱动的

举个例子来说明吧,举个最常见的“请假流程”,流程是这样的:当某人提出请假申请过后,先由项目经理来审批,如果项目经理不同意,审批就直接结束;如果项目经理同意了,再看请假的天数是否超过3天,项目经理的审批权限只有3天以内,如果请假天数在3天以内,那么审批也直接结束,否则就提交给部门经理;部门经理审核过后,无论是否同意,审批都直接结束。流程图如图所示:

简单的请假流程示意图

在实际开发中,如果不考虑使用工作流软件,按照流程来自己实现的话,这个流程基本的运行过程简化描述如下:

  1. UI操作:请假人填写请假单,提出请假申请
  2. 后台处理:保存请假单数据到数据库中,然后为项目经理创建一个工作,把工作信息保存到数据库中
  3. UI操作:项目经理登录系统,获取自己的工作列表
  4. 后台处理:从数据库中获取相应的工作列表
  5. UI操作:项目经理完成审核工作,提交保存
  6. 后台处理:处理项目经理审核的业务,保存审核的信息到数据库。同时判断后续的工作,如果是需要人员参与的,就为参与下一个工作的人员创建工作,把工作信息保存到数据库中
  7. UI操作:部门经理登录系统,获取自己的工作列表,基本上是重复第3步
  8. 后台处理:从数据库中获取相应的工作列表,基本上是重复第4步
  9. UI操作:部门经理完成审核工作,提交保存,基本上是重复第5步
  10. 后台处理:类推,基本上是重复第6步
  1. 实现思路

仔细分析上面的流程图和运行过程,把请假单在流程中的各个阶段的状态分析出来,会发现,整个流程完全可以看成是状态驱动的

在上面的流程中,请假单大致有如下状态:等待项目经理审核、等待部门经理审核、审核结束。如果用状态驱动来描述上述流程:

当请假人填写请假单,提出请假申请后,请假单的状态是等待项目经理审核状态;

当项目经理完成审核工作,提交保存后,如果项目经理不同意,请假单的状态是审核结束状态;如果项目经理同意,请假天数又在3天以内,请假单的状态是审核结束状态;如果项目经理同意,请假天数大于3天,请假单的状态是等待部门经理审核状态;

当部门经理完成审核工作,提交保存后,无论是否同意,请假单的状态都是审核结束状态;

既然可以把流程看成是状态驱动的,那么就可以自然的使用上状态模式,每次当相应的工作人员完成工作,请求流程响应的时候,流程处理的对象会根据当前所处的状态,把流程处理委托给相应的状态对象去处理

又考虑到在一个系统中会有很多流程,虽然不像通用工作流那么复杂的设计,但还是稍稍提炼一下,至少把各个不同的业务流程,在应用状态模式时的公共功能,或者是架子给搭出来,以便复用这些功能。

(1)首先提供一个公共的状态处理机

相当于一个公共的状态模式的Context,在里面提供基本的、公共的功能,这样在实现具体的流程的时候,可以简单一些,对于要求不复杂的流程,甚至可以直接使用。示例代码如下:

/**
 * 公共状态处理机,相当于状态模式的Context
 * 包含所有流程使用状态模式时的公共功能
 */
public  class StateMachine {
    /**
     * 持有一个状态对象
     */
    private State state = null;
    /**
     * 包含流程处理需要的业务数据对象,不知道具体类型,为了简单,不去使用泛型,
     * 用Object,反正只是传递到具体的状态对象里面
     */
    private Object businessVO = null;
    /**
     * 执行工作,客户端处理流程的接口方法。
     * 在客户完成自己的业务工作后调用
     */
    public void doWork(){
       //转调相应的状态对象真正完成功能处理
       this.state.doWork(this);
    }
  
    public State getState() {
       return state;
    }
    public void setState(State state) {
       this.state = state;
    }
    public Object getBusinessVO() {
       return businessVO;
    }
    public void setBusinessVO(Object businessVO) {
       this.businessVO = businessVO;
    }
}

(2)来提供公共的状态接口,各个状态对象在处理流程的时候,可以使用统一的接口,那么它们需要的业务数据从何而来呢?那就通过上下文传递过来。示例代码如下:

/**
 * 公共状态接口
 */
public interface State {
    /**
     * 执行状态对应的功能处理
     * @param ctx 上下文的实例对象
     */
    public void doWork(StateMachine ctx);
}

好了,现在架子已经搭出来了,在实现具体的流程的时候,可以分别扩展它们,来加入跟具体流程相关的功能。

  1. 使用状态模式来实现流程

(1)定义请假单的业务数据模型,示例代码如下:

public class LeaveRequestModel {
    /**
     * 请假人
     */
    private String user;
    /**
     * 请假开始时间
     */
    private String beginDate;
    /**
     * 请假天数
     */
    private int leaveDays;
    /**
     * 审核结果
     */
    private String result;
  
    public String getResult() {
       return result;
    }
    public void setResult(String result) {
       this.result = result;
    }
    public String getUser() {
       return user;
    }
    public String getBeginDate() {
       return beginDate;
    }
    public int getLeaveDays() {
       return leaveDays;
    }
    public void setUser(String user) {
       this.user = user;
    }
    public void setBeginDate(String beginDate) {
       this.beginDate = beginDate;
    }
    public void setLeaveDays(int leaveDays) {
       this.leaveDays = leaveDays;
    }   
}

(2)定义处理客户端请求的上下文,虽然这里并不需要扩展功能,但还是继承一下状态机,表示可以添加自己的处理。示例代码如下:

public class LeaveRequestContext extends StateMachine{
    //这里可以扩展跟自己流程相关的处理
}

(3)来定义处理请假流程的状态接口,虽然这里并不需要扩展功能,但还是继承一下状态,表示可以添加自己的处理。示例代码如下:

public interface LeaveRequestState extends State{
    //这里可以扩展跟自己流程相关的处理
}

(4)接下来该来实现各个状态具体的处理对象了,先看看处理项目经理审核的状态类的实现,示例代码如下:

/**
 * 处理项目经理的审核,处理后可能对应部门经理审核、审核结束之中的一种
 */
public class ProjectManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
        //先把业务对象造型回来
        LeaveRequestModel lrm = (LeaveRequestModel)request.getBusinessVO();

        //业务处理,把审核结果保存到数据库中
     
        //根据选择的结果和条件来设置下一步
        if("同意".equals(lrm.getResult())){
            if(lrm.getLeaveDays() > 3){
                //如果请假天数大于3天,而且项目经理同意了,就提交给部门经理
                request.setState(new DepManagerState());
                //为部门经理增加一个工作
            }else{
                //3天以内的请假,由项目经理做主,
                //就不用提交给部门经理了,转向审核结束状态
                request.setState(new  AuditOverState());
                //给申请人增加一个工作,让他查看审核结果
            }
        }else{
            //项目经理不同意的话,也就不用提交给部门经理了,转向审核结束状态
            request.setState(new  AuditOverState());
         
            //给申请人增加一个工作,让他查看审核结果
        }         
    }  
}

接下来看看处理项目经理审核的状态类的实现,示例代码如下:

/**
 * 处理部门经理的审核,处理后对应审核结束状态
 */
public class DepManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把业务对象造型回来
       LeaveRequestModel lrm = (LeaveRequestModel)request.getBusinessVO();

       //业务处理,把审核结果保存到数据库中
     
       //部门经理审核过后,直接转向审核结束状态了
       request.setState(new AuditOverState());

       //给申请人增加一个工作,让他查看审核结果
    }
}

再来看看处理审核结束的状态类的实现,示例代码如下:

/**
 * 处理审核结束的类
 */
public class AuditOverState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把业务对象造型回来
       LeaveRequestModel lrm = (LeaveRequestModel)request.getBusinessVO();

       //业务处理,在数据里面记录整个流程结束   
    }
}

(5)由于上面的实现中,涉及到大量需要数据库支持的功能,同时还需要提供页面来让用户操作,才能驱动流程运行,所以无法像其它示例那样,写个客户端就能进行测试。当然这个可以在后面稍稍改变一下,模拟一下实现,就可以运行起来看效果了。

先来看看此时用状态模式实现的这个流程的程序结构示意图,如图所示:

用状态模式实现的流程的程序结构示意图
  1. 改进上面使用状态模式来实现流程的示例

上面的示例不能运行有两个基本原因:一是没有数据库实现部分,二是没有界面。要解决这个问题,那就采用字符界面,来让客户输入数据,另外把运行放到同一个线程里面,这样就不存在传递数据的问题,也就不需要保存数据了,数据在内存里面

原来是提交了请假申请,把数据保存在数据库里面,然后项目经理从数据库去获取这些数据。现在一步到位,直接把申请数据传递过去,就可以处理了。

(1)根据上面的思路,其实也就只是需要修改那几个状态处理对象的实现,先看看处理项目经理审核的状态类的实现,使用Scanner来接受命令行输入数据,示例代码如下:

import java.util.Scanner;
/**
 * 处理项目经理的审核,处理后可能对应部门经理审核、审核结束之中的一种
 */
public class ProjectManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把业务对象造型回来
       LeaveRequestModel lrm = (LeaveRequestModel)request.getBusinessVO();
       System.out.println("项目经理审核中,请稍候......");
       //模拟用户处理界面,通过控制台来读取数据
       System.out.println(lrm.getUser()+"申请从"+lrm.getBeginDate()+"开始请假"+lrm.getLeaveDays()+"天,请项目经理审核(1为同意,2为不同意):");
       //读取从控制台输入的数据
       Scanner scanner = new Scanner(System.in);
       if(scanner.hasNext()){
           int a = scanner.nextInt();
           //设置回到上下文中
           String result = "不同意";
           if(a==1){
              result = "同意";
           }
           lrm.setResult("项目经理审核结果:"+result);
           //根据选择的结果和条件来设置下一步
           if(a==1){
              if(lrm.getLeaveDays() > 3){
                  //如果请假天数大于3天,而且项目经理同意了,
                  //就提交给部门经理
                  request.setState(new DepManagerState());
                  //继续执行下一步工作
                  request.doWork();
              }else{
                  //3天以内的请假,由项目经理做主,就不用提交给部门经理了,
                  //转向审核结束状态
                  request.setState(new  AuditOverState());
                  //继续执行下一步工作
                  request.doWork();
              }            
           }else{
              //项目经理不同意,就不用提交给部门经理了,转向审核结束状态
              request.setState(new  AuditOverState());
              //继续执行下一步工作
              request.doWork();
           }
       }     
    }  
}

接下来看看处理项目经理审核的状态类的实现,示例代码如下:

import java.util.Scanner;
/**
 * 处理部门经理的审核,处理后对应审核结束状态
 */
public class DepManagerState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把业务对象造型回来
       LeaveRequestModel lrm = (LeaveRequestModel)request.getBusinessVO();
       System.out.println("部门经理审核中,请稍候......");
       //模拟用户处理界面,通过控制台来读取数据
       System.out.println(lrm.getUser()+"申请从"+lrm.getBeginDate()+"开始请假"+lrm.getLeaveDays()+"天,请部门经理审核(1为同意,2为不同意):");
       //读取从控制台输入的数据
       Scanner scanner = new Scanner(System.in);
       if(scanner.hasNext()){
           int a = scanner.nextInt();
           //设置回到上下文中
           String result = "不同意";
           if(a==1){
              result = "同意";
           }
           lrm.setResult("部门经理审核结果:"+result);
           //部门经理审核过后,直接转向审核结束状态了
           request.setState(new AuditOverState());
           //继续执行下一步工作
           request.doWork();
       }     
    }
}

再来看看处理审核结束的状态类的实现,示例代码如下:

public class AuditOverState implements LeaveRequestState{
    public void doWork(StateMachine request) {
       //先把业务对象造型回来
       LeaveRequestModel lrm = (LeaveRequestModel)request.getBusinessVO();
       System.out.println(lrm.getUser()+",你的请假申请已经审核结束,结果是:"+lrm.getResult());
    }
}

(2)万事俱备,可以写个客户端,来开始我们的流程之旅了。示例代码如下:

public class Client {
    public static void main(String[] args) {
       //创建业务对象,并设置业务数据
       LeaveRequestModel lrm = new LeaveRequestModel();
       lrm.setUser("小李");
       lrm.setBeginDate("2010-02-08");
       lrm.setLeaveDays(5);
     
       //创建上下文对象
       LeaveRequestContext request = new LeaveRequestContext();
       //为上下文对象设置业务数据对象
       request.setBusinessVO(lrm);
       //配置上下文,作为开始的状态,以后就不管了
       request.setState(new ProjectManagerState());
     
       //请求上下文,让上下文开始处理工作
       request.doWork();
    }
}

辛苦了这么久,一定要好好的运行一下,体会在流程处理中是如何使用状态模式的。

第一步:运行一下,刚开始会出现如下信息:

项目经理审核中,请稍候......
小李申请从2010-02-08开始请假5天,请项目经理审核(1为同意,2为不同意):

第二步:程序并没有停止,在等待你输入项目经理审核的结果,如果你输入1,表示同意,那么程序会继续判断,发现请假天数5天大于项目经理审核的范围了,会提交给部门经理审核。在控制台输入1,然后回车看看,会出现如下信息:

项目经理审核中,请稍候......
小李申请从2010-02-08开始请假5天,请项目经理审核(1为同意,2为不同意):
1
部门经理审核中,请稍候......
小李申请从2010-02-08开始请假5天,请部门经理审核(1为同意,2为不同意):

第三步:同样,程序仍然没有停止,在等待你输入部门经理审核的结果,假如输入1,然后回车,看看会发生什么,提示信息如下:

项目经理审核中,请稍候......
小李申请从2010-02-08开始请假5天,请项目经理审核(1为同意,2为不同意):
1
部门经理审核中,请稍候......
小李申请从2010-02-08开始请假5天,请部门经理审核(1为同意,2为不同意):
1
小李,你的请假申请已经审核结束,结果是:部门经理审核结果:同意

这个时候流程运行结束了,程序运行也结束了,有点流程控制的意味了吧。

如果在上面第一步运行过后,在第二步输入2,也就是项目经理不同意,会怎样呢?应该就不会再到部门经理了吧,试试看,运行提示信息如下:

项目经理审核中,请稍候......
小李申请从2010-02-08开始请假5天,请项目经理审核(1为同意,2为不同意):
2
小李,你的请假申请已经审核结束,结果是:项目经理审核结果:不同意
  1. 小结一下

事实上,上面的程序可以和数据库结合起来,比如把审核结果存放在数据库里面,也可以把审核的步骤也放到数据库里面,每次运行的时候从数据库里面获取这些值,然后来判断是创建哪一个状态处理类,然后执行相应的处理就可以了。

现在这些东西都在内存里,所以程序不能停止,否则流程就运行不下去了。

另外,为了演示的简洁性,这里做了相当的简化,比如没有去根据申请人选择相应的项目经理和部门经理,也没有去考虑如果申请人就是项目经理或者部门经理怎么办,只是为了让大家看明白状态模式在这里面的应用,主要是为了体现状态模式而不是业务。

3.5 状态模式的优缺点##

  1. 简化应用逻辑控制

状态模式使用单独的类来封装一个状态的处理。如果把一个大的程序控制分成很多小块,每块定义一个状态来代表,那么就可以把这些逻辑控制的代码分散到很多单独的状态类当中去,这样就把着眼点从执行状态提高到整个对象的状态,使得代码结构化和意图更清晰,从而简化应用的逻辑控制。

对于依赖于状态的if-else,理论上来讲,也可以改变成应用状态模式来实现,把每个if或else块定义一个状态来代表,那么就可以把块内的功能代码移动到状态处理类去了,从而减少if-else,避免出现巨大的条件语句

  1. 更好的分离状态和行为

状态模式通过设置所有状态类的公共接口,把状态和状态对应的行为分离开来,把所有与一个特定的状态相关的行为都放入一个对象中,使得应用程序在控制的时候,只需要关心状态的切换,而不用关心这个状态对应的真正处理。

  1. 更好的扩展性

引入了状态处理的公共接口后,使得扩展新的状态变得非常容易,只需要新增加一个实现状态处理的公共接口的实现类,然后在进行状态维护的地方,设置状态变化到这个新的状态即可。

  1. 显式化进行状态转换

状态模式为不同的状态引入独立的对象,使得状态的转换变得更加明确。而且状态对象可以保证上下文不会发生内部状态不一致的情况,因为上下文中只有一个变量来记录状态对象,只要为这一个变量赋值就可以了。

  1. 引入太多的状态类

状态模式也有一个很明显的缺点,一个状态对应一个状态处理类,会使得程序引入太多的状态类,使程序变得杂乱。

3.6 思考状态模式##

  1. 状态模式的本质

状态模式的本质:根据状态来分离和选择行为。

仔细分析状态模式的结构,如果没有上下文,那么就退化回到只有接口和实现了,正是通过接口,把状态和状态对应的行为分开,才使得通过状态模式设计的程序易于扩展和维护。
而上下文主要负责的是公共的状态驱动,每当状态发生改变的时候,通常都是回调上下文来执行状态对应的功能。当然,上下文自身也可以维护状态的变化,另外,上下文通常还会作为多个状态处理类之间的数据载体,在多个状态处理类之间传递数据。

  1. 何时选用状态模式

建议在如下情况中,选用状态模式:

如果一个对象的行为取决于它的状态,而且它必须在运行时刻根据状态来改变它的行为。可以使用状态模式,来把状态和行为分离开,虽然分离开了,但状态和行为是有对应关系的,可以在运行期间,通过改变状态,就能够调用到该状态对应的状态处理对象上去,从而改变对象的行为。

如果一个操作中含有庞大的多分支语句,而且这些分支依赖于该对象的状态。可以使用状态模式,把各个分支的处理分散包装到单独的对象处理类里面,这样,这些分支对应的对象就可以不依赖于其它对象而独立变化了。

3.7 相关模式##

  1. 状态模式和策略模式

这是两个结构相同,功能各异的模式,具体的在策略模式里面讲过了,这里就不再赘述了。

  1. 状态模式和观察者模式

这两个模式乍一看,功能是很相似的,但是又有区别,可以组合使用。

这两个模式都是在状态发生改变的时候触发行为,只不过观察者模式的行为是固定的,那就是通知所有的观察者,而状态模式是根据状态来选择不同的处理

从表面来看,两个模式功能相似,观察者模式中的被观察对象就好比状态模式中的上下文,观察者模式中当被观察对象的状态发生改变的时候,触发的通知所有观察者的方法;就好比是状态模式中,根据状态的变化,选择对应的状态处理。

但实际这两个模式是不同的,观察者模式的目的是在被观察者的状态发生改变的时候,触发观察者联动,具体如何处理观察者模式不管;而状态模式的主要目的在于根据状态来分离和选择行为,当状态发生改变的时候,动态改变行为

这两个模式是可以组合使用的,比如在观察者模式的观察者部分,当被观察对象的状态发生了改变,触发通知了所有的观察者过后,观察者该怎么处理呢?这个时候就可以使用状态模式,根据通知过来的状态选择相应的处理。

  1. 状态模式和单例模式

这两个模式可以组合使用,可以把状态模式中的状态处理类实现成单例。

  1. 状态模式和享元模式

这两个模式可以组合使用。

由于状态模式把状态对应的行为分散到多个状态对象中,会造成很多细粒度的状态对象,可以把这些状态处理对象通过享元模式来共享,从而节省资源。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容