状态模式3

  • Post author:
  • Post category:其他


18.3  模式讲解

18.3.1  认识状态模式


(1


)状态和行为

所谓对象的状态,通常指的就是对象实例的属性的值;而行为指的就是对象的功能,再具体点说,行为多半可以对应到方法上。

状态模式的功能就是分离状态的行为,通过维护状态的变化,来调用不同的状态对应的不同的功能。

也就是说,状态和行为是相关联的,它们的关系可以描述为:

状态决定行为

由于状态是在运行期被改变的,因此行为也会在运行期,根据状态的改变而改变,看起来,同一个对象,在不同的运行时刻,行为是不一样的,就像是类被修改了一样。


(2


)行为的平行性

注意是平行性而不是平等性。所谓平行性指的是各个状态的行为所处的层次是一样的,相互是独立的、没有关联的,是根据不同的状态来决定到底走平行线的那一条,行为是不同的,当然对应的实现也是不同的,相互之间是不可替换的。如图18.3所示:

图18.3  状态的平行性示意图

而平等性强调的是可替换性,大家是同一行为的不同描述或实现,因此在同一个行为发生的时候,可以根据条件来挑选任意一个实现来进行相应的处理。如图18.4所示:

图18.4  平等性的示意图

大家可能会发现状态模式的结构和策略模式的结构完全一样,但是,它们的目的、实现、本质都是完全不一样的。这个行为之间的特性也是状态模式和策略模式一个很重要的区别,状态模式的行为是平行性的,不可相互替换的;而策略模式的行为是平等性的,是可以相互替换的。


(3


)上下文和状态处理对象

在状态模式中,上下文是持有状态的对象,但是上下文自身并不处理跟状态相关的行为,而是把处理状态的功能委托给了状态对应的状态处理类来处理。

在具体的状态处理类里面经常需要获取上下文自身的数据,甚至在必要的时候会回调上下文的方法,因此,通常将上下文自身当作一个参数传递给具体的状态处理类。

客户端一般只和上下文交互,客户端可以用状态对象来配置一个上下文,一旦配置完毕,就不再需要和状态对象打交道了,客户端通常不负责运行期间状态的维护,也不负责决定到底后续使用哪一个具体的状态处理对象。


(4


)不完美的OCP


体验

好了,已经使用状态模式来重写了前面的示例,那么到底能不能解决前面提出的问题呢?也就是修改和扩展方不方便呢?一起来看一下。

先看修改已有的功能吧,由于现在每个状态对应的处理已经封装到对应的状态类里面了,要修改已有的某个状态的功能,直接扩展某个类进行修改就好了,对其它的程序没有影响。比如:现在要修改正常投票状态对应的功能,对于正常投票的用户给予积分奖励,那么只需要扩展正常投票状态对应的类,然后进行修改,示例代码如下:

public class NormalVoteState2 extends NormalVoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//先调用已有的功能

super.vote(user, voteItem, voteManager);

//给予积分奖励,示意一下

System.

out

.println(“奖励积分10分”);

}

}

一切良好,对吧,可是怎么让VoteManager能使用这个新的实现类呢?按照目前的实现,没有办法,只好去修改VoteManager的vote()方法中对状态的维护代码了,把使用NormalVoteState的地方换成使用NormalVoteState2。

再看看如何添加新的功能,比如投票超过8次但不足10次的,给个机会,只是禁止登录和使用系统3天,如果再犯,才进入黑名单。要实现这个功能,先要对原来的投票超过8次进入黑名单的功能进行修改,修改成投票超过10次才进入黑名单;然后新加入一个功能,实现超过8次但不足10次的,只是禁止登录和使用系统3天的功能。把这个新功能实现出来,示例代码如下:

public class BlackWarnVoteState implements VoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//待进黑名单警告状态

System.

out

.println(“禁止登录和使用系统3天”);

}

}

实现好了这个类,该怎样加入到已有的系统呢?

同样需要去修改上下文的vote()方法中对于状态判断和维护的代码,示例代码如下:

if(oldVoteCount==1){

state = new NormalVoteState2();

}else if(oldVoteCount>1 && oldVoteCount<5){

state = new RepeatVoteState();

}else if(oldVoteCount >= 5 && oldVoteCount<8){

state = new SpiteVoteState();

}else if(oldVoteCount>=8 && oldVoteCount<10){

state = new BlackWarnVoteState();

}else if(oldVoteCount>10){

state = new BlackVoteState();

}

好像也实现了功能是不是,而且改动起来确实也变得简单点了,但是仔细想想,是不是没有完全遵循OCP原则?结论是很显然的,

明显没有完全遵循


OCP


原则

这里要说明一点,设计原则是大家在设计和开发中尽量去遵守的,但不是一定要遵守,尤其是完全遵守,在实际开发中,完全遵守那些设计原则几乎是不可能完成的任务。

就像状态模式的实际实现中,由于状态的维护和转换在状态模式结构里面,不管你是扩展了状态实现类,还是新添加了状态实现类,都需要修改状态维护和转换的地方,以使用新的实现。

虽然可以有好几个地方来维护状态的变化,这个后面会讲到,但是都是在状态模式结构里面的,所以都有这个问题,算是不完美的OCP体验吧。


(5


)创建和销毁状态对象

在应用状态模式的时候,有一个常见的考虑,那就是:究竟何时创建和销毁状态对象。常见的有几个选择:

  • 一个是当需要使用状态对象的时候创建,使用完后就销毁它们
  • 另一个是提前创建它们并且始终不销毁
  • 还有一种是采用延迟加载和缓存合用的方式,就是当第一次需要使用状态对象的时候创建,使用完后并不销毁对象,而是把这个对象缓存起来,等待下一次使用,而且在合适的时候,会由缓存框架销毁状态对象

怎么选择呢?下面给出选择建议:

如果要进入的状态在运行时是不可知的,而且上下文是比较稳定的,不会经常改变状态,而且使用也不频繁,这个时候建议选第一种方案。

如果状态改变很频繁,也就是需要频繁的创建状态对象,而且状态对象还存储着大量的信息数据,这种情况建议选第二种方案。

如果无法确定状态改变是否频繁,而且有些状态对象的状态数据量大,有些比较小,一切都是未知的,建议选第三种方案。

事实上,在实际工程开发中,第三种方案是首选,因为它兼顾了前面两种方案的优点,而又避免了它们的缺点,几乎能适应各种情况的需要。只是这个方案在实现的时候,要实现一个合理的缓存框架,而且要考虑多线程并发的问题,因为需要由缓存框架来在合适的时候销毁状态对象,因此实现上难度稍高点。另外在实现中还可以考虑结合享元模式,通过享元模式来共享状态对象。


(6


)状态模式的调用顺序示意图

状态模式在实现上,对于状态的维护有不同的实现方式,前面的示例中,采用的是在Context中进行状态的维护和转换,这里就先画出这种方式的调用顺序示意图,其它的方式在后面讲到了再画。

在Context进行状态维护和转换的调用顺序示意图如图18.5所示:

图18.5  在Context进行状态维护和转换的调用顺序示意图

18.3.2  状态的维护和转换控制

所谓状态的维护,指的就是维护状态的数据,就是给状态设置不同的状态值;而状态的转换,指的就是根据状态的变化来选择不同的状态处理对象。在状态模式中,通常有两个地方可以进行状态的维护和转换控制。

一个就是在上下文当中,因为状态本身通常被实现为上下文对象的状态,因此可以在上下文里面进行状态维护,当然也就可以控制状态的转换了。前面投票的示例就是采用的这种方式。

另外一个地方就是在状态的处理类里面,当每个状态处理对象处理完自身状态所对应的功能后,可以根据需要指定后继的状态,以便让应用能正确处理后续的请求。

先看看示例,为了对比学习,就来看看如何把前面投票的例子修改成:在状态处理类里面进行后续状态的维护和转换。

(1)同样先来看投票状态的接口,没有变化,示例代码如下:

/**

* 封装一个投票状态相关的行为

*/

public interface VoteState {

/**

* 处理状态对应的行为

* @param user 投票人

* @param voteItem 投票项

* @param voteManager 投票上下文,用来在实现状态对应的功能处理的时候,

*                    可以回调上下文的数据

*/

public void vote(String user,String voteItem

,VoteManager voteManager);

}

(2)对于各个具体的状态实现对象,主要的变化在于:在处理完自己状态对应的功能后,还需要维护和转换状态对象。

一个一个来看吧,先看看正常投票的状态处理对象,示例代码如下:

public class NormalVoteState implements VoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//正常投票,记录到投票记录中

voteManager.getMapVote().put(user, voteItem);

System.

out

.println(“恭喜你投票成功”);

//正常投票完成,维护下一个状态,同一个人再投票就重复了

voteManager.getMapState().put(user,new RepeatVoteState());

}

}

接下来看看重复投票状态对应的处理对象,示例代码如下:

public class RepeatVoteState implements VoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//重复投票,暂时不做处理

System.

out

.println(“请不要重复投票”);

//重复投票完成,维护下一个状态,重复投票到5次,就算恶意投票了

//注意这里是判断大于等于4,因为这里设置的是下一个状态

//下一个操作次数就是5了,就应该算是恶意投票了

if(voteManager.getMapVoteCount().get(user) >= 4){

voteManager.getMapState().put(user,

new SpiteVoteState());

}

}

}

接下来看看恶意投票状态对应的处理对象,示例代码如下:

public class SpiteVoteState implements VoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//恶意投票,取消用户的投票资格,并取消投票记录

String s = voteManager.getMapVote().get(user);

if(s!=null){

voteManager.getMapVote().remove(user);

}

System.

out

.println(“你有恶意刷票行为,取消投票资格”);

//恶意投票完成,维护下一个状态,投票到8次,就进黑名单了

//注意这里是判断大于等于7,因为这里设置的是下一个状态

//下一个操作次数就是8了,就应该算是进黑名单了

if(voteManager.getMapVoteCount().get(user) >= 7){

voteManager.getMapState().put(user,

new BlackVoteState());

}

}

}

接下来看看黑名单状态对应的处理对象,没什么变化,示例代码如下:

public class BlackVoteState implements VoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//黑名单,记入黑名单中,禁止登录系统了

System.

out

.println(“进入黑名单,将禁止登录和使用本系统”);

}

}

(3)该来看看现在的投票管理类该如何实现了,跟在上下文中维护和转换状态相比,大致有如下的变化:

  • 需要按照每个用户来记录他们对应的投票状态,不同的用户,对应的投票状态是不同的,因此使用一个Map来记录,而不再是原来的一个单一的投票状态对象。


    可能有些朋友会问,那为什么前面的实现可以呢?那是因为投票状态是由投票管理对象集中控制的,不同的人员在进入投票方法的时候,是重新判断该人员具体的状态对象的,而现在是要把状态维护分散到各个状态类里面去,因此需要记录各个状态类判断过后的结果。
  • 需要把记录投票状态的数据,还有记录投票次数的数据,提供相应的getter方法,各个状态在处理的时候需要通过这些方法来访问数据。
  • 原来在vote()方法里面进行的状态控制和转换去掉,变成直接根据人员来从状态记录的Map中获取对应的状态对象了。

看看实现代码吧,示例代码如下:

public class VoteManager {

/**

* 记录当前每个用户对应的状态处理对象,每个用户当前的状态是不同的

* Map<String,VoteState>对应Map<用户名称,当前对应的状态处理对象>

*/

private Map<String,VoteState> mapState =

new HashMap<String,VoteState>();

/**

* 记录用户投票的结果,Map<String,String>对应Map<用户名称,投票的选项>

*/

private Map<String,String> mapVote =

new HashMap<String,String>();

/**

* 记录用户投票次数,Map<String,Integer>对应Map<用户名称,投票的次数>

*/

private Map<String,Integer> mapVoteCount =

new HashMap<String,Integer>();

/**

* 获取记录用户投票结果的Map

* @return 记录用户投票结果的Map

*/

public Map<String, String> getMapVote() {

return mapVote;

}

/**

* 获取记录每个用户对应的状态处理对象的Map

* @return 记录每个用户对应的状态处理对象的Map

*/

public Map<String, VoteState> getMapState() {

return mapState;

}

/**

* 获取记录每个用户对应的投票次数的Map

* @return 记录每个用户对应的投票次数的Map

*/

public Map<String, Integer> getMapVoteCount() {

return mapVoteCount;

}

/**

* 投票

* @param user 投票人,为了简单,就是用户名称

* @param voteItem 投票的选项

*/

public void vote(String user,String voteItem){

//1:先为该用户增加投票的次数

//先从记录中取出已有的投票次数

Integer oldVoteCount = mapVoteCount.get(user);

if(oldVoteCount==null){

oldVoteCount = 0;

}

oldVoteCount = oldVoteCount + 1;

mapVoteCount.put(user, oldVoteCount);

//2:获取该用户的投票状态

VoteState state = mapState.get(user);

//如果没有投票状态,说明还没有投过票,就初始化一个正常投票状态

if(state==null){

state = new NormalVoteState();

}

//然后转调状态对象来进行相应的操作

state.vote(user, voteItem, this);

}

}

(4)实现得差不多了,该来测试了,客户端没有变化,去运行一下,看看效果,看看两种维护状态变化的方式实现的结果一样吗?答案应该是一样的。




那么到底如何选择这两种方式呢?


  • 一般情况下,如果状态转换的规则是一定的,一般不需要进行什么扩展规则,那么就适合在上下文中统一进行状态的维护。

  • 如果状态的转换取决于前一个状态动态处理的结果,或者是依赖于外部数据,为了增强灵活性,这种情况下,一般是在状态处理类里面进行状态的维护。

(5)采用让状态对象来维护和转换状态的调用顺序示意图如图18.6所示:

图18.6  状态对象来维护和转换状态的调用顺序示意图

(6)再来看看这种实现方式下,如何修改已有的功能,或者是添加新的状态处理。

要修改已有的功能,同样是找到对应的状态处理对象,要么直接修改,要么扩展,前面已经示例过了,就不再赘述了。

对于添加新的状态处理的功能,这种实现方式会比较简单。先直接添加新的状态处理的类,然后去找到需要转换到这个新状态的状态处理类,修改那个处理类,让其转换到这个新状态就可以了。

比如还是来实现那个:投票超过8次但不足10次的,给个机会,只是禁止登录和使用系统3天,如果再犯,才进入黑名单的功能。按照现在的方式,示例代码如下:

public class BlackWarnVoteState implements VoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//待进黑名单警告状态

System.

out

.println(“禁止登录和使用系统3天”);

//待进黑名单警告处理完成,维护下一个状态,投票到10次,就进黑名单了

//注意这里是判断大于等于9,因为这里设置的是下一个状态

//下一个操作次数就是10了,就应该算是进黑名单了

if(voteManager.getMapVoteCount().get(user) >= 9){

voteManager.getMapState().put(user

, new BlackVoteState());

}

}

}

那么如何加入系统呢?

不再是去修改VoteManger了,而是找到应该转换到这个新状态的那个状态,修改它的状态维护和转换。应该是在恶意投票处理里面,让它转换到这个新的状态,也就是把恶意投票处理里面的下面这句话:

voteManager.getMapState().put(user, new BlackVoteState());

替换成:

voteManager.getMapState().put(user, new BlackWarnVoteState());

这样就自然的把现在新的状态处理添加到了已有的应用中。

18.3.3 使用数据库来维护状态

在实际开发中,还有一个方式来维护状态,那就是使用数据库,在数据库中存储下一个状态的识别数据,也就是说,维护下一个状态,演化成了维护下一个状态的识别数据,比如状态编码。

这样在程序中,通过查询数据库中的数据来得到状态编码,然后再根据状态编码来创建出相应的状态对象,然后再委托相应的状态对象进行功能处理。

还是用前面投票的示例来说明,如果使用数据库来维护状态的话,大致如何实现。

(1)首先,就是每个具体的状态处理类中,原本在处理完成后,要判断下一个状态是什么,然后创建下一个状态对象,并设置回到上下文中。

如果使用数据库的方式,那就不用创建下一个状态对象,也不用设置回到上下文中了,而是把下一个状态对应的编码记入数据库中,这样就可以了。还是示意一个,看看重复投票状态下的实现吧,示例代码如下:

public class RepeatVoteState implements VoteState{

public void vote(String user, String voteItem

, VoteManager voteManager) {

//重复投票,暂时不做处理

System.

out

.println(“请不要重复投票”);

//重复投票完成,维护下一个状态,重复投票到5次,就算恶意投票了

if(voteManager.getMapVoteCount().get(user) >= 4){




voteManager.getMapState().put(user,


new




SpiteVoteState());

//直接把下一个状态的编码记录入数据库就好了

}

}

}

这里只是示意一下,并不真的去写和数据库操作的代码。其它的状态实现类,也做同样类似的修改,就不去赘述了。

(2)在Context里面,也就是投票管理对象里面,就不需要那个记录所有用户状态的Map了,直接从数据库中获取该用户当前对应的状态编码,然后根据状态编码来创建出状态对象来。原有的示例代码如下:

//2:获取该用户的投票状态

VoteState state = mapState.get(user);

//如果没有投票状态,说明还没有投过票,就初始化一个正常投票状态

if(state==null){

state = new NormalVoteState();

}

现在被修改成,示例代码如下:

VoteState state = null;

//2:直接从数据库获取该用户对应的下一个状态的状态编码

String stateId = “从数据库中获取这个状态编码”;

//开始根据状态编码来创建需用的状态对象

if(stateId==null || stateId.trim().length()==0){

//如果没有值,说明还没有投过票,就初始化一个正常投票状态

state = new NormalVoteState();

}else if(“重复投票”.equals(stateId)){

state = new RepeatVoteState();

}else if(“恶意投票”.equals(stateId)){

state = new SpiteVoteState();

}else if(“黑名单”.equals(stateId)){

state = new BlackVoteState();

}

可能有些朋友会发现,如果向数据库里面存储下一个状态对象的状态编码,那么上下文中就不需要再持有状态对象了,有点相当于把这个功能放到数据库中了。有那么点相似性,不过要注意,数据库存储的只是状态编码,而不是状态对象,获取到数据库中的状态编码过后,在程序里面还是需要根据状态编码去真正创建对应的状态对象。

当然,要想程序更通用一点,可以通过配置文件来配置状态编码和对应的状态处理类,当然也可以直接在数据库中记录状态编码和对应的状态处理类,这样的话,在上下文中,先获取下一个状态的状态编码,然后根据这个状态编码去获取对应的类,然后可以通过反射来创建对象,这样实现就避免了那一长串的if-else,而且以后添加新的状态编码和状态处理对象也不用再修改代码了。示例代码如下:

VoteState state = null;

//2:直接从数据库获取该用户对应的下一个状态的状态编码

String stateId = “从数据库中获取这个值”;

//开始根据状态编码来创建需用的状态对象




//


根据状态编码去获取相应的类


String className = ”


根据状态编码去获取相应的类


“;


//


使用反射创建对象实例,简单示意一下


Class c = Class.

forName

(className);


state = (VoteState)c.newInstance();


直接把“转移”记录到数据库中

还有一种情况是直接把“转移”记录到数据库中,这样会更灵活。

所谓转移,指的就是描述从


A


状态到B


状态的这么一个转换变化。

比如:在正常投票状态处理对象里面指定使用“转移A”,而“转移A”描述的就是从正常投票状态转换成重复投票状态。这样一来,假如今后想要让正常投票处理过后变换成恶意投票状态,那么就不需要修改程序,直接修改数据库中的数据,把数据库中“转移A”的描述数据修改一下,使其描述从正常投票状态转换成恶意投票状态就可以了。

18.3.4  模拟工作流

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

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

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

图18.7  简单的请假流程示意图

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

  • 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);

}

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


2


:使用状态模式来实现流程

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

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

图18.8  用状态模式实现的流程的程序结构示意图

下面来看看怎么改造一下上面的示例,让它能运转起来,这样更加有利于大家去体会在处理这种流程的应用中,如何使用状态模式。


3


:改进上面使用状态模式来实现流程的示例

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

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

(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

小李,你的请假申请已经审核结束,结果是:项目经理审核结果:不同意

(5)小结一下

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

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

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

18.3.5  状态模式的优缺点

l          简化应用逻辑控制


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


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

l          更好的分离状态和行为


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

l          更好的扩展性


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

l          显式化进行状态转换


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

l          引入太多的状态类


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

18.3.6  思考状态模式


1:状态模式的本质

状态模式的本质:

根据状态来分离和选择行为

仔细分析状态模式的结构,如果没有上下文,那么就退化回到只有接口和实现了,正是通过接口,把状态和状态对应的行为分开,才使得通过状态模式设计的程序易于扩展和维护。

而上下文主要负责的是公共的状态驱动,每当状态发生改变的时候,通常都是回调上下文来执行状态对应的功能。当然,上下文自身也可以维护状态的变化,另外,上下文通常还会作为多个状态处理类之间的数据载体,在多个状态处理类之间传递数据。


2


:何时选用状态模式

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

  • 如果一个对象的行为取决于它的状态,而且它必须在运行时刻根据状态来改变它的行为。可以使用状态模式,来把状态和行为分离开,虽然分离开了,但状态和行为是有对应关系的,可以在运行期间,通过改变状态,就能够调用到该状态对应的状态处理对象上去,从而改变对象的行为。
  • 如果一个操作中含有庞大的多分支语句,而且这些分支依赖于该对象的状态。可以使用状态模式,把各个分支的处理分散包装到单独的对象处理类里面,这样,这些分支对应的对象就可以不依赖于其它对象而独立变化了。

18.3.7  相关模式

l          状态模式和策略模式


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

l          状态模式和观察者模式


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


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


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


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


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

l          状态模式和单例模式


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

l          状态模式和享元模式


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


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



版权声明:本文为jasshine原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。