state image

(图片来源网络)

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

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

1. 活动状态案例

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

state activity flow
Figure 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):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

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

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

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

2.2. 状态模式结构

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

state class
Figure 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. 状态模式优缺点

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

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

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

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

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

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

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

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

state activity use pattern
Figure 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的上下文引用变量,子类可以直接复用,将公共的setActivityContextnextState方法放到抽象类中,以便子类复用.

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. 总结

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


相关阅读