阅读视图

发现新文章,点击刷新页面。

别再写一堆 if-else 了:用状态模式优雅管理状态行为

前言

介绍了状态模式(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 语句里,维护起来就非常容易出错。

而状态模式提供了一种更清晰的方案:将每种模式都作为一个独立的状态类,里面包含各自的逻辑。手机只需要持有一个状态对象的引用(比如 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 来管理也没问题。但如果是一个有十种状态、状态之间还有复杂切换逻辑的状态机,那用状态模式结构化处理会更好维护。

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

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

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(上下文)来判断是否切换,这取决于具体业务。状态模式对此并没有硬性规定,你可以根据可读性和清晰度来决定。若切换逻辑太复杂,建议写好注释或简化切换规则。

状态数量管理

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

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

总结

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

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

关于本文
译者:@飘飘
作者:@Maxim Gorin
原文:maxim-gorin.medium.com/stop-writin…

vue 入门到实战 一

第1章 初始Vue.js

1.1 网站交互方式

Web网站有单页应用程序(SPA,Single-page Application)和多页应用程序(MPA,Multi-page Application)两种交互方式。

多页应用程序,顾名思义是由多个页面组成的站点。在多页应用程序中,每个网页在每次收到相应的请求时都会重新加载。多页应用程序很大,由于不同页面的数量和层数,有时甚至可以认为很麻烦,我们可以在大多数电子商务网站上找到MPA的示例。

多页应用程序以服务端为主导,前后端混合开发,例如:.php、.aspx、.jsp。技术堆栈包括HTML、CSS、JavaScript、jQuery,有时还包括AJAX。

图片

单页应用程序,就是只有一张Web页面的应用。单页应用程序是加载单个HTML页面并在用户与应用程序交互时,动态更新该页面的Web应用程序。浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。单页应用开发技术复杂,所以诞生了许多前端开发框架:Angular.js、React.js、Vue.js等。

选择单页应用程序开发时,软件工程师通常采用以下技术堆栈:HTML5、Angular.js、React.js、Vue.js、Ember.js、AJAX等。

图片

1.2 MVVM模式

MVVM是Model-View-ViewModel的缩写,它是一种基于前端开发的架构模式,其核心是提供对View和ViewModel的双向数据绑定,这使得ViewModel的状态改变可以自动传递给View,即所谓的数据双向绑定。

在MVVM架构下,View和Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model和ViewModel之间的交互是双向的,因此View数据的变化会同步到Model中,而Model数据的变化也会立即反应到View上。

图片

1.2.1、Model

模型(Model):对应data中的数据,一般JS对象

data: {a'',address: '',name: ''}

data中书写的Key:Value都会出现在Vue实例VM身上

图片

1.2.2、View

视图(View):对应模板(DOM)

<div id="root"><h2>{  { a }}</h2><h2>{{ address }}</h2><h2>{  { name }}</h2></div>

1.2.3、ViewModel

视图模型(ViewModel):对应Vue实例对象(VM)

3、ViewModel

视图模型(ViewModel):对应Vue实例对象(VM)<script type="text/javascript">// ViewModelnew Vue({.   // Viewel: '#root', // Modeldata: {  a: '',address: '', name: ''})</script>

1.3 Vue.js是什么

Vue(读音/vjuː/,类似于view)是一套构建用户界面的渐进式框架。与其它重量级框架不同的是,Vue.js采用自底向上增量开发的设计。

Vue.js本身只是一个JS库,它的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件。Vue.js可以轻松构建SPA(Single Web Application)应用程序,通过指令扩展HTML,通过表达式将数据绑定到HTML,最大程度解放DOM操作。

1.4 安装Vue.js

将Vue.js添加到项目中有4种主要方法:本地独立版本方法、CDN方法、NPM方法以及命令行工具(CLI)方法。

本地独立版本方法

可通过地址“unpkg.com/vue@next”将最…

首先安装一个live server插件,在helllovue.html代码上点击右键出现一个名为Open with Live Server的选项,自动打开浏览器,默认端口号是5500。

图片图片

CDN方法

可通过CDN(Content Delivery Network,内容分发网络)引入最新版本的Vue.js库。

图片

NPM方法

在使用Vue.js构建大型应用时推荐使用NPM安装最新稳定版的Vue.js,因为NPM能很好地和webpack模块打包器配合使用。示例如下:

npm install vue@next

命令行工具(CLI)方法

Vue.js提供一个官方命令行工具(Vue CLI),为单页面应用快速搭建繁杂的脚手架。对于初学者不建议使用NPM和Vue CLI方法安装Vue.js。

具体步骤可以参考下面的链接;

aistudy.baidu.com/okam/pages/…

1.5 第一个Vue.js程序

可通过“code.visualstudio.com”地址下载VSCode,本书使用的安装文件是VSCodeUserSetup-x64-1.52.1.exe(双击即可安装)。

图片图片

const vueApp = Vue.createApp({        //数据        data() {            return {                title"Vue3.0 使用 Vue.createApp() 创建一个应用程序 ",                userInfo: {} //定义用户对象            }        },        //初始化的入口        created: function () {            //调用方法:获取用户信息            this.getUserInfo();        },        //方法        methods: {            //获取用户信息            getUserInfo: function () {                this.userInfo = {                    userId1,                    userName"vivi的博客",                    blogName"您好,欢迎访问 vivi的博客",                    blogUrl"https://blog.csdn.net/vivi"                }            }        }        //使用 mount() 方法,装载应用程序实例的根组件    }).mount('#app'); 

每个Vue.js应用都是通过用createApp函数创建一个新实例开始,具体语法如下:

  const app = Vue.createApp({ /* 选项 */ }) 通过上面那个图片,可以看出这个选项可以定义一个参数,也可以包裹一个data数据,所以选项是选择。

传递给createApp的选项用于配置根组件(渲染的起点)。Vue.js应用创建后,调用mount方法将Vue.js应用挂载到一个DOM元素(HTML元素或CSS选择器)中,例如,如果把一个Vue.js应用挂载到

上,应传递#app。示例代码如下:

  const HelloVueApp = {}//配置根组件

  const vueApp = Vue.createApp(HelloVueApp)//创建Vue实例

  const vm = vueApp.mount('#hello-vue')//将Vue实例挂载到#app

1.6 插值与表达式

Vue的插值表达式“{ { }}”的作用是读取Vue.js中data数据,显示在视图中,数据更新,视图也随之更新。“{ { }}”里只能放表达式(有返回值),不可以放语句,例如,{ { var a = 1 }}与{ { if (ok) { return message } }}都是无效的。

数据绑定最常见的形式就是使用“Mustache(小胡子)”语法(双花括号)的文本插值,它将绑定的数据实时显示出来。例如,{ { counter }},无论何时,绑定的Vue.js实例的counter属性值发生改变,插值处的内容都将更新。

“{ { }}”将数据解释为普通文本,而非HTML代码。当我们需要输出真正的HTML代码时,可使用v-html指令。

假如,Vue.js实例的data为:

data() {            return {                rawHtml'<hr>'            }    }

则“

无法显示HTML元素内容: { { rawHtml }}

”显示的结果是
;而“

可正常显示HTML元素内容:

”显示的结果是一条水平线。

对于所有的数据绑定,Vue.js都提供了完全的JavaScript表达式支持。示例如下:

{ { number + 1 }}

{ { isLogin? 'True' : 'False' }}

{ { message.split('').reverse().join('')}}

❌