概念
状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式是一种对象行为型模式。大家着重理解对象,多种状态
状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。 状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。
在状态模式结构图中包含如下几个角色:
- Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
- State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
- ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
环境类实际上是真正拥有状态的对象,我们只是将环境类中与状态有关的代码提取出来封装到专门的状态类中。在状态模式结构图中,环境类Context与抽象状态类State之间存在单向关联关系,在Context中定义了一个State对象。在实际使用时,它们之间可能存在更为复杂的关系,State与Context之间可能也存在依赖或者关联关系。
在状态模式的使用过程中,一个对象的状态之间还可以进行相互转换,通常有两种实现状态转换的方式:
(1) 统一由环境类来负责状态之间的转换。此时,环境类还充当了状态管理器(State Manager)角色,在环境类的业务方法中通过对某些属性值的判断实现状态转换,还可以提供一个专门的方法用于实现属性判断和状态转换。
(2)由具体状态类来负责状态之间的转换。可以在具体状态类的业务方法中判断环境类的某些属性值再根据情况为环境类设置新的状态对象,实现状态转换,同样,也可以提供一个专门的方法来负责属性值的判断和状态转换。此时,状态类与环境类之间就将存在依赖或关联关系,因为状态类需要访问环境类中的属性值。
实践
以日常电梯为例,将电梯简单分为四种状态:开门、关门、运行、停止
。
如果不使用状态模式,普通if…else if …逻辑如何实现呢?
/**
* 普通实现结构
*/
public void liftState(String state) {
if (Constant.OPEN.equals(state)) {
//open
//close
} else if (Constant.CLOSE.equals(state)) {
//open
//close
//running
//stop
} else if (Constant.RUNNING.equals(state)) {
//running
//stop
} else if (Constant.STOP.equals(state)) {
//open
//close
//running
//stop
}
}
以上模式的实现,很明显不遵循设计模式的“开闭原则”,后期维护存在一定的问题。 下面使用状态模式如何改进。 电梯状态每个状态还都要有特定的行为,比如在开门的状态下,电梯只能关门,而不能运行;在关门状态下,电梯可以运行、开门等。用一张表来表示这个关系:
状态/动作 | 开门 | 关门 | 运行 | 停止 |
---|---|---|---|---|
开门状态 | O | O | X | X |
关门状态 | O | O | O | |
运行状态 | X | X | O | |
停止状态 | O | X | O |
抽象状态类
定义抽象状态类:分别定义电梯的四种状态方法。
代码语言:javascript复制public abstract class LiftState {
protected LiftContext liftContext;
public void setLiftContext(LiftContext liftContext){
this.liftContext = liftContext;
}
abstract void openState();
abstract void closeState();
abstract void runningState();
abstract void stopState();
}
这里解释一下为什么 LiftState 类里面会有一个LiftContext 对象。它的作用是去调节状态的变化,它就是电梯,电梯状态肯定是针对电梯来说的,所以选择组合LiftContext 对象。 下面看下具体状态实现类
具体状态类
- 开门状态:对于电梯开门时,肯定是可以关门的,同时开着门也不能运行电梯运行,多危险啊~故running()不做任何处理,实际业务中可以抛异常处理等等…
public class LiftOpen extends LiftState {
@Override
public void openState() {
System.out.println("电梯开门...");
}
@Override
public void closeState() {
//修改状态
super.liftContext.changeState(Constant.CLOSE);
//动作委托为CloseState来执行,也就是委托给了LifeClose子类执行这个动作
super.liftContext.close();
}
@Override
public void runningState() {
}
@Override
public void stopState() {
//电梯开门,已经处于停止状态,无需执行其他操作
}
}
- 关门状态:除了当前的关门状态,还具备开门、运行、停止状态;电梯关门之后可以再开门;电梯门关闭之后,可以允许电梯运行;如果关门之后,未选择楼层,电梯处于停止状态。
public class LiftClose extends LiftState {
@Override
public void openState() {
super.liftContext.changeState(Constant.OPEN);
super.liftContext.open();
}
@Override
public void closeState() {
System.out.println("电梯关门...");
}
@Override
public void runningState() {
super.liftContext.changeState(Constant.RUNNING);
super.liftContext.running();
}
@Override
public void stopState() {
super.liftContext.changeState(Constant.STOP);
super.liftContext.stop();
}
}
- 运行状态:电梯运行时,除了具体运行状态,当电梯到达指定楼层后,便可以处于停止状态。对于开门状态,运行时肯定不允许开门,故实际业务中可以抛异常处理。
public class LiftRunning extends LiftState {
@Override
public void openState() {
}
@Override
public void closeState() {
//电梯运行过程中,门始终处于关闭状态
}
@Override
public void runningState() {
System.out.println("电梯运行中...");
}
@Override
public void stopState() {
super.liftContext.changeState(Constant.STOP);
super.liftContext.stop();
}
}
- 停止状态:电梯停止状态时,此时门处于关闭状态,故closeState不必处理;停止时允许开门,也能够允许电梯再次运行。
public class LiftStop extends LiftState {
@Override
public void openState() {
super.liftContext.changeState(Constant.OPEN);
super.liftContext.open();
}
@Override
public void closeState() {
}
@Override
public void runningState() {
super.liftContext.changeState(Constant.RUNNING);
super.liftContext.running();
}
@Override
public void stopState() {
System.out.println("电梯停止...");
}
}
环境上下文
环境角色Context中,定义当前电梯的状态,处于初始化状态。同时提供外部访问方法。
代码语言:javascript复制public class LiftContext {
//定义当前电梯状态
private LiftState liftState;
public LiftState getLiftState() {
return liftState;
}
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
//通知各个实现类中
this.liftState.setLiftContext(this);
}
/**
* 环境类定义状态转化方法
* @param state
*/
public void changeState(String state) {
switch (state) {
case Constant.OPEN:
this.setLiftState(new LiftOpen());
break;
case Constant.CLOSE:
this.setLiftState(new LiftClose());
break;
case Constant.RUNNING:
this.setLiftState(new LiftRunning());
break;
case Constant.STOP:
this.setLiftState(new LiftStop());
break;
default:
break;
}
}
public void open() {
this.liftState.openState();
}
public void close() {
this.liftState.closeState();
}
public void running() {
this.liftState.runningState();
}
public void stop() {
this.liftState.stopState();
}
}
客户端
代码语言:javascript复制public class LiftClient {
public static void main(String[] args) {
LiftContext liftContext = new LiftContext();
liftContext.changeState(Constant.OPEN);
liftContext.open();
liftContext.close();
liftContext.running();
liftContext.stop();
}
}
运行结果:
源代码地址:https://github.com/stream-source/stream-source/tree/master/informal-essay
总结
总结,对于状态模式,自己理解起来挺费劲的,总感觉有一个梗过不去,实际上可以将状态模式与责任链模式对比起来理解(后期分享责任链),就很清晰了。状态模式将对象内部状态的转化,对于外部访问并不知道。 状态模式:主要理解状态和行为。状态是如何产生的,以及这个状态怎么过渡到其他状态(执行动作)。
- 何时使用
- 代码中包含大量与对象状态有关的条件语句
- 方法
- 将各种具体的状态类抽象出来
- 优点
- 结构清晰,避免了过多的switch…case或if…else语句的使用
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块
- 缺点:
- 状态模式的使用必然会增加系统类的对象的个数
- 状态模式的结构与实现都较为复杂,如果使用不当讲导致程序结构和代码的混乱。
- 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
- 使用场景:
- 对象的行为依赖于它的状态(属性)并且可以根据它的状态而改变它的相关行为
- 代码中包含大量与对象状态相关的条件语句
参考资料: 1.状态模式笔记 2.《Java 设计模式》