下面用一个例子来帮助大家理解状态模式
我们每天都在乘电梯, 那我们来看看电梯有哪些动作(映射到Java中就是有多少方法) : 开门、 关门、运行、停止。好,我们就用程序来实现一下电梯的动作.
电梯程序第一版
首先我们定义一个电梯接口
在坐电梯时,你不可能希望电梯在运行期间突然开门吧,也不希望电梯停下了打不开门吧,所以,电梯执行这几个动作之前都需要前置条件。
- 敞门状态:在这个状态下电梯只能做的动作是关门动作。
- 闭门状态:在这个状态下, 可以进行的动作是:开门(我不想坐电梯了)、 停止(忘记按路层号了)、运行。
- 运行状态:在这个状态,电梯只能做停止。
- 停止状态:可以选择继续运行和开门
下面我们改一下之前做的程序,看一下修改之后的类图
这里增加了4个静态常量, 并增加了一个方法setState, 设置电梯的状态。
public interface ILift { //电梯的4个状态 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();}
电梯实现类
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("电梯停止了..."); }}
场景类
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(); }}
在业务调用的方法中增加了电梯状态判断, 电梯要不是随时都可以开的, 必须满足一定
条件才能开门, 人才能走进去, 我们设置电梯的起始是停止状态 。
运行结果
这段程序的问题:
- 电梯实现类过长,长的原因是我们在程序中使用了大量的switch...case这样的判断 。
- 扩展性差:如果程序还要加上通电状态和断电状态,Open()、 Close()、 Run()、 Stop()这4个方法都要增加判断条件 。
电梯程序第二版--状态模式
下图为电梯程序的以状态作为导向的类图
在类图中, 定义了一个LiftState抽象类, 声明了一个受保护的类型Context变量, 这个是
串联各个状态的封装类。 封装的目的很明显, 就是电梯对象内部状态的变化不被调用类知
晓, 也就是迪米特法则了(我的类内部情节你知道得越少越好) , 并且还定义了4个具体的
实现类, 承担的是状态的产生以及状态间的转换过渡 。
抽象电梯状态
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();}
上下文类Context
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(); }}
敞门状态实现
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; }}
关门状态实现
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(); }}
运行和停止状态实现与上面两种类似
场景测试类
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(); }}
状态模式的应用
定义:状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类
我们先来看看状态模式中的3个角色。
● State——抽象状态角色
接口或抽象类, 负责对象状态定义, 并且封装环境角色以实现状态切换。
● ConcreteState——具体状态角色
每一个具体状态必须完成两个职责: 本状态的行为管理以及趋向状态处理, 通俗地说,
就是本状态下要做的事情, 以及本状态如何过渡到其他状态。
● Context——环境角色
定义客户端需要的接口, 并且负责具体状态的切换。
状态模式的优缺点
优点
- 结构清晰,避免了过多的switch...case或者if...else语句的使用, 避免了程序的复杂性,提高系统的可
维护性。 - 遵循设计原则,很好地体现了开闭原则和单一职责原则, 每个状态都是一个子类, 你要增加状态就要增
加子类, 你要修改状态, 你只修改一个子类就可以了。 - 封装性好,这也是状态模式的基本要求, 状态变换放置到类的内部来实现, 外部的调用不用知道类
内部如何实现状态和行为的变换。
缺点
- 子类会太多, 也就是类膨胀 。
状态模式的使用场景
- 行为随状态改变而改变的场景
- 条件、 分支判断语句的替代者:在程序中大量使用switch语句或者if判断语句会导致程序结构不清晰, 逻辑混乱, 使用状态模式可以很好地避免这一问题, 它通过扩展子类实现了条件的判断处理
下面用一个例子来帮助大家理解状态模式
我们每天都在乘电梯, 那我们来看看电梯有哪些动作(映射到Java中就是有多少方法) : 开门、 关门、运行、停止。好,我们就用程序来实现一下电梯的动作.
电梯程序第一版
首先我们定义一个电梯接口
在坐电梯时,你不可能希望电梯在运行期间突然开门吧,也不希望电梯停下了打不开门吧,所以,电梯执行这几个动作之前都需要前置条件。
- 敞门状态:在这个状态下电梯只能做的动作是关门动作。
- 闭门状态:在这个状态下, 可以进行的动作是:开门(我不想坐电梯了)、 停止(忘记按路层号了)、运行。
- 运行状态:在这个状态,电梯只能做停止。
- 停止状态:可以选择继续运行和开门
下面我们改一下之前做的程序,看一下修改之后的类图
这里增加了4个静态常量, 并增加了一个方法setState, 设置电梯的状态。
public interface ILift { //电梯的4个状态 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();}
电梯实现类
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("电梯停止了..."); }}
场景类
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(); }}
在业务调用的方法中增加了电梯状态判断, 电梯要不是随时都可以开的, 必须满足一定
条件才能开门, 人才能走进去, 我们设置电梯的起始是停止状态 。
运行结果
这段程序的问题:
- 电梯实现类过长,长的原因是我们在程序中使用了大量的switch...case这样的判断 。
- 扩展性差:如果程序还要加上通电状态和断电状态,Open()、 Close()、 Run()、 Stop()这4个方法都要增加判断条件 。
电梯程序第二版--状态模式
下图为电梯程序的以状态作为导向的类图
在类图中, 定义了一个LiftState抽象类, 声明了一个受保护的类型Context变量, 这个是
串联各个状态的封装类。 封装的目的很明显, 就是电梯对象内部状态的变化不被调用类知
晓, 也就是迪米特法则了(我的类内部情节你知道得越少越好) , 并且还定义了4个具体的
实现类, 承担的是状态的产生以及状态间的转换过渡 。
抽象电梯状态
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();}
上下文类Context
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(); }}
敞门状态实现
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; }}
关门状态实现
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(); }}
运行和停止状态实现与上面两种类似
场景测试类
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(); }}
状态模式的应用
定义:状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类
我们先来看看状态模式中的3个角色。
● State——抽象状态角色
接口或抽象类, 负责对象状态定义, 并且封装环境角色以实现状态切换。
● ConcreteState——具体状态角色
每一个具体状态必须完成两个职责: 本状态的行为管理以及趋向状态处理, 通俗地说,
就是本状态下要做的事情, 以及本状态如何过渡到其他状态。
● Context——环境角色
定义客户端需要的接口, 并且负责具体状态的切换。
状态模式的优缺点
优点
- 结构清晰,避免了过多的switch...case或者if...else语句的使用, 避免了程序的复杂性,提高系统的可
维护性。 - 遵循设计原则,很好地体现了开闭原则和单一职责原则, 每个状态都是一个子类, 你要增加状态就要增
加子类, 你要修改状态, 你只修改一个子类就可以了。 - 封装性好,这也是状态模式的基本要求, 状态变换放置到类的内部来实现, 外部的调用不用知道类
内部如何实现状态和行为的变换。
缺点
- 子类会太多, 也就是类膨胀 。
状态模式的使用场景
- 行为随状态改变而改变的场景
- 条件、 分支判断语句的替代者:在程序中大量使用switch语句或者if判断语句会导致程序结构不清晰, 逻辑混乱, 使用状态模式可以很好地避免这一问题, 它通过扩展子类实现了条件的判断处理
参考书籍:《设计模式之禅》