状态模式的针对性很强,当有状态变化的时候很多工程师会倾向于选择状态模式,但在使用时仍然存在着很多问题,比如状态的拆分不清晰、状态的变迁不准确、类的职责划分不单一等,一旦与具体的业务逻辑和已有的代码上下文放在一起,就出现了混乱。因此,本文以一个实际的例子来总结一下在现实当中如何使用状态模式。
1.什么是状态模式
状态模式(State Pattern)是设计模式的一种,属于行为模式。其定义(源于Design Pattern)是当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
状态模式的主要适用场景有:
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支结构,并且这些分支决定于对象的状态。
2.使用状态模式的前提条件
从定义中可以看到,状态模式的目的是为了将状态与不同状态下的行为进行隔离,从而简化复杂的条件判断,所以在决定是否选用状态模式的时候,就要从这个复杂状态变化入手。我推荐工程师的第一作法就是画个状态图出来。如果这个状态图能画出来,并且有一定的复杂度,则可以考虑选择状态模式,否则不如果断弃用。任何模式都有它的适用场景,也可以理解成性价比吧。用一个模式就会增加成本,也会带来收益。这是要认真思考的。对于只有2-3种状态的,如果用了模式,会增加出至少3-4个类,而且会增加类间耦合,某些工程师的实现阶段还可能明显降低代码的可理解性,这样就得不偿失了。或者这些状态只是起个标识作用,在不同的状态无需改变对象的行为,那也不必把状态模式祭出来,直接用常量或枚举就可以解决问题了。
3.状态模式的使用
以下我们需要一个实例来描述状态模式的使用。
假设我们有一个简化版的业务描述:某软件可以提供文件上传和下载的服务,无论是上传任务还是下载任务,都能够响应用户的启动或取消的动作。用户选好文件以后,点击开始即启动任务,进行网络传输,在此过程中,用户也可以点击取消来中止任务,被取消的任务将还原为初始状态等待用户重新启动。
在确定选用状态模式以后,就可以进行下一步的设计工作了。建议采用如下的思路进行。
3.1.建立状态图
根据业务描述,建立状态图如下图所示。
3.2.设计类图
根据状态图,设计出如下的类图。
这个类图是与书中的状态模式的类图一致的,到目前为止,我们一直按照教科书上的设计在划分类和职责,比如Task类的start()方法就是简单的委托给了State的start()方法去执行。
public void start(){
currentState.start();
}
3.3.处理状态迁移
接下来就是要把状态图中的状态迁移加以实现。以PendingState的代码为例。
public classPendingState extends State {
public PendingState(Task task){
super(task);
}
public void start() {
task.changeCurrentState(task.getRunningState());
}
public void cancel() {
//Do nothing
}
}
在Pending状态下,如果执行start则将task的当前状态修改为Running状态,如果执行cancel操作则不做任何事情。其他的状态变迁以此类推。
3.4.实现具体的动作
现在我们需要做的就是实现真正的功能,在这个例子中也就是网络传输的动作了。在做这个事情的时候,我们面临着一个选择,按教科书中的示例,这些具体的执行应该放在State中去实现,但在实际情况中,能否放在Task中去实现呢?
3.4.1.由State来实现动作
如果把实现放在State中,那么每个具体的State就不用去判断当前的状态、上一次的状态这些复杂且易出错的逻辑了,只要确保在当前状态下的实现就可以了。但对于本例中的每个状态能允许用户执行两种操作的情形而言,我们要处理的事情相对来说要复杂得多,至少在cancel的时候要把running时启动的http request取到并对其执行取消网络请求的操作。这是同一操作对象(httpRequest)在不同State类间传递的问题。
由此还有其他类似的问题,比如Task类持有着上传下载所需的参数,例如请求类型、资源路径等,这些都要在State子类中可以获取。还有在实际当中往往要把任务状态、进度等信息再通过其他方式上报给上层逻辑或UI展现,假设通过Observer模式实现的话,那么Subject往往会选择Task,因此掌握具体情况的State就要同时操心很多这种问题。
3.4.2.由Task来实现动作
从上文的分析来看,具体的动作放在Task中实现是比较方便的,参数在Task中,HttpRequest也在Task中,对外的Listener维护及回调也在Task中,利于Client理解和使用,便于维护。同时在内聚性上有所提高。
因此,本例中与教科书不同,根据实际情况进行了设计调整,调整以后的类图如下所示。
PendingState的start()方法的代码变更为:
public void start() {
task.changeCurrentState(task.getRunningState());
task.performStart();
}
由Task的子类负责perfomrStart()的实现。
3.5.再说状态迁移
仔细分析,还可以注意到,实际上状态迁移是分两种的,一种是由外界触发引起的状态迁移,即前文说由用户执行了start、cancel引起的状态迁移。另一种是任务执行过程中内部引起的状态迁移,比如一个Running状态的任务顺利执行完毕以后要把状态变更为Finished。对于这种自然迁移的状态,在实现的时候还是要考虑一下如何处理更加方便的。
我们自然希望对状态的迁移都集中在State当中,这样可以充分利用分离出来的State类。比如:
public void start() {
task.changeCurrentState(task.getRunningState());
task.performStart();
task.changeCurrentState(task.getFinishedState());
}
但这样做的前提是performStart()得是一个同步方法。如果是异步的,显然在动作没执行完前就将状态置为Finished是错误的,这种情况下的状态变化由performStart()方法来切换比较方便。即在performStart()方法中实现这种内部的状态迁移。比如:
void performStart() {
//...success
changeCurrentState(getFinishedState());
}
4.小结
状态模式的针对性很强,易于识别与使用,但在使用中要注意总结自己的思路,尤其重要的是根据实际情况进行变通。教科书中的代码示例、类图示例都是为了说明该设计方案的意图的,而不是一个标准的模板,如果一味的照搬照抄,那就失去了设计模式的本意。要充分理解模式的意图然后在实际当中应用,就要从教科书中跳出来,用自己的思维才能把设计模式变成自己的模式。