状态模式
一、定义
状态( State )模式:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
二、适用性
- 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常, 有多个操作包含这一相同的条件结构。State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
三、结构
3.1、类图
3.2、所涉及的角色:
- 抽象状态 (State) 角色:定义一个接口,用以封装对象的一个特定状态所对应的形为。
- 具体状态 (ConcreteState) 角色:每一个具体状态类都实现环境(Context)的一个状态所对应的行为。
- 环境(Context)角色:定义客户端所感兴趣的接口,并且保留一个具体状态类的实例。这个具体状态类的实例给出些环境对象的现有状态。
3.3、通用代码
四、例子1 TCP监听的例子
TCP监听的例子,TCP有三个状态:等待,连接,断开,然后这三个状态中按照顺序循环变更
State
interface State { public void handle(Context ctx); }
ConcreteStateA
class ConcreteStateA implements State { public void handle(Context ctx) { System.out.println("handle by ConcreteStateA"); if (ctx != null) { ctx.ChangeState(new ConcreteStateB()); } } }
ConcreteStateB
class ConcreteStateB implements State { public void handle(Context ctx) { System.out.println("handle by ConcreteStateB"); if (ctx != null) { ctx.ChangeState(new ConcreteStateA()); } } }
Context
class Context { private State state; public Context(State _state) { state = _state; } public void request() { if (state != null) { state.handle(this); } } public void ChangeState(State _state) { state = _state; } }
Client
public class Client { public static void main(String[] args) { State state = new ConcreteStateA(); Context context = new Context(state); context.request(); context.request(); context.request(); context.request(); } }
运行结果
handle by ConcreteStateA handle by ConcreteStateB handle by ConcreteStateA handle by ConcreteStateB |
五、例子2 电梯的例子
模拟电梯
电梯包含四个状态如下
- 门敞状态---按了电梯上下按钮,电梯门开,这中间有5秒的时间(当然你也可以用身体挡住电梯门,那就不是5秒了),那就是门敞状态;在这个状态下电梯只能做的动作是关门动作,做别的动作?那就危险
- 门闭状态---电梯门关闭了,在这个状态下,可以进行的动作是:开门(我不想坐电梯了)、停止(忘记按路层号了)、运行
- 运行状态---电梯正在跑,上下窜,在这个状态下,电梯只能做的是停止;
- 停止状态---电梯停止不动,在这个状态下,电梯有两个可选动作:继续运行和开门动作;
我们用一张表来表示电梯状态和动作之间的关系:
|
开门(open) |
关门(close) |
运行(run) |
停止(stop) |
门敞状态 |
不允许 |
☆ |
不允许 |
不允许 |
门闭状态 |
☆ |
不允许 |
☆ |
☆ |
运行状态 |
不允许 |
不允许 |
不允许 |
☆ |
停止状态 |
☆ |
不允许 |
☆ |
不允许 |
5.1、不使用状态模式
在接口中定义了四个常量,分别表示电梯的四个状态:门敞状态、关闭状态、运行状态、停止状态,然后在实现类中电梯的每一次动作发生都要对状态进行判断,判断是否运行执行,也就是动作的执行是否符合业务逻辑,实现类中的四个私有方法是仅仅实现电梯的动作,没有任何的前置条件,因此这四个方法是不能为外部类调用的,设置为私有方法。
电梯接口 增加了四个静态常量,增加了一个方法setState,设置电梯的状态
/** * 定义一个电梯的接口 */ public interface ILift { // 电梯的四个状态 public final static int OPENING_STATE = 1; // 门敞状态 public final static int CLOSING_STATE = 2; // 门闭状态 public final static int RUNNING_STATE = 3; // 运行状态 public final static int STOPPING_STATE = 4; // 停止状态; // 设置电梯的状态 public void setState(int state); // 首先电梯门开启动作 public void open(); // 电梯门有开启,那当然也就有关闭了 public void close(); // 电梯要能上能下,跑起来 public void run(); // 电梯还要能停下来,停不下来那就扯淡了 public void stop(); }
实现类(存在大量的 switch case 判断)
/** * 电梯的实现类 */ public class Lift implements ILift { private int state; public void setState(int state) { this.state = state; } // 电梯门关闭 public void close() { // 电梯在什么状态下才能关闭 switch (this.state) { case OPENING_STATE: // 如果是则可以关门,同时修改电梯状态 this.closeWithoutLogic(); this.setState(CLOSING_STATE); break; case CLOSING_STATE: // 如果电梯就是关门状态,则什么都不做 // do nothing; break; case RUNNING_STATE: // 如果是正在运行,门本来就是关闭的,也说明都不做 // do nothing; break; case STOPPING_STATE: // 如果是停止状态,本也是关闭的,什么也不做 // do nothing; break; } } // 电梯门开启 public void open() { // 电梯在什么状态才能开启 switch (this.state) { case OPENING_STATE: // 如果已经在门敞状态,则什么都不做 // do nothing; break; case CLOSING_STATE: // 如是电梯时关闭状态,则可以开启 this.openWithoutLogic(); this.setState(OPENING_STATE); break; case RUNNING_STATE: // 正在运行状态,则不能开门,什么都不做 // do nothing; break; case STOPPING_STATE: // 停止状态,淡然要开门了 this.openWithoutLogic(); this.setState(OPENING_STATE); break; } } // 电梯开始跑起来 public void run() { switch (this.state) { case OPENING_STATE: // 如果已经在门敞状态,则不你能运行,什么都不做 // do nothing; break; case CLOSING_STATE: // 如是电梯时关闭状态,则可以运行 this.runWithoutLogic(); this.setState(RUNNING_STATE); break; case RUNNING_STATE: // 正在运行状态,则什么都不做 // do nothing; break; case STOPPING_STATE: // 停止状态,可以运行 this.runWithoutLogic(); this.setState(RUNNING_STATE); } } // 电梯停止 public void stop() { switch (this.state) { case OPENING_STATE: // 如果已经在门敞状态,那肯定要先停下来的,什么都不做 // do nothing; break; case CLOSING_STATE: // 如是电梯时关闭状态,则当然可以停止了 this.stopWithoutLogic(); this.setState(CLOSING_STATE); break; case RUNNING_STATE: // 正在运行状态,有运行当然那也就有停止了 this.stopWithoutLogic(); this.setState(CLOSING_STATE); break; case STOPPING_STATE: // 停止状态,什么都不做 // do nothing; break; } } // 纯粹的电梯关门,不考虑实际的逻辑 private void closeWithoutLogic() { System.out.println("电梯门关闭..."); } // 纯粹的店门开,不考虑任何条件 private void openWithoutLogic() { System.out.println("电梯门开启..."); } // 纯粹的运行,不考虑其他条件 private void runWithoutLogic() { System.out.println("电梯上下跑起来..."); } // 单纯的停止,不考虑其他条件 private void stopWithoutLogic() { System.out.println("电梯停止了..."); } }
Client
public class Client { public static void main(String[] args) { ILift lift = new Lift(); // 电梯的初始条件应该是停止状态 lift.setState(ILift.STOPPING_STATE); // 首先是电梯门开启,人进去 lift.open(); // 然后电梯门关闭 lift.close(); // 再然后,电梯跑起来,向上或者向下 lift.run(); // 最后到达目的地,电梯挺下来 lift.stop(); } }
执行结果
电梯门开启... 电梯门关闭... 电梯上下跑起来... 电梯停止了... |
这段程序存在的问题
- 首先Lift.java这个文件有点长,长的原因是我们在程序中使用了大量的switch…case这样的判断(if…else也是一样),程序中只要你有这样的判断就避免不了加长程序,同步的在业务比较复杂的情况下,程序体会更长,这个就不是一个很好的习惯了,较长的方法或者类的维护性比较差,毕竟程序是给人来阅读的;
- 其次,扩展性非常的不好,大家来想想,电梯还有两个状态没有加,是什么?通电状态和断电状态,你要是在程序再增加这两个方法,你看看Open()、Close()、Run()、Stop()这四个方法都要增加判断条件,也就是说switch判断体中还要增加case项,也就说与开闭原则相违背了;
- 再其次,我们来思考我们的业务,电梯在检修的时候,可以在stop状态下不开门,这也是正常的业务需求呀,你想想看,如果加上这些判断条件,上面的程序有多少需要修改?虽然这些都是电梯的业务逻辑,但是一个类有且仅有一个原因引起类的变化,单一职责原则,看看我们的类,业务上的任务一个小小增加或改动都对我们的这个电梯类产生了修改,这是在项目开发上是有很大风险的。
5.2、使用状态模式
刚刚我们是从电梯的有哪些方法以及这些方法执行的条件去分析,现在我们换个角度来看问题,我们来想电梯在具有这些状态的时候,能够做什么事情,也就是说在电梯处于一个具体状态时,我们来思考这个状态是由什么动作触发而产生以及在这个状态下电梯还能做什么事情,举个例子来说,电梯在停止状态时,我们来思考两个问题:
- 第一、这个停止状态时怎么来的,那当然是由于电梯执行了stop方法而来的;
- 第二、在停止状态下,电梯还能做什么动作?继续运行?开门?那当然都可以了。
我们再来分析其他三个状态,也都是一样的结果,我们只要实现电梯在一个状态下的两个任务模型就可以了:这个状态是如何产生的以及在这个状态下还能做什么其他动作(也就是这个状态怎么过渡到其他状态),既然我们以状态为参考模型,那我们就先定义电梯的状态接口,思考过后我们来看类图:
在类图中,定义了一个LiftState抽象类,声明了一个受保护的类型Context变量,这个是串联我们各个状态的封装类,封装的目的很明显,就是电梯对象内部状态的变化不被调用类知晓,也就是迪米特法则了,我的类内部情节你知道越少越好,并且还定义了四个具体的实现类,承担的是状态的产生以及状态间的转换过渡,
LiftState
/** *定义一个电梯状态的接口 */ public abstract class LiftState { // 定义一个环境角色,也就是封装状态的变换引起的功能变化 protected Context context; public void setContext(Context _context) { this.context = _context; } // 首先电梯门开启动作 public abstract void open(); // 电梯门有开启,那当然也就有关闭了 public abstract void close(); // 电梯要能上能下,跑起来 public abstract void run(); // 电梯还要能停下来,停不下来那就扯淡了 public abstract void stop(); }
四个具体状态
RunningState 运行状态
/** * 电梯在运行状态下能做哪些动作 */ public class RunningState extends LiftState { // 电梯门关闭?这是肯定了 @Override public void close() { // do nothing } // 运行的时候开电梯门?你疯了!电梯不会给你开的 @Override public void open() { // do nothing } // 这是在运行状态下要实现的方法 @Override public void run() { System.out.println("电梯上下跑..."); } // 这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了 @Override public void stop() { super.context.setLiftState(Context.stoppingState); // 环境设置为停止状态; super.context.getLiftState().stop(); } }
StoppingState 停止状态
/** *在停止状态下能做什么事情 */ public class StoppingState extends LiftState { // 停止状态关门?电梯门本来就是关着的! @Override public void close() { // do nothing; } // 停止状态,开门,那是要的! @Override public void open() { super.context.setLiftState(Context.openningState); super.context.getLiftState().open(); } // 停止状态再跑起来,正常的很 @Override public void run() { super.context.setLiftState(Context.runningState); super.context.getLiftState().run(); } // 停止状态是怎么发生的呢?当然是停止方法执行了 @Override public void stop() { System.out.println("电梯停止了..."); } }
ClosingState 关门状态
/** * 电梯门关闭以后,电梯可以做哪些事情 */ public class ClosingState extends LiftState { // 电梯门关闭,这是关闭状态要实现的动作 @Override public void close() { System.out.println("电梯门关闭..."); } // 电梯门关了再打开,逗你玩呢,那这个允许呀 @Override public void open() { super.context.setLiftState(Context.openningState); // 置为门敞状态 super.context.getLiftState().open(); } // 电梯门关了就跑,这是再正常不过了 @Override public void run() { super.context.setLiftState(Context.runningState); // 设置为运行状态; super.context.getLiftState().run(); } // 电梯门关着,我就不按楼层 @Override public void stop() { super.context.setLiftState(Context.stoppingState); // 设置为停止状态; super.context.getLiftState().stop(); } }
OpenningState 开门状态
/** * 在电梯门开启的状态下能做什么事情 */ public class OpenningState extends LiftState { // 开启当然可以关闭了,我就想测试一下电梯门开关功能 @Override public void close() { // 状态修改 super.context.setLiftState(Context.closeingState); // 动作委托为CloseState来执行 super.context.getLiftState().close(); } // 打开电梯门 @Override public void open() { System.out.println("电梯门开启..."); } // 门开着电梯就想跑,这电梯,吓死你! @Override public void run() { // do nothing; } // 开门还不停止? public void stop() { // do nothing; } }
Context是一个环境角色,它的作用是串联各个状态的过渡,在LiftSate抽象类中我们定义了并把这个环境角色聚合进来,并传递到了子类,也就是四个具体的实现类中自己根据环境来决定如何进行状态的过渡。
public class Context { // 定义出所有的电梯状态 public final static OpenningState openningState = new OpenningState(); public final static ClosingState closeingState = new ClosingState(); public final static RunningState runningState = new RunningState(); public final static StoppingState stoppingState = new StoppingState(); // 定一个当前电梯状态 private LiftState liftState; public LiftState getLiftState() { return liftState; } public void setLiftState(LiftState liftState) { this.liftState = liftState; // 把当前的环境通知到各个实现类中 this.liftState.setContext(this); } public void open() { this.liftState.open(); } public void close() { this.liftState.close(); } public void run() { this.liftState.run(); } public void stop() { this.liftState.stop(); } }
Client
/** * 模拟电梯的动作 */ public class Client { public static void main(String[] args) { Context context = new Context(); context.setLiftState(new ClosingState()); context.open(); context.close(); context.run(); context.stop(); } }
六、优缺点
6.1、优点
状态模式中有什么优点呢?
1.首先是避免了过多的swith…case或者if..else语句的使用,避免了程序的复杂性;
2.其次是很好的使用体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就增加子类,你要修改状态,你只修改一个子类就可以了;
3.最后一个好处就是封装性非常好,这也是状态模式的基本要求,状态变换放置到了类的内部来实现,外部的调用不用知道类内部如何实现状态和行为的变换。
6.2、缺点
只有一个缺点,子类会太多,也就是类膨胀,你想一个事物有七八、十来个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好管理。状态模式使用时对象的状态最好不要超过五个,防止你写子类写疯掉。
七、状态模式与策略模式的区别
两个模式很容易混淆,区别两者的一个方法是考察环境角色是否有明显的状态和状态过渡。如果环境角色只有一个状态,那么就应当使用策略模式。策略模式的特点:一旦环境角色选择了一个具体的策略类,那么在整个环境类的生命周期里它都不会改变这个具体策略类。而状态模式则适用另一情况,即环境角色有明显的状态转移。在环境类的生命周期里面,会有几个不同的状态被使用。
另外一个微妙的区别在于策略模式的环境自己选择一个具体的策略类,而状态模式的环境类是被外在原因放进一个具体的状态中。
策略模式所选择的策略类往往不明显的告诉客户端它所选择的具体策略类,而状态模式则相反,环境角色所处的状态是明显告诉客户端的。
参考:
设计模式之禅