广州总部电话:020-85564311
广州总部电话:020-85564311

广州网站建设-小程序商城开发-广州小程序开发-企业微信开发公司-网站建设高端品牌-优网科技

20年
互联网应用服务商
请输入搜索关键词
知识库 知识库

优网知识库

探索行业前沿,共享知识宝库

【第3501期】别再写一堆 if-else 了:用状态模式优雅管理状态行为
发布日期:2025-05-06 17:51:50 浏览次数: 820 来源:前端早读课

前言

介绍了状态模式(State Pattern)的概念及其在管理对象动态行为中的应用。今日前端早读课文章由 @Maxim Gorin 分享,@飘飘翻译。

译文从这开始~~

状态设计模式(State Design Pattern)是一种行为型软件设计模式,它允许对象在其内部状态发生改变时调整自身的行为。简单来说,状态模式让对象根据当前的状态表现出不同的行为,而不用在代码中堆满 if/else 或 switch 语句。

在我们之前的文章《为什么命令模式比你想象的更有用》中,我们探讨了如何将动作封装为对象以提升代码的灵活性。状态模式采取了类似的思路,不过它专注于将 “状态” 和 “行为” 封装为独立的对象。和命令模式一样,状态模式有助于我们减少大量条件语句,并遵循良好的设计原则 —— 但它解决的是另一类问题。

一个现实生活中的类比:手机的通知模式

想象一下,你有一部智能手机,它有多个通知模式:正常、振动、静音。

  • 在 “正常” 模式下,来电会响铃;
  • 在 “振动” 模式下,手机不会响铃,而是震动;
  • 在 “静音” 模式下,既不会响铃也不会震动 —— 可能只是记录一个未接来电。

你(作为手机的使用者)可能会根据不同场景(比如上班、开会、看电影等)手动切换这些模式,而手机的行为会随之改变,而无需你每次都去修改手机内部的响铃机制。

这个例子就是状态模式的一个贴切类比:

  • 手机是那个行为会根据状态变化的对象;
  • 当前的模式(正常 / 振动 / 静音)就是手机的内部状态;
  • 每种状态定义了手机在特定操作下应该如何响应(比如接到电话时怎么做);
  • 切换模式,其实就是更换内部的状态对象,从而改变了手机的行为。

那为什么不用 if-else 或枚举呢?你当然可以用简单的 if 或 switch 来处理手机的行为:

if(mode ==NORMAL){
   响铃
}else if(mode ==VIBRATE){
   震动
}else if(mode ==SILENT){
   保持安静
}

这个方式在模式不多的时候确实没问题。但想象一下,如果手机有十几种模式,每种模式还要影响多个行为(比如来电、短信、闹钟、通知等),那么条件分支会迅速增多,而且每种模式的逻辑会分散在代码中的各个 if 语句里,维护起来就非常容易出错。

【早阅】用CSS混合模式创建惊人视觉效果的实用指南

而状态模式提供了一种更清晰的方案:将每种模式都作为一个独立的状态类,里面包含各自的逻辑。手机只需要持有一个状态对象的引用(比如 SilentState 或 VibrateState 的实例),并将行为委托给这个对象。当你切换模式时,实际上就是更换状态对象,这样就避免了大量的条件判断,而是依靠多态来实现 —— 每个状态类知道在该状态下该如何处理具体的动作。

状态模式的工作原理

状态模式包含几个关键组成部分:

  • 上下文(Context)- 就是那个拥有动态内部状态的主要对象。在前面的类比中,手机就是上下文。
  • 状态接口(或抽象类)- 定义所有状态类共有的接口,声明了上下文想要委托给状态对象的一些操作方法。例如,一个 PhoneState 接口可能会声明一个 handleIncomingCall() 方法。
  • 具体状态类(Concrete State Classes)- 这些类代表特定的状态,每个类实现状态接口,并提供该状态下的具体行为,比如 NormalStateVibrateStateSilentState 分别定义了接电话时的不同响应方式。
  • 状态切换 - 上下文通常会有一个方法用于更换当前状态。这种切换可能是外部触发的(比如用户手动换模式),也可以是内部逻辑决定的(某个状态对象决定切换到其他状态)。

在状态模式中,当上下文对象接收到某个请求(比如 phone.receiveCall()),它并不会自己处理这个请求,而是将其委托给当前的状态对象(比如 currentState.handleIncomingCall())。由于每个状态对象对这个方法的实现都不同,所以即便调用的是同一个方法,结果也会因当前状态的不同而有所区别。

这就是多态在起作用:一个方法调用,根据具体的状态对象不同,表现出不同的行为。

避免条件语句过于复杂

使用状态模式的主要动机之一,就是为了消除代码中重复且分散的条件逻辑。如果一个对象的行为会根据状态变化而改变,你可能会倾向于用枚举或布尔标志来跟踪状态,然后在每个需要根据状态处理的地方写 switch 或 if 语句。这种做法会让代码变得臃肿、难以维护。

状态模式的做法是把每种状态下的逻辑封装在独立的类中:

  • 每种状态的逻辑都放在自己专属的类中(比如 “静音模式” 的所有逻辑都放在 SilentState 里)。
  • 上下文(Context)对象的代码会变得更简单,不再需要处理大段的条件判断逻辑。
  • 增加新的状态或修改已有状态,不需要在多个地方修改庞大的 switch 语句 —— 只需新增或修改一个状态类。

经典定义中提到:“当某些操作包含大量依赖于对象状态的条件语句时,状态模式会将每个条件分支封装在独立的类中,把状态当作一个独立的对象来看待。”

这种封装方式符合开闭原则(Open/Closed Principle):我们可以在不修改原有上下文或其他状态类的情况下引入新的状态。同时也符合单一职责原则(Single Responsibility Principle),因为每个状态类只负责处理一种状态下的行为。

什么时候该使用(或不使用)状态模式

适合使用状态模式的场景:
  • 当一个对象的行为依赖其当前状态,并且它在运行时需要根据状态改变行为时。如果你发现自己在多个地方都写着 “如果状态是 X 做这个,状态是 Y 做那个”,那可能就适合用状态模式。
  • 当一个对象有多个行为逻辑,并且这些逻辑可以明确地按状态划分。例如,手机的响铃、震动、静音记录等行为都可以独立处理。
  • 想要避免状态判断逻辑重复出现在多个方法中。使用状态模式后,这些行为被集中封装在状态类中,不再重复。
  • 预计未来可能会增加新的状态,或每个状态下的逻辑会变得更复杂。状态模式的结构更容易扩展(新增一个状态类)或修改(只需改动一个类的代码)。
不适合使用状态模式的情况或需谨慎使用:
  • 如果对象只有一两个状态,而且每种状态下的行为差异非常简单,那么使用状态模式可能就有点小题大做了。用普通的条件判断反而更清晰。
  • 如果状态切换很少发生,或者每种状态的逻辑基本不会变,那用状态模式引入的一堆类可能并不值得。
  • 如果状态数量固定且逻辑简单明确,使用枚举加 switch 语句可能就足够了。状态模式适用于那些状态复杂且易变的场景。

可以这样想:一个只有两个状态的小状态机,用 if 来管理也没问题。但如果是一个有十种状态、状态之间还有复杂切换逻辑的状态机,那用状态模式结构化处理会更好维护。

【第3445期】React 设计模式:实例钩子模式

为什么状态模式比枚举和标志变量更好?

一开始,很多人会选择用枚举或布尔标志来表示状态,比如:

enum Mode { normal, vibrate, silent }

然后用类似这样的逻辑处理行为:

if(mode == Mode.normal){
// 响铃
}else if(mode == Mode.vibrate){
// 震动
}else if(mode == Mode.silent){
// 保持静音
}

这种方式起初是可行的,但随着程序变复杂,会出现以下问题:

  • 逻辑分散 - 如果多个行为都依赖状态判断,你就会在很多方法里看到类似的 if/else 或 switch,例如 handleCall()notifyMessage()alarmRing() 等等。状态行为稍有改动,就得到处找这些条件语句并改动。
  • 违反开闭原则 - 比如你想新增一个 “请勿打扰” 模式(Do Not Disturb),就得修改所有相关的 switch 语句。每次修改都有可能引入 bug,影响原有功能。
  • 维护困难 - 状态和条件越来越多,代码就越难阅读和维护,容易变成一个嵌在业务逻辑中的 “巨型状态机”。

状态模式通过封装各个状态的行为,解决了这些问题。你不再需要一个大函数来处理各种分支,而是有多个小类,各自处理自己的状态行为。这样结构更清晰:

  • Phone
     类(上下文)无需了解各个模式的具体行为,它只需把行为委托给当前的状态对象。
  • 想增加一个 “请勿打扰” 模式,只需新增一个 DoNotDisturbState 类,定义好它的行为。Phone 类可能只需做一点小改动,甚至不改(如果状态可以通过 setter 或工厂设置)。
  • 删除或修改某个状态行为,只需要修改该状态类,不会影响到其他代码,降低了出错风险。

简而言之:在复杂场景中,状态模式比枚举 / 标志 + 条件判断更健壮、更灵活。它让代码模块化,符合设计原则,也更方便多个开发者(前端、后端、移动端等)理解和协作,不用去翻那些密密麻麻的条件语句。

状态模式的优缺点

像所有设计模式一样,状态模式也有优劣之分。我们来具体看一下:

优点:
  • 代码结构更清晰 - 每种状态对应一个独立的类,满足单一职责原则。每个状态类只负责一种状态下的行为。
  • 消除复杂的条件语句 - 上下文对象不再被 if/else 或 switch 语句包围,结构更简单,维护起来更轻松。
  • 更符合开闭原则 - 添加新状态无需修改原有代码,特别是上下文对象或其他状态类,扩展性好。
  • 状态切换逻辑集中管理 - 可以在状态类或上下文中集中处理状态之间的转换,流程更易管理和理解。
  • 支持多态行为 - 通过运行时替换状态对象来改变行为,其他系统部分不需要感知变化,降低出错率。
缺点:
  • 类数量增加,结构变复杂 - 每种状态都需要一个类,对于简单场景来说,这可能是 “用大炮打蚊子”,过度设计。
  • 状态爆炸问题 - 如果一个对象有很多种状态,就会出现大量状态类,状态间的切换也变得复杂。(解决办法:可以分组、分层,或重新思考是否真有那么多必要的状态)
  • 状态与上下文之间耦合 - 状态类通常需要知道上下文对象的情况,甚至可能要知道其他状态类的信息,这会引入耦合。好在这种耦合是局部且可控的,通常是可以接受的权衡
  • 学习成本 - 对一些不熟悉设计模式的开发者来说,“对象中还有另一个对象来处理逻辑” 这个思路可能不太直观。对比直观的 if 条件语句,状态模式可能需要点时间适应。
  • 内存 / 性能开销 - 在某些语言中,频繁创建状态对象可能会有一点性能损耗(不过大多数情况下可以忽略)。如果状态对象包含大量从上下文复制的数据,可能会导致效率问题。但通常状态类都很轻量,甚至可以使用单例模式来复用,因此一般不会成为瓶颈。
应对这些缺点的建议:
  • 担心类太多?可以将状态类写成内部类,甚至匿名类(如果语言支持),这样可以把它们和上下文放在一起。
  • 担心对象创建成本?可以复用状态实例,状态模式并不要求每次都新建对象。你可以用单例或无状态对象。
  • 担心别人看不懂?使用清晰的命名和注释,让每个状态类的职责一目了然,降低理解门槛。

用 Dart 实现状态模式(以手机模式为例)

为了加深理解,我们用 Dart 来实现前面讲到的智能手机通知模式的例子。我们将创建一个简单的模拟程序,模拟手机在不同模式下接到电话的行为。代码是完整可运行的,可以在 Dart 在线编辑器或其他环境中直接运行并在控制台查看输出。

示例设计结构:
  • 我们会创建一个抽象类 PhoneState,定义当手机接到来电时应该执行的方法(onReceiveCall)。
  • 创建三个具体的状态类:NormalStateVibrateState 和 SilentState,分别继承 PhoneState 并实现各自不同的来电响应逻辑。
  • Phone
     类是上下文类,它持有一个 PhoneState 类型的状态属性。所有的来电处理都会委托给当前状态对象,同时它也提供方法来切换状态(setState())。
  • 我们将模拟手机在不同模式下接听电话的过程,以观察行为变化。

以下是 Dart 示例代码:

// 状态接口(在 Dart 中用抽象类表示)
 abstract class PhoneState{
  void onReceiveCall(Phone context);
}

// 具体状态类:正常模式(响铃)
class NormalState implements PhoneState{
   @override
   void onReceiveCall(Phone context){
      print("来电:铃铃铃!?(正常模式)");
      // 正常模式下手机响铃,不会自动切换状态
   }
}

// 具体状态类:振动模式
class VibrateState implements PhoneState{
   int _vibrateCount =0;// 模拟内部状态,例如振动次数

   @override
   void onReceiveCall(Phone context){
     _vibrateCount++;
     print("来电:嗡嗡嗡…… ?(振动模式)");
     // 如果振动次数过多,自动切换为静音模式(只是示例,现实中不会这样)
    if(_vibrateCount >=3){
       print("连续振动 $_vibrateCount 次未接听,切换为静音模式。");
       context.setState(SilentState());
    }
  }
}

// 具体状态类:静音模式
classSilentStateimplementsPhoneState{
   @override
   void onReceiveCall(Phone context){
      print("来电:(静音模式,无声音)?");
      print("手机保持静音,稍后可能会看到未接来电。");
   }
}

// 上下文类:手机
class Phone{
   // 默认从正常模式开始
   PhoneState _state =NormalState();

   void setState(PhoneState newState){
     _state = newState;
     // 可以在这里打印或记录模式变更(可选)
   }

  void receiveCall(){
     // 将行为委托给当前状态对象
     _state.onReceiveCall(this);
  }

   // 可选:获取当前状态名称用于打印日志
   String get modeName => _state.runtimeType.toString();
}

void main(){
   Phone phone =Phone();
   print("手机当前模式:${phone.modeName}");

   // 模拟来电(正常模式)
   phone.receiveCall();// 响铃

   // 切换到振动模式
   phone.setState(VibrateState());
   print("\n手机当前模式:${phone.modeName}");
   phone.receiveCall();// 第一次振动
   phone.receiveCall();// 第二次振动
   phone.receiveCall();// 第三次振动,触发自动切换为静音

   // 此时应自动进入静音模式
   print("\n手机当前模式:${phone.modeName}");
   phone.receiveCall();// 静音,不响铃

   // 手动切换回正常模式
   phone.setState(NormalState());
   print("\n手机当前模式:${phone.modeName}");
   phone.receiveCall();// 再次响铃
}
上面的代码要点:
  • Phone
     类(上下文)并不知道来电时具体该做什么,它只是调用 _state.onReceiveCall(this),由当前状态对象处理行为。这正是状态模式的核心。
  • 每个状态类只处理一种模式下的行为。比如 SilentState 只处理静音模式的来电情况。
  • VibrateState
     中加入了一个小彩蛋:如果连续接到 3 个电话都没有应答,它会自动切换为静音模式。这是为了演示状态对象内部可以触发状态切换的能力。
  • 切换状态的方式很简单:调用 phone.setState(SomeState()) 即可。你可以想象这种行为由用户操作或程序逻辑触发。

运行这段代码,会看到类似如下的输出:

 手机当前模式:NormalState
 来电:铃铃铃!?(正常模式)

 手机当前模式:VibrateState
 来电:嗡嗡嗡…… ?(振动模式)
 来电:嗡嗡嗡…… ?(振动模式)
 来电:嗡嗡嗡…… ?(振动模式)
 连续振动 3 次未接听,切换为静音模式。

 手机当前模式:SilentState
 来电:(静音模式,无声音)?
 手机保持静音,稍后可能会看到未接来电。

 手机当前模式:NormalState
 来电:铃铃铃!?(正常模式)

可以看到,每次状态变化后,手机的行为也随之改变。而这些行为的逻辑并不在 Phone 类中,而是被封装在各自的状态类中。这体现了状态模式的强大之处。

局限性和权衡取舍

虽然状态模式功能强大,但也并非万能:

复杂性 vs 简洁性

在使用前要权衡问题的复杂度。只有当状态模式能降低整体复杂度时才值得引入。如果感觉它反而增加了不必要的层级结构,那也许应该退一步,采用更简单的方案。一个常用的经验法则是:当你有两三个以上的行为分支,并且这些行为可能会扩展或变化时,就可以考虑使用状态模式。

状态切换逻辑的归属问题

设计时一个常见的挑战是:状态切换的逻辑应放在哪?在我们的示例中,是由 VibrateState 自己决定切换到 SilentState。在其他设计中,可能会让 Phone(上下文)来判断是否切换,这取决于具体业务。状态模式对此并没有硬性规定,你可以根据可读性和清晰度来决定。若切换逻辑太复杂,建议写好注释或简化切换规则。

【第3212期】在 React 中管理状态的 4 种方法

状态数量管理

如果你预计状态会爆炸式增长,那需要重新思考是否每个状态都值得用一个类。有时候看起来像 “多个状态” 的,其实可以合并处理,或者通过数据而非类来管理。比如手机的 10 级音量,不需要写 10 个状态类,而是把音量值作为一个参数放到 “正常模式” 类中就行。只有当状态间行为本质上不同时,才值得使用不同的状态类。

尽管存在这些考虑因素,状态模式仍是一种久经考验的工具。它使代码保持灵活且易于扩展。许多框架和库在内部使用状态模式或类似的概念(例如,用户界面组件通常具有启用 / 禁用 / 悬停等状态,这些状态在幕后通过状态对象或状态模式来实现)。

总结

状态设计模式能让对象更具灵活性、更易维护,它通过将状态相关的行为封装到独立的类中,实现了 “行为与状态解耦” 的目标。我们的手机模式示例展示了:一个设备如何通过切换内部状态对象,改变自己的行为(响铃、振动、静音),而无需在主类中堆积大量条件语句。

如果你正在开发一个系统,它涉及 “模式、阶段、条件行为” 的切换,状态模式是你值得掌握的一种工具。虽然初期设置可能略显麻烦,但随着项目增长,你会越来越体会到它带来的好处:关注点分离、逻辑清晰、可扩展性强。

关于本文
译者:@飘飘
作者:@Maxim Gorin
原文:https://maxim-gorin.medium.com/stop-writing-if-else-trees-use-the-state-pattern-instead-1fe9ff39a39c

图片
这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。

优网科技,优秀企业首选的互联网供应服务商

优网科技秉承"专业团队、品质服务" 的经营理念,诚信务实的服务了近万家客户,成为众多世界500强、集团和上市公司的长期合作伙伴!

优网科技成立于2001年,擅长网站建设、网站与各类业务系统深度整合,致力于提供完善的企业互联网解决方案。优网科技提供PC端网站建设(品牌展示型、官方门户型、营销商务型、电子商务型、信息门户型、DIY体验、720全景展厅及3D虚拟仿真)、移动端应用(手机站APP开发)、微信定制开发(微信官网、微信商城、企业微信)、微信小程序定制开发等一系列互联网应用服务。


我要投稿

姓名

文章链接

提交即表示你已阅读并同意《个人信息保护声明》

专属顾问 专属顾问
扫码咨询您的优网专属顾问!
专属顾问
马上咨询
联系专属顾问
联系专属顾问
联系专属顾问
扫一扫马上咨询
扫一扫马上咨询

扫一扫马上咨询

和我们在线交谈!