Design-Pattern-State
状态模式是一种行为设计模式,它允许对象在其内部状态改变时改变它的行为,对象看起来好像修改了它的类。
场景
笔者曾基于按键精灵开发过一些自动化软件,用于在PC端及移动设备上模拟用户点击与输入操作,从而自动完成游戏内的日常任务。其底层实现逻辑为:程序通过一个持续运行的死循环,定时截取当前屏幕画面,基于图像特征判断当前所处的游戏界面状态(如主菜单、战斗中、战斗结束等),随后依据预先配置的操作策略,执行对应的自动化动作——例如当识别到主菜单界面时,触发“开始战斗”按钮的点击操作;当检测到战斗结束界面时,则自动完成“结算”操作。在具体代码实现层面,这一系列界面状态判断与操作分发的逻辑,最初是通过传统的if条件分支或switch语句结构来完成的。
对于上述过程,其核心设计思想在于:程序在运行过程中始终仅处于若干种预定义的有限状态之一。针对每一种特定的状态,程序会展现出差异化的行为逻辑,并且能够在无需中间过渡过程的情况下,直接从一个状态切换至另一个状态。然而,状态的变更并非必然发生——程序可能基于当前状态触发向其他状态的跳转,也可能保持当前状态持续运行。这些预先设定且数量有限的、定义了”在何种状态下应如何转换至其他状态(或维持原状态)“的规则,被统称为状态转移(State Transition)。这类程序可以抽象为有限状态机。显然,有限状态机由有限个状态、状态下的行为、状态转移规则所定义。
随着向自动化软件添加了更多状态和行为,整个程序变得难以维护,原本简洁的if-else或switch-case会膨胀为冗长的嵌套分支。例如:
1 | |
状态模式
状态模式的核心思想是将对象的行为与其状态解耦,通过以下方式实现:
- 状态抽象化:为对象的每个可能状态创建一个独立的类,将原本分散在
if/switch分支中的状态相关行为抽取到对应的状态类中。每个状态类负责封装该状态下对象应表现的行为逻辑。 - 上下文委托:原始对象(称为上下文
Context)不再自行处理所有状态相关的行为,而是:- 持有一个表示当前状态的状态对象引用
- 将所有状态相关的操作委托给当前状态对象处理
- 提供状态切换接口供状态对象修改当前状态
- 状态转换机制:状态对象在执行行为逻辑后,可以:
- 保持当前状态不变(当无需切换时)
- 通过上下文切换到其他状态(当满足转换条件时)
显然需要定义一个状态接口,使得上下文可以方便的引用和切换不同状态。状态接口中需要声明哪些方法,通常取决于具体的业务场景以及上下文对象的行为变化。例如上文中的自动化软件的例子,程序仅根据所处状态决定执行逻辑,而不涉及复杂的业务操作。那可以仅简单声明一个handle方法。
然而,在更复杂的业务系统中,比如订单系统。订单有几种状态:待支付、已支付、已发货、已完成等。每种状态下,对某些操作(如支付、取消、发货)的响应是不同的。此时仅通过状态无法确定执行逻辑,还需要额外根据每一种可能的业务操作。因此需要在状态接口中声明pay()、cancel()、ship()等方法。
至此,有限个状态、状态下的行为已经定义完毕,还需要实现状态转移规则。每个具体状态中,需要根据执行逻辑的结果,切换到下一个状态,或继续保持当前状态。要在状态类中切换上下文的状态,那么需要上下文类需要提供public setter。而状态类如何引用上下文对象,有两个策略:通过构造函数和成员变量引用、通过方法的入参引用。如果通过成员变量引用,意味着状态对象和某个具体上下文对象绑定了,无法构建上下文和状态多对多的关系,也不利于复用状态对象。所以笔者还是更建议通过方法入参传递上下文对象引用。
基于上述描述,可以给出如下类图。
classDiagram
direction LR
class Context {
-state: State
+Context(initialState: State)
+setState(state: State)
+request()
}
class State {
<<interface>>
+handle(context: Context)
}
class ConcreteState {
+handle(context: Context)
}
class Client {
}
Context --o State : 维护当前状态
State <|-- ConcreteState
Context <-- ConcreteState
Client --> Context
Client --> ConcreteState
总结
当代码中出现复杂的if/switch语句,可以考虑使用状态模式。
状态模式中,代码中的不同if/switch分支被抽取为状态类。在原上下文对象中通过引用和切换状态对象,来表现出不同行为。
每个状态类中,需要定义具体的执行逻辑,并根据执行结果决定下一个状态。