23中设计模式之_状态模式
前言
状态模式听起来有点像策略模式,根据不同的状态执行不同的程序代码,真正去理解的时候,才知道另有一片天地。
引入
现在城市发展很快,百万级人口的城市一堆一堆的,那其中有两个东西的发明在城市的发展中起到非
常重要的作用:一个是汽车,一个呢是…,猜猜看,是什么?是电梯!汽车让城市可以横向扩展,电梯让
城市可以纵向延伸,向空中伸展。汽车对城市的发展我们就不说了,电梯,你想想看,如果没有电梯,每
天你需要爬 10 层楼梯, 你是不是会崩溃掉?建筑师设计了一个没有电梯的建筑,那投资家肯定不愿意投资,
那也是建筑师的耻辱呀,今天我们就用程序表现一下这个电梯是怎么运作的。
一个接口,一个实现类,然后Client去调用不同的方法
package stateParrern;
/**
* 电梯的四种状态
*
* @author weichyang
*
*/
public interface ILIft {
void stop();
void run();
void openDoor();
void closeDoor();
}
package stateParrern;
/**
* 最简单的实现
* @author weichyang
*
*/
public class Client {
public static void main(String[] args) {
ILIft ift = new LIftIml();
ift.openDoor();
ift.run();
ift.stop();
ift.closeDoor();
}
}
运行结果:
openDoor
run
stop
closeDoor
之所以没有贴源码,一,是因为太简单,想想都知道怎么实现,2,是下面会给到源码下载路径
这个程序有什么问题,你想呀电梯门可以打开,但不是随时
都可以开,是有前提条件的的,你不可能电梯在运行的时候突然开门吧?!电梯也不会出现停止了但是不
开门的情况吧?!那要是有也是事故嘛,再仔细想想,电梯的这四个动作的执行都是有前置条件,具体点
说说在特定状态下才能做特定事,那我们来分析一下电梯有什么那些特定状态:
门敞状态—按了电梯上下按钮,电梯门开,这中间有 5 秒的时间(当然你也可以用身体挡住电梯门,那就不是 5 秒了),那就是门敞状态;在这个状态下电梯只能做的动作是关门动作,做别的动作?那就危险喽
门闭状态—电梯门关闭了,在这个状态下,可以进行的动作是:开门(我不想坐电梯了)、停止(忘
记按路层号了)、运行
运行状态—电梯正在跑,上下窜,在这个状态下,电梯只能做的是停止;
停止状态—电梯停止不动,在这个状态下,电梯有两个可选动作:继续运行和开门动作;
我们用一张表来表示电梯状态和动作之间的关系
看到这张表后,我们才发觉,哦~~,我们的程序做的很不严谨,好,我们来修改一下,先看类图:
在接口中定义了四个常量,分别表示电梯的四个状态:门敞状态、关闭状态、运行状态、停止状态,
然后在实现类中电梯的每一次动作发生都要对状态进行判断,判断是否运行执行,也就是动作的执行是否
符合业务逻辑,实现类中的四个私有方法是仅仅实现电梯的动作,没有任何的前置条件,因此这四个方法
是不能为外部类调用的,设置为私有方法。我们先看接口的改变:
package stateParrern.typetwo;
/**
* 电梯的四种行为,当然四种行为不是随意切换的,行为根据不同的状态进行
*
* @author weichyang
*
*/
public interface ILIft {
public static int DOOR_OPEN = 0;// 敞开状态
public static int DOOR_CLOSE = 1;// 关闭状态
public static int Lift_RUNNING = 2;// 运行状态
public static int Lift_STOPED = 3;// 停止状态
void setliftState(int state);
void Liftstop();
void LiftRunning();
void openDoor();
void closeDoor();
}
这种写法添加了四中状态,在具体行为操作时候需要进行判断
Eg:
/**
* 什么条件可以跑起来
*/
public void LiftRunning() {
switch (this.state) {
case ILIft.DOOR_CLOSE:
this.runWithoutLogic();
this.setliftState(ILIft.Lift_RUNNING);
break;
case ILIft.DOOR_OPEN:
break;
case ILIft.Lift_RUNNING:
break;
case ILIft.Lift_STOPED:
this.runWithoutLogic();
this.setliftState(ILIft.Lift_RUNNING);
break;
default:
break;
}
System.out.println("state " + state);
}
四种行为属性,每种行为均会进行状态判断。
但是还是很简单的,就是在每一个接口定义的方法中使用 switch…case 来进行判断,是
否运行运行指定的动作。我们来 Client 程序的变更:
public class Client {
public static void main(String[] args) {
/**
* 理解起来费事
* 1.电梯门开启 关闭时候
*/
ILIft ift = new LIftIml();
ift.setliftState(ILIft.Lift_STOPED);
ift.openDoor();
ift.closeDoor();
ift.LiftRunning();
ift.Liftstop();
}
}
业务调用的方法中增加了电梯状态判断,电梯要开门不是随时都可以开的,必须满足了一定条件你才
能开门,人才能走进去,我们设置电梯的起始是停止状态,看运行结果:
电梯门开启…
电梯门关闭…
电梯上下跑起来…
电梯停止了…
我们来想一下,这段程序有什么问题,首先 Lift.java 这个文件有点长,长的原因是我们在程序中使
用了大量的 switch…case 这样的判断(if…else 也是一样),程序中只要你有这样的判断就避免不了加长
程序,同步的在业务比较复杂的情况下,程序体会更长,这个就不是一个很好的习惯了,较长的方法或者
类的维护性比较差,毕竟程序是给人来阅读的;其次,扩展性非常的不好,大家来想想,电梯还有两个状
态没有加,是什么?通电状态和断电状态,你要是在程序再增加这两个方法, 你看看 Open()、Close()、Run()、
Stop()这四个方法都要增加判断条件,也就是说 switch 判断体中还要增加 case 项,也就说与开闭原则相
违背了;再其次,我们来思考我们的业务,电梯在门敞开状态下就不能上下跑了吗?电梯有没有发生过只
有运行没有停止状态呢(从 40 层直接坠到 1 层嘛)?电梯故障嘛,还有电梯在检修的时候,可以在 stop
状态下不开门,这也是正常的业务需求呀,你想想看,如果加上这些判断条件,上面的程序有多少需要修
改?虽然这些都是电梯的业务逻辑,但是一个类有且仅有一个原因引起类的变化,单一职责原则,看看我
们的类,业务上的任务一个小小增加或改动都对我们的这个电梯类产生了修改,这是在项目开发上是有很
大风险的。既然我们已经发现程序上有以上问题,我们怎么来修改呢?
刚刚我们是从电梯的有哪些方法以及这些方法执行的条件去分析,现在我们换个角度来看问题,我们
来想电梯在具有这些状态的时候,能够做什么事情,也就是说在电梯处于一个具体状态时,我们来思考这
个状态是由什么动作触发而产生以及在这个状态下电梯还能做什么事情,举个例子来说,电梯在停止状态
时,我们来思考两个问题:
第一、这个停止状态时怎么来的,那当然是由于电梯执行了 stop 方法而来的;
第二、在停止状态下,电梯还能做什么动作?继续运行?开门?那当然都可以了。
我们再来分析其他三个状态,也都是一样的结果,我们只要实现电梯在一个状态下的两个任务模型就可以了:这个状态是如何产生的以及在这个状态下还能做什么其他动作(也就是这个状态怎么过渡到其他
状态),既然我们以状态为参考模型,那我们就先定义电梯的状态接口,思考过后我们来看类图:
在类图中,定义了一个 LiftState 抽象类,声明了一个受保护的类型 Context 变量,这个是串联我们
各个状态的封装类,封装的目的很明显,就是电梯对象内部状态的变化不被调用类知晓,也就是迪米特法
则了,我的类内部情节你知道越少越好,并且还定义了四个具体的实现类,承担的是状态的产生以及状态
间的转换过渡,我们先来看 LiftState 程序:
package stateParrern.typethree;
/**
* 抽象类
*
* @author weichyang
*
*/
public abstract class LiftState {
public Context context;
public void setContext(Context context) {
this.context = context;
}
abstract void Liftstop();
abstract void LiftRunning();
abstract void openDoor();
abstract void closeDoor();
}
我来解释一下这个类的几个方法,Openning 状态是由 open()方法产生的,因此这个方法中有一个具体
的业务逻辑,我们是用 print 来代替了;在 Openning 状态下,电梯能过渡到其他什么状态呢?按照现在的
定义的是只能过渡到 Closing 状态,因此我们在 Close()中定义了状态变更,同时把 Close 这个动作也委托
了给 CloseState 类下的 Close 方法执行,这个可能不好理解,我们再看看 Context 类就可能好理解一点:
package stateParrern.typethree;
public class Context {
/**
* 定义出所有电梯的状态
*/
public final static OpenningState openningState = new OpenningState();
public final static ClosingState closingState = new ClosingState();
public final static RunningState runningState = new RunningState();
public final static StoppingState stopstate = new StoppingState();
public LiftState liftState;
public LiftState getLiftState() {
return liftState;
}
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
liftState.setContext(this);
System.out.println(liftState.getClass().getSimpleName());
}
public void Liftstop() {
liftState.Liftstop();
}
/**
* 什么条件可以跑起来
*/
public void LiftRunning() {
liftState.LiftRunning();
}
/**
* 开门的条件 二维表如何看 横纵进行合并
*
*/
public void openDoor() {
liftState.openDoor();
}
public void closeDoor() {
liftState.closeDoor();
}
}
结合以上三个类,我们可以这样理解,Context 是一个环境角色,它的作用是串联各个状态的过渡,在
LiftSate 抽象类中我们定义了并把这个环境角色聚合进来,并传递到了子类,也就是四个具体的实现类中
自己根据环境来决定如何进行状态的过渡。
package stateParrern.typethree;
/**
* 最简单的实现
*
* @author weichyang
*
*/
public class Client {
public static void main(String[] args) {
/**
* 理解起来费事 1.电梯门开启 关闭时候
*/
Context context = new Context();
context.setLiftState(Context.stopstate);
context.openDoor();
context.closeDoor();
context.LiftRunning();
context.Liftstop();
}
}
Client 调用类太简单了,只要定义个电梯的初始状态,然后调用相关的方法,就完成了,完全不用考
虑状态的变更,看运行结果:
电梯门开启
电梯关闭状态
电梯正在运行
电梯停止状态
我们再来回顾一下我们刚刚批判上一段的代码,首先我们说人家代码太长,这个问题我们解决了,通
过各个子类来实现,每个子类的代码都很短,而且也取消了的 switch…case 条件的判断;其次,说人家不
符合开闭原则,那如果在我们这个例子中要增加两个状态怎么加?增加两个子类,一个是通电状态,一个
是断电状态,同时修改其他实现类的相应方法,因为状态要过渡呀,那当然要修改原有的类,只是在原有
类中的方法上增加,而不去做修改;再其次,我们说人家不符合迪米特法则,我们现在呢是各个状态是单
独的一个类,只有与这个状态的有关的因素修改了这个类才修改,符合迪米特法则,非常完美!
上面例子中多次提到状态,那我们这节讲的就是状态模式,什么是状态模式呢? 当一个对象内在状态
改变时允许其改变行为,这个对象看起来像是改变了其类。说实话,这个定义的后半句我也没看懂,看过
GOF 才明白是怎么回事: Allow an object to alter its behavior when its internal state changes. The
object will appear to change its class. [GoF, p305],也就是说状态模式封装的非常好,状态的变更
引起了行为的变更,从外部看起来就好像这个对象对应的类发生了改变一样。状态模式的通用实现类如下:
状态模式通用类图
状态模式中有什么优点呢?首先是避免了过多的 swith…case 或者 if..else 语句的使用,避免了程序
的复杂性;其次是很好的使用体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就
增加子类,你要修改状态,你只修改一个子类就可以了;最后一个好处就是封装性非常好,这也是状态模
式的基本要求,状态变换放置到了类的内部来实现,外部的调用不用知道类内部如何实现状态和行为的变
换。
状态模式既然有优点,那当然有缺点了,只有一个缺点,子类会太多,也就是类膨胀,你想一个事物
上面就是状态模式的实现,写法
demo下载链接: http://pan.baidu.com/s/1mhVf5VE
引用 cbf4Life 写的23中设计模式