小时候,每当我骑车上学时,妈妈总会叮嘱我:上坡要加速,下坡则要减速。
如果将上坡和下坡看作是所处的状态,那么加速和减速就是与之对应的行为。刚好,状态模式就能很好的描述这个过程,它可以实现对象处在不同状态时,表现出不同的行为。接下来,我们将通过一个例子深入探讨状态模式。
假如我们现在正在构建一款文稿工具,这个工具可以供用户编辑文稿并发布到平台中,以便让更多用户从这份文稿中获益。一个文稿的所有状态及转换关系如下图所示:
我们知道,状态也是文稿对象的一部分。所以,在最初的设计中,我们可能会为每一个状态设定一个状态值。确定一个文稿的状态,只需要知道文稿的状态值。
public class Document {
private int state; // 状态值
private String title; // 标题
private String content; // 正文
}
另外,还应该为文稿类提供一些操作,这些操作既可能让用户修改文稿的数据,也可以转换文稿的所处状态。比如审核操作,系统根据一些原则对该文稿进行审核,如果审核通过,则该文稿将发布到平台中供其他用户查阅;但如果不符合通过的条件,则该文档将重新进入草稿状态,用户可以对其进行修改,以便再次投稿。
public void audit() {
// 是否可以进行审核
if (state != "审核中") {
// 不允许其他状态进行审核操作
return;
}
// 系统审核是否通过
boolean passed;
...
if (passed) {
// 审核通过
state = "已发布";
} else {
// 审核失败
state = "草稿";
}
}
这个设计的问题是,条件分支臃肿且复杂。如果我们需要添加一个状态或者操作时,不得不改动条件分支,或者增加更多的条件分支,每次的改动都可能对整个类产生影响。尽管在项目初期,我们的状态和操作较少,看起来各个操作中的条件分支还算简洁,但是随着项目的进行,越来越多的状态和操作加入,这将导致行为中的条件分支越来越多,看起来就像是一团乱麻。
想要解决问题,就得先分析问题。为什么条件分支会越来越臃肿?因为对于任意一个操作,我们必须对文稿当前所处的状态进行检查。比如说撤稿操作,对于系统来说,只允许文稿当前是‘已发布’和‘已下架’的状态进行,对于其他的状态来说,这一行为都是非法的。所以,每新增一个状态,我们就不得不考虑在这个状态下,每一个操作所表现出的行为。就像下图这样:
我们发现,每个操作都为不同的状态提供了相应的表现行为。也就是说,想要解决条件分支的问题,我们必须把状态从操作中抽离出来。我们可以将状态硬编码成为一个个的状态对象,每一个状态都包含了文稿对象的所有操作,每一个状态的每一个操作中,只需要表现出文稿对象处于当前状态下的行为。这就是状态模式所关注的核心。
如上类图所示,状态接口中定义了所有对于文稿对象的操作:
- 【stateName():String】:获取状态的名称;
- 【saveDraft(title, content)】:保存草稿,提供参数包含文稿标题和文稿正文;
- 【contribute()】:投稿;
- 【audit()】:审核,系统根据当前文稿是否包含敏感信息进行审核;
- 【revoke()】:撤稿;
- 【remove()】:下架;
文稿对象(Document)持有一个当前状态对象(currentState)的引用,客户端对于文稿对象的所有操作都将被委托给当前状态对象执行。如果该操作涉及到状态的转换(比如对处于已发布状态的文稿对象发起下架操作后,当前文稿对象的状态将变成已下架),可使用文稿对象的状态转换方法(nextState())改变对象的状态。值得一提的是,这个例子中,文稿对象和状态对象具有同样的行为,所以此处文稿类也继承自状态接口,但状态模式并未约定他们必须有此关系。
代码层次及类说明如上所示,更多内容请参考案例代码。客户端示例代码如下
public class Client {
public static void main(String[] args) {
System.out.println("|==> Start -------------------------------------------------------------------------|");
// 新建文稿
Document doc = new Document("四川13所高校外籍师生走进三星堆等地领略巴蜀文明", "ssss");
// 重新编辑 再次保存
doc.saveDraft("四川13所高校外籍师生走进三星堆等地领略巴蜀文明", "7月14日,“文化中国·锦绣" +
"四川——高校外籍师生巴蜀文化品悟之旅”活动走进四川德阳市绵竹年画村、广汉三星堆博物馆。" +
"来自四川13所高校的26个国家的48名外籍师生在绵竹年画村提笔绘制年画,体验中国非物质文化魅力...");
// 投稿
doc.contribute();
// 撤稿
doc.revoke();
// 审核
doc.audit();
// 下架
doc.remove();
}
}
运行结果如下
|==> Start -------------------------------------------------------------------------|
状态转换:【草稿】 -> 【审核中】
非法操作:审核中状态不允许撤稿
审核通过
状态转换:【审核中】 -> 【已发布】
状态转换:【已发布】 -> 【已下架】
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
结合上面的案例来说,状态模式的意图指的是一个对象,在不同的状态下进行同一操作,却具有不同的行为。就像是处于草稿状态(DraftState
)的文稿对象,进行保存草稿操作(saveDraft()
)和处于已发布状态(PublishedState
)时变现出了不同的行为。前者会将文稿对象更新到最新,而后者只会得到一个非法操作的提示。
状态模式的类图结构如上所示,在状态模式中,有如下的参与角色:
- Context:定义客户端感兴趣的接口,并且维护一个 State 的实例,这个实例表示着当前的状态,并且客户端对于 Context 的操作将委托给该实例进行执行;
- State:状态定义,定义与 Context 的一个特定状态相关的行为;
- ConcreteState:每一个状态的子类对 State 中定义的所有行为进行实现。
(1)对象需要根据自身当前的状态表现出不同的行为
模式建议你将所有独立于状态的代码抽取到一组独立的类中。这样一来,你可以在独立于其他状态的情况下轻松添加新状态或修改已有状态。
(2)某个类在为变量的值构建大量的条件分支
状态模式会将这些条件语句的分支抽取到相应状态类的方法中,这样不仅解决了大量的条件分支,并且还能让各个状态相对独立的变化。
(1) 谁实现状态转换
通常有两种方式定义状态转换,一种是在 Context 中定义,而另一种是在各个状态类中定义。
- 对于一个涉及到状态转换的操作来说,如果后继状态是固定的(比如处于已发布状态的下架操作,后继状态固定为已下架),那么可以在 Context 中定义实现。但是大多数情况下,对于一个操作的后继状态往往需要依据当前操作的执行结果(比如处于审核中状态的审核操作来说,会根据审核的结果转换到不同的状态中,审核成功将变成已发布状态,审核失败将回到草稿状态),此时我们不得不在 Context 中处理状态对象返回的不同结果,这无疑增加了复杂度;
- 另一种实现方式是在状态类中进行定义,这就要求状态类需要持有 Context 的引用,并且 Context 还需要提供一个设置状态的接口。当状态类确定好后继状态后,可以通过 Context 提供的接口设置后继状态。这种方式非常灵活,并且往往更加容易实现。但缺点也很明显,一个 ConcreteState 至少拥有一个其他子类的信息(比如,UnderReviewState 就需要依赖 DraftState 和 PublishedState 两个状态子类),这就让各个子类之间产生了依赖。
我们在本章的案例中,就采用了在各个状态类中实现状态转换的方式。
(2)State 可被共享
如果 State 对象没有实例变量,或者 State 对象的实例变量不可变,此时可以采用共享的 State。对于一个没有内部状态的对象来说,他是线程安全的,所以可以被多个 Context 所共享。此时可以从享元(Flyweight)模式中受益。
(3)客户端不应与 State 对象打交道
对于客户端来说,他只应和 Context 对象产生交集,客户端所有可能的行为都应实现在 Context 中。有一种情况除外,如果客户端需要使用一个状态来装配 Context,此时客户端不得不为 Context 提供一个初始的状态。一旦 Context 配置完成,它的客户端不必再和状态对象打交道。