3.4 模拟工作流##
做企业应用的朋友,大多数都接触过工作流,至少处理过业务流程。当然对于工作流,复杂的应用可能会使用工作流中间件,用工作流引擎来负责流程处理,这个会比较复杂,其实工作流引擎的实现也可以应用上状态模式
,这里不去讨论。
简单点的,把流程数据存放在数据库里面,然后在程序里面自己来进行流程控制。对于简单点的业务流程控制,可以使用状态模式来辅助进行流程控制,因为大部分这种流程都是状态驱动的
。
举个例子来说明吧,举个最常见的“请假流程”,流程是这样的:当某人提出请假申请过后,先由项目经理来审批,如果项目经理不同意,审批就直接结束;如果项目经理同意了,再看请假的天数是否超过3天,项目经理的审批权限只有3天以内,如果请假天数在3天以内,那么审批也直接结束,否则就提交给部门经理;部门经理审核过后,无论是否同意,审批都直接结束。流程图如图所示:
在实际开发中,如果不考虑使用工作流软件,按照流程来自己实现的话,这个流程基本的运行过程简化描述如下:
- UI操作:请假人填写请假单,提出请假申请
- 后台处理:保存请假单数据到数据库中,然后为项目经理创建一个工作,把工作信息保存到数据库中
- UI操作:项目经理登录系统,获取自己的工作列表
- 后台处理:从数据库中获取相应的工作列表
- UI操作:项目经理完成审核工作,提交保存
- 后台处理:处理项目经理审核的业务,保存审核的信息到数据库。同时判断后续的工作,如果是需要人员参与的,就为参与下一个工作的人员创建工作,把工作信息保存到数据库中
- UI操作:部门经理登录系统,获取自己的工作列表,基本上是重复第3步
- 后台处理:从数据库中获取相应的工作列表,基本上是重复第4步
- UI操作:部门经理完成审核工作,提交保存,基本上是重复第5步
- 后台处理:类推,基本上是重复第6步
- 实现思路
仔细分析上面的流程图和运行过程,把请假单在流程中的各个阶段的状态分析出来,会发现,整个流程完全可以看成是状态驱动的
。
在上面的流程中,请假单大致有如下状态:等待项目经理审核、等待部门经理审核、审核结束。如果用状态驱动来描述上述流程:
当请假人填写请假单,提出请假申请后,请假单的状态是等待项目经理审核状态;
当项目经理完成审核工作,提交保存后,如果项目经理不同意,请假单的状态是审核结束状态;如果项目经理同意,请假天数又在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)定义请假单的业务数据模型,示例代码如下:
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)根据上面的思路,其实也就只是需要修改那几个状态处理对象的实现,先看看处理项目经理审核的状态类的实现,使用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
小李,你的请假申请已经审核结束,结果是:项目经理审核结果:不同意
- 小结一下
事实上,上面的程序可以和数据库结合起来,比如把审核结果存放在数据库里面,也可以把审核的步骤也放到数据库里面,每次运行的时候从数据库里面获取这些值,然后来判断是创建哪一个状态处理类,然后执行相应的处理就可以了。
现在这些东西都在内存里,所以程序不能停止,否则流程就运行不下去了。
另外,为了演示的简洁性,这里做了相当的简化,比如没有去根据申请人选择相应的项目经理和部门经理,也没有去考虑如果申请人就是项目经理或者部门经理怎么办,只是为了让大家看明白状态模式在这里面的应用,主要是为了体现状态模式而不是业务。
3.5 状态模式的优缺点##
- 简化应用逻辑控制
状态模式使用单独的类来封装一个状态的处理
。如果把一个大的程序控制分成很多小块,每块定义一个状态来代表,那么就可以把这些逻辑控制的代码分散到很多单独的状态类当中去,这样就把着眼点从执行状态提高到整个对象的状态,使得代码结构化和意图更清晰,从而简化应用的逻辑控制。
对于依赖于状态的if-else,理论上来讲,也可以改变成应用状态模式来实现,把每个if或else块定义一个状态来代表,那么就可以把块内的功能代码移动到状态处理类去了,从而减少if-else,避免出现巨大的条件语句
。
- 更好的分离状态和行为
状态模式通过设置所有状态类的公共接口,把状态和状态对应的行为分离开来,把所有与一个特定的状态相关的行为都放入一个对象中
,使得应用程序在控制的时候,只需要关心状态的切换,而不用关心这个状态对应的真正处理。
- 更好的扩展性
引入了状态处理的公共接口后,使得扩展新的状态变得非常容易,只需要新增加一个实现状态处理的公共接口的实现类,然后在进行状态维护的地方,设置状态变化到这个新的状态即可。
- 显式化进行状态转换
状态模式为不同的状态引入独立的对象,使得状态的转换变得更加明确。而且状态对象可以保证上下文不会发生内部状态不一致的情况,因为上下文中只有一个变量来记录状态对象,只要为这一个变量赋值就可以了。
- 引入太多的状态类
状态模式也有一个很明显的缺点,一个状态对应一个状态处理类,会使得程序引入太多的状态类,使程序变得杂乱。
3.6 思考状态模式##
- 状态模式的本质
状态模式的本质:根据状态来分离和选择行为。
仔细分析状态模式的结构,如果没有上下文,那么就退化回到只有接口和实现了,正是通过接口,把状态和状态对应的行为分开,才使得通过状态模式设计的程序易于扩展和维护。
而上下文主要负责的是公共的状态驱动,每当状态发生改变的时候,通常都是回调上下文来执行状态对应的功能
。当然,上下文自身也可以维护状态的变化,另外,上下文通常还会作为多个状态处理类之间的数据载体,在多个状态处理类之间传递数据。
- 何时选用状态模式
建议在如下情况中,选用状态模式:
如果一个对象的行为取决于它的状态,而且它必须在运行时刻根据状态来改变它的行为
。可以使用状态模式,来把状态和行为分离开,虽然分离开了,但状态和行为是有对应关系的,可以在运行期间,通过改变状态,就能够调用到该状态对应的状态处理对象上去,从而改变对象的行为。
如果一个操作中含有庞大的多分支语句,而且这些分支依赖于该对象的状态
。可以使用状态模式,把各个分支的处理分散包装到单独的对象处理类里面,这样,这些分支对应的对象就可以不依赖于其它对象而独立变化了。
3.7 相关模式##
- 状态模式和策略模式
这是两个结构相同,功能各异的模式,具体的在策略模式里面讲过了,这里就不再赘述了。
- 状态模式和观察者模式
这两个模式乍一看,功能是很相似的,但是又有区别,可以组合使用。
这两个模式都是在状态发生改变的时候触发行为,只不过观察者模式的行为是固定的,那就是通知所有的观察者,而状态模式是根据状态来选择不同的处理
。
从表面来看,两个模式功能相似,观察者模式中的被观察对象就好比状态模式中的上下文,观察者模式中当被观察对象的状态发生改变的时候,触发的通知所有观察者的方法
;就好比是状态模式中,根据状态的变化,选择对应的状态处理。
但实际这两个模式是不同的,观察者模式的目的是在被观察者的状态发生改变的时候,触发观察者联动,具体如何处理观察者模式不管
;而状态模式的主要目的在于根据状态来分离和选择行为,当状态发生改变的时候,动态改变行为
。
这两个模式是可以组合使用的,比如在观察者模式的观察者部分,当被观察对象的状态发生了改变,触发通知了所有的观察者过后,观察者该怎么处理呢?这个时候就可以使用状态模式,根据通知过来的状态选择相应的处理。
- 状态模式和单例模式
这两个模式可以组合使用,可以把状态模式中的状态处理类实现成单例。
- 状态模式和享元模式
这两个模式可以组合使用。
由于状态模式把状态对应的行为分散到多个状态对象中,会造成很多细粒度的状态对象,可以把这些状态处理对象通过享元模式来共享,从而节省资源。