状态 State

状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。或者说通过控制状态的变化使其行为发生变化。


为什么要使用?

状态模式的对象职责:

通过控制状态的变化使其行为发生变化。找到合适的抽象状态以及状态之间的转移关系,通过改变状态来达到改变行为的目的。

当要设计的业务具有复杂的状态变迁时,期望通过状态变化来快速进行变更操作,并降低代码耦合性,避免增加代码的复杂性。

例如,一首歌曲的状态可以是“📝草稿”、“🔬审核中”、“📤上架”、“📥下架”。

  1. “草稿状态”只能跃迁到“审核状态”;
  2. “审核状态”如果审核成功可以跃迁到“上架状态”;如果审核失败则跃迁到“草稿状态”;
  3. “上架状态”和“下架状态”可以任意切换;
  4. “下架状态”可以选择永久下架,即一首歌曲的生命周期结束。

模式结构

  • 上下文(Context)保存了一个具体的状态对象,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互,且会向外界提供一个更新状态的操作。

  • 状态(State)接口会声明特定于状态的方法,这些方法应该适用于每个具体状态。

  • 具体状态(Concrete States)会自行实现特定于状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装有部分通用行为的中间抽象类。状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息,并且能触发状态转移。上下文和具体状态都可以设置上下文的下个状态,并可通过替换连接到上下文的状态对象来完成实际的状态转换。

状态模式的类图:


模式实现

该示例使用状态模式把包裹的状态分为 6 个部分,分别为“已下单”、“仓库处理中”、“运输中”、“派送中”、“待取件”、“已签收”。每个状态按顺序更新。每次调用 PackageContextupdate 方法时,它就会根据当前状态,去调用当前状态的 updateState 方法。每个状态的 updateState 方法中会设置下一个状态为当前状态,以便下次调用 PackageContextupdate 方法时,能够调用新状态的 updateState 方法。

示例程序的类图

示例程序的状态图

代码实现

上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package example;

/** 包裹上下文 */
public class PackageContext {
/** 当前状态 */
private PackageState currentState;
/** 包裹ID */
private String packageId;

public PackageContext(PackageState currentState, String packageId) {
this.currentState = currentState;
this.packageId = packageId;
}

public void setCurrentState(PackageState currentState) {
this.currentState = currentState;
}

/** 更新状态 */
public void update() {
System.out.print("当前包裹id:" + packageId + " --> 包裹状态:");
currentState.updateState(this);
System.out.println();
}
}
状态接口
1
2
3
4
5
6
7
8
9
10
package example;

/** 包裹状态接口 */
public interface PackageState {
/**
* 更新包裹状态
* @param ctx 包裹上下文
*/
void updateState(PackageContext ctx);
}
具体状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package example;

/** 已下单包裹状态 */
public class Acknowledged implements PackageState {
private static Acknowledged instance = new Acknowledged();

private Acknowledged() { }

/** 单例状态 */
public static Acknowledged getInstance() {
return instance;
}

/**
* 更新包裹状态
* @param ctx 包裹上下文
*/
@Override
public void updateState(PackageContext ctx) {
System.out.println("包裹已下单...");
ctx.setCurrentState(WarehouseProcessing.getInstance());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package example;

/** 仓库处理中包裹状态 */
public class WarehouseProcessing implements PackageState {
private static WarehouseProcessing instance = new WarehouseProcessing();

private WarehouseProcessing() { }

/** 单例状态 */
public static WarehouseProcessing getInstance() {
return instance;
}

/**
* 更新包裹状态
* @param ctx 包裹上下文
*/
@Override
public void updateState(PackageContext ctx) {
System.out.println("包裹正在仓库处理中...");
ctx.setCurrentState(InTransition.getInstance());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package example;

/** 运输中包裹状态 */
public class InTransition implements PackageState {
private static InTransition instance = new InTransition();

private InTransition() { }

/** 单例状态 */
public static InTransition getInstance() {
return instance;
}

/**
* 更新包裹状态
* @param ctx 包裹上下文
*/
@Override
public void updateState(PackageContext ctx) {
System.out.println("包裹正在运输中...");
ctx.setCurrentState(Delivering.getInstance());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package example;

/** 派送中包裹状态 */
public class Delivering implements PackageState {
private static Delivering instance = new Delivering();

private Delivering() { }

/** 单例状态 */
public static Delivering getInstance() {
return instance;
}

/**
* 更新包裹状态
* @param ctx 包裹上下文
*/
@Override
public void updateState(PackageContext ctx) {
System.out.println("包裹已下单...");
ctx.setCurrentState(WaitForPickUp.getInstance());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package example;

/** 待取件包裹状态 */
public class WaitForPickUp implements PackageState {
private static WaitForPickUp instance = new WaitForPickUp();

private WaitForPickUp() { }

/** 单例状态 */
public static WaitForPickUp getInstance() {
return instance;
}

/**
* 更新包裹状态
* @param ctx 包裹上下文
*/
@Override
public void updateState(PackageContext ctx) {
System.out.println("包裹待取件...");
ctx.setCurrentState(Received.getInstance());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package example;

/** 签收包裹状态 */
public class Received implements PackageState {
private static Received instance = new Received();

private Received() { }

/** 单例状态 */
public static Received getInstance() {
return instance;
}

/**
* 更新包裹状态
* @param ctx 包裹上下文
*/
@Override
public void updateState(PackageContext ctx) {
System.out.println("包裹已签收...");
}
}

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import example.Acknowledged;
import example.PackageContext;

/** 测试状态模式 */
public class Test {
public static void main(String[] args) {
PackageContext context = new PackageContext(Acknowledged.getInstance(), "9527");
context.update();
context.update();
context.update();
context.update();
context.update();
context.update();
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
当前包裹id:9527 --> 包裹状态:包裹已下单...

当前包裹id:9527 --> 包裹状态:包裹正在仓库处理中...

当前包裹id:9527 --> 包裹状态:包裹正在运输中...

当前包裹id:9527 --> 包裹状态:包裹已下单...

当前包裹id:9527 --> 包裹状态:包裹待取件...

当前包裹id:9527 --> 包裹状态:包裹已签收...

常用场景和解决方案

  • 对象根据自身状态的变化来进行不同行为的操作,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可以使用状态模式。
  • 对象需要根据自身变量的当前值改变行为,但不期望使用大量 if-else 语句时。
  • 对于某些确定的状态和行为,不想使用重复代码时。

模式的优缺点

优点 缺点
单一职责原则。将与特定状态相关的代码放在单独的类中。 如果状态机只有很少的几个状态,或者很少发生改变, 那么应用该模式可能会显得小题大作。
开闭原则。无需修改已有状态类和上下文就能引入新状态。 如果要修改当前状态类,就有可能要修改前一个状态类和后一个状态类。
通过消除臃肿的状态机条件语句简化上下文代码。

使用状态模式的优势

  • 提前定好可能的状态,降低代码实现复杂度。
  • 快速理解状态和行为之间的关系。
  • 避免写大量的 if-else 条件语句。
  • 可以让多个环境对象共享一个状态对象,从而减少重复代码。

使用状态模式的劣势

  • 造成很多零散类。
  • 状态切换关系越复杂,代码实现难度越高。

拓展知识

  • 状态可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给“帮手”对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。
  • 状态模式类似于面向对象的“关注点分离”,即将问题分解,让每个对象专注于每个问题。状态模式将一个对象的每种状态分离,使每个状态对象只专注于当前状态。而至于状态间的转换则交给上下文去处理。


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1