拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

前端精读:设计模式 State 状态模式 —— 用状态类替换 if-else 分支,实现行为随内部状态切换

前端精读:设计模式 State 状态模式 —— 用状态类替换 if-else 分支,实现行为随内部状态切换
  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载

State(状态模式)属于行为型模式,其核心价值在于:允许一个对象在其内部状态改变时改变它的行为,让对象看起来似乎修改了它的类。本文以前端精读周刊《设计模式 - State 状态模式》为主体,完整继承其团队接口人、台灯按钮、数据库连接器三个实战案例与 TypeScript 实现,并结合周刊仓库内 《robot 源码 - 有限状态机》 的源码级剖析,帮助读者在真实业务中判断"何时该用状态模式、何时该老老实实用 if-else",并掌握"多状态类 + 内部切换"的落地写法。

一句话理解状态模式

状态模式的本质,是将"一个大 class + 一堆 if else" 替换为 "一堆小 class":

  • 大 class:包含全部状态与全部分支逻辑,状态一变,判断逻辑成倍膨胀;
  • 一堆小 class:每个小 class 就是一个状态,每个状态只负责自己的行为,以及"如何切换到下一个状态"。

用一堆状态类代替 if-else 分支,会带来更好的可拓展性与可维护性:新增一种状态,只需新增一个类、改动相关状态的切换目标,而不是在一大段 if-else 里小心翼翼地插入新分支。

三个案例:什么时候会用到状态模式

设计模式只有在日常工作里用起来才有价值。以下三个场景分别从"职责分发"、"状态循环"、"生命周期"三个角度,帮助你识别状态模式的适用场景。

案例一:团队接口人(职责分发)

团队由很多同学组成,但对外只有一位接口人 TL。这位 TL 可能一会儿和产品经理谈需求,一会儿和其他 TL 谈规划,一会儿和 HR 谈人事,一个人显然忙不过来。

TL 的做法是:将任务分发给团队中每个同学,让他们不直接与产品经理、其他 TL、HR 接触。每位同学只负责一块具体的业务,TL 在不同时刻叫上不同的同学,让他们出面解决各自专业领域的问题。

这样从外部看,这位 TL 的团队能力很广;从内部看,每个人负责的事情非常单一。这与状态模式的思路如出一辙:对外是一个统一入口(TL / Context),对内把不同"状态"(同学)拆分,各自独立维护。

案例二:台灯按钮(状态循环)

我们经常见到只有一个按钮但可以调节亮度的台灯,其循环是:

关 -> 弱光 -> 亮 -> 强光 -> 关

每次按下按钮后要跳转到什么状态,和当前状态有关。这可以用 if-else 解决,也可以用状态模式解决:

  • 用 if-else:一个Lamp类内部维护currentState枚举,click()里写大量分支;
  • 用状态模式:将四个状态封装为四个类,每个类自己实现"按下按钮后跳转到哪个状态"。未来新增一种亮度模式,只需要改变部分类,而不需要改动所有分支。

案例三:数据库连接器(生命周期)

数据库连接器在连接前、连接中、连接后,其状态显然非常不同。如果仅用一个类描述连接器,内部免不了写大量分支语句做状态判断。

状态模式给出的方案是:创建多个不同状态类——连接前、连接中、连接后。这些状态类继承同一个父类/实现同一个接口,在不同时刻,Context 内部替换为不同的子类。对外,调用方不需要感知内部状态的变化(API 稳定);对内,状态被拆分,维护更清晰。

意图解释:重点是"内部状态"

意图:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

理解这个意图,关键在于"内部状态"三个字:

  • 状态改变是由对象内部触发的,而不是外部强制的;
  • 因此外部根本无需关心对象是否使用了状态模式。

以数据库连接器为例:无论这个类是用 if-else 堆砌的,还是用状态模式实现的,都完全不妨碍它对外提供稳定 API。状态模式的替换只发生在内部,所以它实质上是一种内聚的设计模式——把"状态相关的复杂逻辑"收敛在对象内部,对外隐藏细节。

结构图:State 与 ConcreteState

状态模式的经典结构包含两类角色(以台灯为例):

角色含义台灯类比
State状态接口台灯的"状态"抽象
ConcreteState具体状态,都实现 State台灯的强光、弱光等具体状态
  • State:定义状态行为接口,例如show()展示当前状态、click()响应按钮按下;
  • ConcreteState:继承/实现 State 的各个具体状态类,每个类内部持有 Context 引用,并在触发事件时通过context.setState(...)切换到下一个具体状态;
  • Context:持有当前状态对象,对外暴露统一 API(如click()),内部将行为委托给当前状态。

只要"具体状态都符合统一接口"这一点成立,就满足了状态模式的核心条件。

完整代码例子:TypeScript 实现台灯状态循环

下面的例子使用 TypeScript 编写,完整演示了"关 -> 弱光 -> 亮 -> 强光 -> 关"的状态循环:

abstract class Context { abstract setState(state: State): void; } // 定义状态接口 interface State { // 模拟台灯点亮 show: () => string } interface Light { click: () => void } // 状态与灯的复合类型 type LightState = State & Light class TurnOff implements State, Light { context: Context; constructor(context: Context) { this.context = context } show() { return '关灯' } // 按下按钮 public click() { this.context.setState(new WeakLight(this.context)) } } class WeakLight implements State, Light { context: Context; constructor(context: Context) { this.context = context } show() { return '弱光' } // 按下按钮 public click() { this.context.setState(new StandardLight(this.context)) } } class StandardLight implements State, Light { context: Context; constructor(context: Context) { this.context = context } show() { return '亮' } // 按下按钮 public click() { this.context.setState(new StrongLight(this.context)) } } class StrongLight implements State, Light { context: Context; constructor(context: Context) { this.context = context } show() { return '强光' } // 按下按钮 public click() { this.context.setState(new TurnOff(this.context)) } } // 台灯 class Lamp extends Context { // 当前状态 #currentState: LightState = new TurnOff(this) setState(state: LightState) { this.#currentState = state } getState() { return this.#currentState } // 按下按钮 click() { this.getState().click() } } const lamp = new Lamp() // 关闭 console.log(lamp.getState().show()) // 关灯 lamp.click() // 弱光 console.log(lamp.getState().show()) // 弱光 lamp.click() // 亮 console.log(lamp.getState().show()) // 亮 lamp.click() // 强光 console.log(lamp.getState().show()) // 强光 lamp.click() // 关闭 console.log(lamp.getState().show()) // 关闭

对照输出,整个调用链非常清晰:

  1. Lamp初始状态为TurnOff,getState().show()输出"关灯";
  2. 每次lamp.click()都会委托给当前状态的click(),由当前状态自己决定切换到下一个状态(TurnOff -> WeakLight -> StandardLight -> StrongLight -> TurnOff);
  3. 因为Lamp继承Context,每个具体状态都持有同一个context引用,切换时通过context.setState(...)完成内部状态替换。

需要注意代码中的几个关键设计点:

  • Context 抽象:Lamp继承Context,强制约定"任何具体状态都可以通过setState替换自己";
  • 状态持有 Context:每个 ConcreteState 构造函数接收context,才能在被触发时反向修改 Context 的内部状态;
  • 复合类型LightState = State & Light:保证Lamp持有的对象同时具备"展示状态"与"响应点击"两种能力。

其实实现方式有很多种,不必拘泥于形式,大体上只要保证两点即可:由多个类实现不同状态;每个类自己实现"切换到下一个状态"的逻辑。

源码佐证:有限状态机与状态模式的关系

状态模式与"有限状态机"(Finite State Machine)是一对紧密相关的概念。周刊仓库中的 《robot 源码 - 有限状态机》 从源码层面剖析了有限状态机的实现,恰好可以作为理解状态模式底层机制的补充:

  • createMachine(current, states, contextFn):创建状态机,保存context(内部数据)、current(当前状态名)、states(全部状态描述)三个属性。若只传入状态对象,则取Object.keys(states)[0]作为初始状态。这与台灯例子中Lamp初始化#currentState = new TurnOff(this)是对应的——状态机/Context 需要知道自己当前处于哪个状态。
  • state(...args):描述"一个状态支持哪些转换",并从中过滤出transition(普通转换)与immediate(立即转换)。若参数数量为 0,则说明这是最终态,无法再转换。
  • transition(from, to, ...args):定义"从某状态触发某事件后到达的目标状态",其中from为null即表示立即转换(immediate)。转换还可以携带guards(守卫,执行成功才允许转换)与reducers(修改状态机内部数据)。
  • transitionTo(service, fromEvent, candidates):真正执行状态切换的函数,遍历候选转换,通过guards(context)校验守卫,通过service.context = reducers.call(...)更新内部数据,最后生成一个current指向新状态的状态机对象。

对照可见,状态模式与有限状态机的思想同源:都强调"状态集合 + 状态切换规则 + 内部触发"。区别在于:

  • 状态模式更偏"面向对象"的表达:每个状态是一个类,行为内聚在类中(本文台灯例子即如此);
  • 有限状态机更偏"数据驱动"的表达:状态与转换被建模为数据(states对象 +transition描述),由统一的引擎负责调度(robot 即如此)。

在实际业务中,二者可互为补充:状态清晰、转换规则固定的场景,用有限状态机管理工具可以"先罗列全部状态、避免遗漏,且转换安全(错误状态下发错误指令不会产生异常)";状态行为复杂、希望逻辑内聚的场景,则可以用经典状态模式。

与策略模式的区分

状态模式与策略模式结构上非常相似(都是"接口 + 多个实现类"),但意图不同,容易混淆。仓库内 《设计模式 - Strategy 策略模式》 明确了两者的差异:

  • 策略模式:定义一系列算法并封装,使它们可以相互替换,算法可以独立于使用它的客户而变化(如地图导航的步行/骑车/开车/公交,布局的栅格/流式适配)。策略由外部注入,可随时替换;
  • 状态模式:状态改变由对象内部触发,外部无需感知。每个状态还掌握"切换到下一个状态"的规则(如台灯从弱光到亮到强光)。

简言之:策略是"客户选择方案",状态是"状态决定后续"。策略模式的代码例子可参考:

interface Strategy { doSomething: () => void } class Strategy1 implements Strategy { doSomething: () => { console.log('实现方案1') } } class Strategy2 implements Strategy { doSomething: () => { console.log('实现方案2') } } // 使用:外部注入不同策略 new System(new Strategy1()) // 策略1实现的系统 new System(new Strategy2()) // 策略2实现的系统

可以看到,策略模式的"切换"发生在构造时(外部决定),而状态模式的"切换"发生在内部(context.setState由状态类自己调用)——这是判断二者最直观的分界线。

弊端:该用 if-else 时就要用 if-else

状态模式并非万能,原文明确提醒:该用 if-else 的时候还是要用,不要但凡遇到 if-else 就使用状态模式,那样就是"书读傻了"。

使用状态模式前,一定要判断:

  1. 各状态间差异是否很大——只有差异大、行为复杂度高时,拆分才有价值;
  2. 使用状态模式后维护性是否真的比 if-else 更好——如果分支简单清晰、状态数量少,if-else 反而更直观。

同样地,策略模式也存在类似警示:不要每个分支都套一层策略模式,否则策略类数量会失控。当分支逻辑简单、清晰、好维护时,直接使用条件分支即可。模式是工具,不是教条,滥用比不用更糟。

总结

在合适场景下,状态模式可以带来以下收益:

  • 更符合开闭原则:新增状态时,只需新增一个状态类、调整相关状态的切换目标,无需改动 Context 和其他状态类;
  • 每个类逻辑更精简、聚焦:单个状态类只关心"自己是什么、按下按钮后去哪",复杂度被均摊到多个小类中;
  • 对外 API 稳定:内部状态如何切换对外完全透明,是一种高内聚、低耦合的设计。

判断是否采用状态模式的检查清单:

信号倾向
状态数量少、分支简单清晰直接用 if-else / switch
状态数量多、各状态行为差异大考虑状态模式
未来很可能新增状态状态模式(符合开闭原则)
状态切换规则复杂、需要守卫校验考虑有限状态机(见 robot 源码解读)

延伸阅读

  • 本文主体出处:设计模式/186.精读《设计模式 - State 状态模式》.md
  • 有限状态机源码剖析(状态模式的工程化实现):源码解读/122.精读《robot 源码 - 有限状态机》.md
  • 与状态模式极易混淆的兄弟模式:设计模式/187.精读《设计模式 - Strategy 策略模式》.md
  • 更多设计模式系列文章见 readme.md 的"设计模式"目录
  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表