(图片来源网络)
生活中,存在很多与"状态"相关的案例,比如:昨天晚上你熬夜看球赛,上午状态很差,迷迷糊糊的,熬到中午睡了一觉,下午又变得精神百倍了。在不同的时间,所处的状态可能不一样,而且还会按照一定条件流转,比如从犯困的状态变成了精神百倍的状态。这种在不同时间、不同条件下状态产生变化的对象,我们称之为"有状态"对象,状态作为其属性会产生变化。
在软件开发过程中,这种状态转换的场景非常多。比如,系统订单随着时间的推移,状态会产生转换,可能从下单后的待支付转换到支付后的待发货,也可能在发货后从待收货变成已收货,等等。
1. 活动状态案例
假设有一个活动需求,管理员可以在系统中添加活动,让用户来参与,同时可以对活动进行管理,比如禁用启用活动、终止活动等。假设活动的状态有:正常、已开始、已结束、已禁用、已终止等,它们之间的流转过程如下图所示:
那么,如何实现活动的状态变化呢?传统的方式是将活动的状态定义为枚举类,然后在代码中进行if..else..
或者switch
的条件判断,通过编码切换到其他的状态,示例代码如下:
enum ActivityStateEnum {
NORMAL, STARTED, FINISHED, DISABLED, TERMINATED (1)
}
1 | 通过枚举来定义活动的不同状态 |
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):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。 |
状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。当控制一个对象的状态转换条件过于复杂时,就可以将判断逻辑转移到状态类中,以简化复杂的判断逻辑。
- 状态模式的适用场景
对象需要根据自身行为不断转换状态,而且这个状态数量非常多且转换逻辑可能变化时,可以使用状态模式
对象的状态判断需要使用大量条件语句时,可以使用状态模式进行条件判断的简化
2.2. 状态模式结构
状态模式类结构如下图所示:
状态模式有如下角色:
抽象状态接口(State):可以为接口或抽象类,定义状态的公共方法和特定状态的方法,但是特定状态的方法需要所有具体状态对象所理解,因为它们可能需要实现这些方法。
具体状态对象(Concrete State): 实现抽象状态定义的特定状态方法和公共方法,状态对象可存储对于上下文对象的反向引用,从而可以从上下文处获取所需信息, 并且能触发状态转换。
上下文(Context):上下文,提供状态转换所需要的信息,并持有一个具体对象的引用,将所有与状态相关的工作委派给它,同时支持发起状态转换
上边的角色中,抽象状态(State)需要注意,定义特定状态的方法时,需要被所有具体状态对象理解,应为他们需要实现这些方法。这会造成子类实现多余的无关的方法。因此,最好再提供一个顶层抽象类来实现抽象状态接口,并提供空实现,这样子类就可以按需重载或实现特定的方法了。 |
2.3. 状态模式代码
看看状态模式的基本代码:
抽象状态(State):
interface State {
void setContext(Context context); (1)
void doSomething(); (2)
}
1 | 反向引用上下文 |
2 | 特定业务方法 |
具体状态对象(Concrete State)
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):
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 | 具体业务方法,委派给特定状态执行 |
客户端调用代码:
Context context = new Context(new ConcreteState1());
context.operation();
context.operation();
context.operation();
context.operation();
结果输出:
状态1完成一些逻辑后转换状态... 状态2完成一些逻辑后转换状态... 状态1完成一些逻辑后转换状态... 状态2完成一些逻辑后转换状态...
2.4. 状态模式优缺点
状态模式的优点和不足如下:
- 状态模式的优点
遵循单一职责原则,每一个状态类负责自身状态的业务逻辑,使得代码结构清晰
可以容易扩展新的状态,而改动的类较少
将状态转换过程放到单独的类来处理,更清晰,易于理解
- 状态模式的缺点
不符合开闭原则,状态对象见存在依赖关系,扩展新的状态时,其他转换到新状态的状态对象需要更改代码,客户端也可能需要更改代码
状态对象增多,增加了系统类的数量,带来一定的复杂性
3. 改造后的活动状态设计
接下来使用状态模式解决文章开头的活动状态转换问题。类图如下:
上图中,在活动上下文对象中设计了活动的各种操作方法,内部对应了状态的更改。同时,增加了一个抽象类来实现活动状态,以便复用公共代码。
改造后的代码如下:
1、抽象状态接口
interface ActivityState {
void setActivityContext(ActivityContext context);
String name(); (1)
void nextState(ActivityState state); (2)
}
1 | 该方法返回当前状态名称 |
2 | 该方法定义了从当前状态转换为下一个状态 |
2、活动上下文:
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、抽象活动状态实现:
// 抽象状态类
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
的上下文引用变量,子类可以直接复用,将公共的setActivityContext
和nextState
方法放到抽象类中,以便子类复用.
4、具体状态类:
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、客户端调用:
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. 总结
状态模式将每一个状态定义为单独的状态对象,简化了多状态对象的复杂状态判断和状态转换逻辑,适用于状态多、转换逻辑变化频分的业务场景,如果状态少而且相对稳定,那么最好不用状态模式,因为它对开闭原则支持不友好,扩展状态修改的类较多,而且具体状态对象过多,也会提高系统的复杂性。