logo头像

寒墨轩

—— 墨寒的👨‍💻码疯窝

Java设计模式(19)-状态模式

state image

(图片来源网络)

生活中,存在很多与"状态"相关的案例,比如:昨天晚上你熬夜看球赛,上午状态很差,迷迷糊糊的,熬到中午睡了一觉,下午又变得精神百倍了。在不同的时间,所处的状态可能不一样,而且还会按照一定条件流转,比如从犯困的状态变成了精神百倍的状态。这种在不同时间、不同条件下状态产生变化的对象,我们称之为"有状态"对象,状态作为其属性会产生变化。

在软件开发过程中,这种状态转换的场景非常多。比如,系统订单随着时间的推移,状态会产生转换,可能从下单后的待支付转换到支付后的待发货,也可能在发货后从待收货变成已收货,等等。

1. 活动状态案例

假设有一个活动需求,管理员可以在系统中添加活动,让用户来参与,同时可以对活动进行管理,比如禁用启用活动、终止活动等。假设活动的状态有:正常、已开始、已结束、已禁用、已终止等,它们之间的流转过程如下图所示:

state activity flow
Figure 1. 活动的状态变化

那么,如何实现活动的状态变化呢?传统的方式是将活动的状态定义为枚举类,然后在代码中进行if..else..或者switch的条件判断,通过编码切换到其他的状态,示例代码如下:

活动枚举类
1
2
3
enum ActivityStateEnum {
NORMAL, STARTED, FINISHED, DISABLED, TERMINATED (1)
}
1 通过枚举来定义活动的不同状态
通过条件判断来实现状态转换
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
class NormalActivityStateChange {
public ActivityStateEnum change(ActivityStateEnum state) {
ActivityStateEnum retState = null;
switch (state) {
case NORMAL:
// 省略具体业务逻辑...
retState = ActivityStateEnum.STARTED; (1)
break;
case STARTED:
// 省略具体业务逻辑...
retState = ActivityStateEnum.FINISHED; (2)
break;
case DISABLED:
// 省略具体业务逻辑...
retState = ActivityStateEnum.NORMAL; (3)
case TERMINATED:
// 省略具体业务逻辑...
case FINISHED:
// 省略具体业务逻辑...
default:
// 其他为最终状态,什么都不做
}
return retState;
}
}
1 开始时间到,开启任务
2 结束时间到了,结束任务
3 活动未开始之间,可以人工禁用

上述代码,非常不利于扩展其他状态,代码显得冗长不易理解,如果要增加新的状态,需要在switch语句中添加case分支。如果改进呢?答案是使用状态模式。

2. 状态模式

2.1. 状态模式简介

状态模式(State Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。当控制一个对象的状态转换条件过于复杂时,就可以将判断逻辑转移到状态类中,以简化复杂的判断逻辑。

状态模式的适用场景
  1. 对象需要根据自身行为不断转换状态,而且这个状态数量非常多且转换逻辑可能变化时,可以使用状态模式

  2. 对象的状态判断需要使用大量条件语句时,可以使用状态模式进行条件判断的简化

2.2. 状态模式结构

状态模式类结构如下图所示:

state class
Figure 2. 状态模式类图

状态模式有如下角色:

  • 抽象状态接口(State):可以为接口或抽象类,定义状态的公共方法和特定状态的方法,但是特定状态的方法需要所有具体状态对象所理解,因为它们可能需要实现这些方法。

  • 具体状态对象(Concrete State): 实现抽象状态定义的特定状态方法和公共方法,状态对象可存储对于上下文对象的反向引用,从而可以从上下文处获取所需信息, 并且能触发状态转换。

  • 上下文(Context):上下文,提供状态转换所需要的信息,并持有一个具体对象的引用,将所有与状态相关的工作委派给它,同时支持发起状态转换

上边的角色中,抽象状态(State)需要注意,定义特定状态的方法时,需要被所有具体状态对象理解,应为他们需要实现这些方法。这会造成子类实现多余的无关的方法。因此,最好再提供一个顶层抽象类来实现抽象状态接口,并提供空实现,这样子类就可以按需重载或实现特定的方法了。

2.3. 状态模式代码

看看状态模式的基本代码:

抽象状态(State):

1
2
3
4
5
interface State {
void setContext(Context context); (1)

void doSomething()
; (2)
}
1 反向引用上下文
2 特定业务方法

具体状态对象(Concrete State)

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
26
27
28
29
class ConcreteState1 implements State {
private Context context; (1)

@Override
public void setContext(Context context) {
this.context = context;
}

@Override
public void doSomething() {
System.out.println("状态1完成一些逻辑后转换状态...");
this.context.changeState(new ConcreteState2()); (2)
}
}

class ConcreteState2 implements State {
private Context context; (1)

@Override
public void setContext(Context context) {
this.context = context;
}

@Override
public void doSomething() {
System.out.println("状态2完成一些逻辑后转换状态...");
this.context.changeState(new ConcreteState1()); (2)
}
}
1 反向引用上下文,可以从上下文获取信息
2 发起状态转换,自身不直接转换而是委派给context

上下文(Context):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Context {
private State state; (1)

// 初始状态
public Context(State state) { (2)
this.state = state;
state.setContext(this);
}

// 状态转换
public void changeState(State state) { (3)
this.state = state;
this.state.setContext(this);
}

// 具体业务方法,委派给状态执行
public void operation() { (4)
this.state.doSomething();
}
}
1 持有一个特定状态的引用
2 通过构造器设置初始状态
3 状态转换方法,更改特定状态的引用
4 具体业务方法,委派给特定状态执行

客户端调用代码:

1
2
3
4
5
Context context = new Context(new ConcreteState1());
context.operation();
context.operation();
context.operation();
context.operation();

结果输出:

状态1完成一些逻辑后转换状态...
状态2完成一些逻辑后转换状态...
状态1完成一些逻辑后转换状态...
状态2完成一些逻辑后转换状态...

2.4. 状态模式优缺点

状态模式的优点和不足如下:

状态模式的优点
  1. 遵循单一职责原则,每一个状态类负责自身状态的业务逻辑,使得代码结构清晰

  2. 可以容易扩展新的状态,而改动的类较少

  3. 将状态转换过程放到单独的类来处理,更清晰,易于理解

状态模式的缺点
  1. 不符合开闭原则,状态对象见存在依赖关系,扩展新的状态时,其他转换到新状态的状态对象需要更改代码,客户端也可能需要更改代码

  2. 状态对象增多,增加了系统类的数量,带来一定的复杂性

3. 改造后的活动状态设计

接下来使用状态模式解决文章开头的活动状态转换问题。类图如下:

state activity use pattern
Figure 3. 使用状态模式的活动状态转换类图

上图中,在活动上下文对象中设计了活动的各种操作方法,内部对应了状态的更改。同时,增加了一个抽象类来实现活动状态,以便复用公共代码。

改造后的代码如下:

1、抽象状态接口

1
2
3
4
5
6
7
interface ActivityState {
void setActivityContext(ActivityContext context);

String name(); (1)

void nextState(ActivityState state)
; (2)
}
1 该方法返回当前状态名称
2 该方法定义了从当前状态转换为下一个状态

2、活动上下文:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ActivityContext {
private ActivityState state;

public ActivityContext(ActivityState initState) {
this.state = initState;
this.state.setActivityContext(this);
print();
}

public void changeState(ActivityState state) {
this.state = state;
this.state.setActivityContext(this);
}

// 为了简单,下边的业务非法省略了当前状态的检查

public void disable() {
((ActivityNormalState) this.state).disable();
print();
}

public void enable() {
((ActivityDisabledState) this.state).enable();
print();
}

public void start() {
((ActivityNormalState) this.state).start();
print();
}

public void finish() {
((ActivityStartedState) this.state).finish();
print();
}

public void terminate() {
((ActivityStartedState) this.state).terminate();
print();
}

private void print() {
System.out.println("当前状态:" + this.state.name());
}
}

上下文中定义一系列操作活动状态的方法,这些方法更改了活动状态后会输出状态名称。

3、抽象活动状态实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 抽象状态类
abstract class AbstractActivityState implements ActivityState {
protected ActivityContext context;

@Override
public void setActivityContext(ActivityContext context) {
this.context = context;
}

@Override
public void nextState(ActivityState state) {
this.context.changeState(state); (1)
}

@Override
public abstract String name();
}
1 委托给上下文发起状态转换

抽象状态类定义protected的上下文引用变量,子类可以直接复用,将公共的setActivityContextnextState方法放到抽象类中,以便子类复用.

4、具体状态类:

正常状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ActivityNormalState extends AbstractActivityState {
@Override
public String name() {
return "正常";
}

public void disable() { (1)
this.nextState(new ActivityDisabledState());
}

public void start() { (2)
this.nextState(new ActivityStartedState());
}
}
1 特定于当前状态的禁用活动的方法
2 特定于当前状态的开始活动的方法

其他状态代码类似,不再列出。

5、客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Client {
public void invoke() {
// 通过调用context的业务方法实现状态转换
ActivityContext context = new ActivityContext(new ActivityNormalState());
context.disable();
context.enable();
context.start();
context.finish();

context = new ActivityContext(new ActivityNormalState());
context.disable();
context.enable();
context.start();
context.terminate();
}
}

上边的调用代码,前半部分为活动正常流程,可以从正常状态转换到最终的结束状态;后半部分为异常流程,活动开始后被终结,状态从开始转换为终止。

代码运行结果输出:

当前状态:正常
当前状态:已禁用
当前状态:正常
当前状态:已开始
当前状态:已结束
当前状态:正常
当前状态:已禁用
当前状态:正常
当前状态:已开始
当前状态:已终止

完整的实例代码见: github

4. 总结

状态模式将每一个状态定义为单独的状态对象,简化了多状态对象的复杂状态判断和状态转换逻辑,适用于状态多、转换逻辑变化频分的业务场景,如果状态少而且相对稳定,那么最好不用状态模式,因为它对开闭原则支持不友好,扩展状态修改的类较多,而且具体状态对象过多,也会提高系统的复杂性。

支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励