下面用一个例子来帮助大家理解状态模式
我们每天都在乘电梯,那我们来看看电梯有哪些动作(映射到 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 判断语句会导致程序结构不清晰,逻辑混乱,使用状态模式可以很好地避免这一问题,它通过扩展子类实现了条件的判断处理
参考书籍:《设计模式之禅》