(目录)
状态模式的应用场景非常广泛,比如:
线上购物订单、手机支付、音乐播放器、游戏、工作流引擎等场景。
状态模式设计的初衷是应对同一个对象里不同状态变化时的不同行为的变化。
模式原理
原始定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了自己的类一样。 这个定义确实有些抽象,简而言之,状态模式就是让一个对象通过定义一系列状态的变化来控制行为的变化。 比如,给购买的物品定义几个包裹运送状态,已下单、运送中、已签收等,当“已下单”状态变为“运送中”状态时,物流货车会把包装好的包裹运送到指定地址,也就是说,当包裹的状态发生改变时,就会触发相应的外部操作。
状态模式的标准 UML 图:
从这个 UML 图中,我们能看出状态模式包含的关键角色有三个。
- 上下文信息类(Context):实际上就是存储当前状态的类,对外提供更新状态的操作。
- 抽象状态类(State):可以是一个接口或抽象类,用于定义声明状态更新的操作方法有哪些。
- 具体状态类(StateA 等):实现抽象状态类定义的方法,根据具体的场景来指定对应状态改变后的代码实现逻辑。
UML 对应代码实现:
package cn.happymaya.ndp.behavioral.state;
public class Context {
private State currentState;
public Context(State currentState) {
this.currentState = currentState;
if (null == currentState) {
this.currentState = StateA.instance();
}
}
public State getCurrentState() {
return currentState;
}
public void setCurrentState(State currentState) {
this.currentState = currentState;
}
public void request() {
currentState.handle(this);
}
}
public interface State {
void handle(Context context);
}
public class StateA implements State{
private static StateA instance = new StateA();
private StateA() {}
public static StateA instance() {
return instance;
}
@Override
public void handle(Context context) {
System.out.println("=== 状态 A");
}
}
public class StateB implements State{
private static StateB instance = new StateB();
private StateB() {}
public static StateB instance() {
return instance;
}
@Override
public void handle(Context context) {
System.out.println("=== 状态 B");
}
}
在该代码实现中:
- 状态 A 和状态 B 被封装为具体的状态类 StateA 和 StateB,均实现了同一个抽象状态类 State 接口;
- 上下文信息类 Context 存储一个全局变量 currentState,用以保存当前状态对象,具体状态类通过将 Context 对象作为参数输入,就能获取访问全局的当前状态,以完成状态切换。
因此,状态模式设计的核心点在于找到合适的抽象状态以及状态之间的转移关系,通过改变状态来达到改变行为的目的。
使用场景
一般来讲,状态模式常见使用场景有这样几种:
- 对象根据自身状态的变化来进行不同行为的操作时, 比如,购物订单状态;
- 对象需要根据自身变量的当前值改变行为,不期望使用大量 if-else 语句时, 比如,商品库存状态。
- 对于某些确定的状态和行为,不想使用重复代码时, 比如,某一个会员当天的购物浏览记录。
举一个现实中的例子,我们可以通过按电视遥控器上的按钮来改变电视的显示状态。如果电视处于打开状态,我们可以将其设置为关闭、静音或更换频道。但如果电视是关闭状态,当我们按下切换频道的按钮时是不会有任何作用的。因为这时对于关闭状态的电视来说,只有设置为打开状态才有效。
再比如,状态模式在程序中的另一个例子就是 Java 线程状态,一个线程的生命周期里有五种状态,只有在获得当前状态后才能确定下一个状态。
通过一个简单的例子来演示一下:
在线上购物的过程中,当我们选定好了商品并提交订单后,装有商品的包裹就会开始进行运送。
这里
定义了 6 种简单的包裹运送状态:已下单、仓库处理中、运输中、派送中、待取件和已签收。
如下图所示:
首先,定义包裹的状态 PackageState,在接口中声明一个更新状态的方法updateState(),该方法接收包裹上下文信息类 PackageContext 作为参数
package cn.happymaya.ndp.behavioral.state;
/**
* 包裹状态
*/
public interface PackageState {
/**
* 定义了 6 中状态
* 1 - 已下单
* 2 - 仓库处理中
* 3 - 运输中
* 4 - 派送中
* 5 - 待取件
* 6 - 已签收
*/
void updateState(PackageContext ctx);
}
然后,定义详细上下文信息类 PackageContext,其中包含一个当前状态 PackageState 和一个包裹的 id
package cn.happymaya.ndp.behavioral.state;
/**
* 包裹上下文
*/
public class PackageContext {
private PackageState currentState;
private String packageId;
public PackageContext(PackageState currentState, String packageId) {
this.currentState = currentState;
this.packageId = packageId;
if (currentState == null) {
this.currentState = Ackonwledged.getInstance();
}
}
public PackageState getCurrentState() {
return currentState;
}
public void setCurrentState(PackageState currentState) {
this.currentState = currentState;
}
public void setPackageId(String packageId) {
this.packageId = packageId;
}
public String getPackageId() {
return packageId;
}
public void update() {
currentState.updateState(this);
}
}
接着,依次定义具体状态类:
- 已下单(Acknowledged)
- 仓库处理中(WarehouseProcessing)
- 运输中(InTransition)
- 派送中(Delivering)
- **待取件(WaitForPickUp)、已签收(Received)。每一个类都会实现 updateState() 方法,同时使用单例模式模拟状态的唯一性。 **
package cn.happymaya.ndp.behavioral.state;
public class Ackonwledged implements PackageState{
// Singleton
private static Ackonwledged instance = new Ackonwledged();
private Ackonwledged(){}
public static Ackonwledged getInstance() {
return instance;
}
@Override
public void updateState(PackageContext ctx) {
System.out.println("=== state start...");
System.out.println("1 - Package is acknowledged !!");
ctx.setCurrentState(WarehouseProcessing.getInstance());
}
}
public class WarehouseProcessing implements PackageState {
//Singleton
private static WarehouseProcessing instance = new WarehouseProcessing();
private WarehouseProcessing() {}
public static WarehouseProcessing getInstance() {
return instance;
}
@Override
public void updateState(PackageContext ctx) {
System.out.println("2 - Package is WarehouseProcessing");
ctx.setCurrentState(InTransition.getInstance());
}
}
public class InTransition implements PackageState {
//Singleton
private static InTransition instance = new InTransition();
private InTransition() {}
public static InTransition getInstance() {
return instance;
}
@Override
public void updateState(PackageContext ctx) {
System.out.println("3 - Package is in transition !!");
ctx.setCurrentState(Delivering.getInstance());
}
}
public class Delivering implements PackageState{
//Singleton
private static Delivering instance = new Delivering();
private Delivering() {}
public static Delivering getInstance() {
return instance;
}
@Override
public void updateState(PackageContext ctx) {
System.out.println("4 - Package is Delivering !!");
ctx.setCurrentState(WaitForPickUp.getInstance());
}
}
public class Received implements PackageState {
//Singleton
private static Received instance = new Received();
private Received() {}
public static Received getInstance() {
return instance;
}
@Override
public void updateState(PackageContext ctx) {
System.out.println("6 - Package is Received !!");
System.out.println("=== state end ");
}
}
最后,运行一个单元测试,通过执行上下文信息类的更新操作了变更状态。
package cn.happymaya.ndp.behavioral.state;
public class Client {
public static void main(String[] args) {
PackageContext ctx = new PackageContext(null, "Test123");
ctx.update();
ctx.update();
ctx.update();
ctx.update();
ctx.update();
ctx.update();
}
}
从单元测试的结果中,能直观地看到:执行一次状态更新,状态会变为下一个状态,直至状态结束。
使用状态模式的原因
使用状态模式的原因,主要有以下两个:
- 当要设计的业务具有复杂状态变迁时,期望通过状态变化来快速进行变更操作,并降低代码耦合性。 在上面使用场景的例子中,大致看到了一个包裹的状态变化流程,实际上的购物订单的状态变化远比这个要复杂。对于状态变化引起行为变化的情况,使用状态模式就能够很好地解决。
- 一方面因为状态是提前进行分析整理的,这样能减少代码实现的难度;
- 另一方面是因为状态与状态之间做了天然的隔离,能够将相关的行为聚合到一起,提高类的内聚度,降低耦合性;
- 避免增加代码的复杂性。
- 在通常的设计中,每次新增状态时,需要添加大量的条件判断语句。最典型的就是 if-else 不断嵌套处理,这样的代码发展到后期,逻辑会变得异常复杂,进而导致代码可维护性和灵活性变差;
- 使用状态模式能够很好地从状态的维度来进行逻辑的关联,状态与状态之间只有切换的动作,至于状态本身如何进行复杂处理,对于另一个状态来说,其实并不关心,这样就能很好地避免对象间的调用关系变得复杂。
优缺点
通过上述分析,我们可以得出使用状态模式主要有以下优点。
- 提前定好可能的状态,降低代码实现复杂度。 状态模式通常需要提前设计好状态的转移,这样就需要提前设计好状态之间的转移关系,在实现代码时就变得容易很多。
- 快速理解状态和行为之间的关系。 由于将操作和状态相关联,那么所有与某个状态相关的对象都会被聚合在一起,这样可以很方便地增加和删除操作,而不会影响其他状态的功能。
- 避免写大量的 if-else 条件语句。 比如,要判断订单商品到达配送站的状态,需要判断商品是在运送中还是已送达,到了以后还要再判断发往哪里,等等,这样的 if-else 条件语句会随着场景的增多而不断增加。如果建立起状态之间的转移关系,订单商品到达配送站会触发状态变换,然后进行对应状态下的对应操作,这样就能够有效减少直接的条件语句判断。
- 可以让多个环境对象共享一个状态对象,从而减少重复代码。 状态模式通常用于整体的流程控制和状态变更,这样对于多环境的应用程序来说,只需要共享一整个状态,而不需要每个环境都各自实现自己的状态对象。
当然,状态模式也有一些缺点。
- 造成很多零散类。 状态模式因为需要对每一个状态定义一个具体状态类,所以势必会增加系统类和对象的个数。
- 状态切换关系越复杂,代码实现难度越高。 随着状态的不断扩展,状态的结构与实现就会变得复杂,比如,A 状态切换到 B,B 切换到 C,C 异常回退 A,A 再走 D 异常状态,等等。如果使用不当,就会造成维护代码的人需要花费大量时间来梳理状态转移关系。
- 不满足开闭原则。 状态模式虽然降低了状态与状态之间的耦合性,但是新增和修改状态都会涉及前一个状态和后一个状态的代码修改,增大了引入代码问题的概率。
总结
- 状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为;
- 适合场景是**:对象本身具备很多状态变化,同时不同变化需要不同的行为来处理;**
- 状态模式虽然可以让代码条理清楚,容易阅读,但是实际上对开闭原则的支持并不友好,新增状态可能会影响原有的状态,在使用时要注意!
- 要想用好状态模式,关键点在于寻找好的状态以及状态与状态之间的关系,而不是急着去实现状态模式。状态确定好以后,状态模式本身代码实现是非常容易的。
标签:状态,ctx,private,状态机,instance,State,设计模式,public,currentState From: https://blog.51cto.com/panyujie/7463269