JavaScript策略模式与工厂模式、状态模式的区别

作者:小括号

时间:2026-11-02

浏览:666

深入对比JavaScript中策略模式、工厂模式与状态模式的核心区别,从设计意图、控制权归属、执行机制、代码重构实战到组合使用方式,帮助开发者准确选择最合适的设计模式。

文章目录
主题:软件架构与设计模式 适用环境:Node.js / 现代前端框架 难度:进阶实用
JavaScript策略模式与工厂模式、状态模式的区别
策略模式、工厂模式与状态模式核心意图概览

在日常 JavaScript 开发中,当代码中出现大量臃肿的 if-else 或 switch-case 时,开发者通常会考虑引入设计模式来重构。策略模式、工厂模式和状态模式常常同时被提及,因为它们在表面形态上都利用多态、委托或键值对查表来消除条件判断分支。然而,这三者的设计初衷、控制权归属以及适用的生命周期阶段截然不同:策略模式解决的是“怎么做(算法可平级替换)”,工厂模式解决的是“造什么(对象的解耦实例化)”,而状态模式解决的是“随态而变(行为与内部状态紧密绑定并自驱流转)”。本文将通过具体的场景、可执行的 JavaScript 代码与图解,系统梳理这三种模式的本质差异与协同方案。

一、三者核心区别总览

为了在面临业务设计时快速做出判断,我们首先从设计意图、控制权来源以及内部机制三个核心维度,对比这三种模式的本质差异:

对比维度 策略模式 (Strategy) 工厂模式 (Factory) 状态模式 (State)
核心意图 封装可相互替换的算法族,使算法独立于调用者变化 封装对象的创建过程,将实例化与具体业务使用解耦 封装对象的内部状态变化,使对象在不同状态下呈现不同行为
控制权归属 调用端(外部主动决策):调用者根据诉求选定策略并注入 工厂内部(按参生成):调用者仅传达需求类型,工厂返回产品 状态对象自身(内部自驱流转):外部仅触发动作,状态自主跃迁
解决的核心痛点 同一种任务下,多种并列算法导致的硬编码分支 创建复杂对象时,构造细节散落在业务逻辑各处 复杂生命周期下,相同方法根据内部变量产生大量条件判断
状态时序关系 无关联:各策略平行独立,不存在前后推导关系 一次性:生产完成后立即移交控制权,工厂不介入运行 强时序:状态之间存在明确的前驱后继关系与转换规则
JavaScript 惯用形态 策略字典映射对象、高阶函数 工厂函数、动态模块映射 类委托体系、有限状态机(FSM)配置
JavaScript策略模式与工厂模式、状态模式的区别
图 1:策略模式、工厂模式与状态模式的架构组织与控制流对比

二、策略模式(Strategy Pattern):可替换的算法簇

策略模式的核心是定义一系列算法,把它们各自封装起来,并且使它们可以相互替换。在策略模式中,执行具体计算的逻辑与使用该计算的环境(Context)是彻底解耦的。

1. 典型应用场景

电商购物结算时的优惠计算是策略模式最典型的场景。同一个结算动作,针对不同会员类型或促销活动有不同计费方式:如普通用户无折扣、金牌会员打 8 折、满减活动直接减 20 元。这些折扣算法是平级且互相独立的。

2. JavaScript 实现与重构

在传统的面向对象语言中,策略模式需要声明策略接口并为每个算法创建单独的 Class。但在 JavaScript 中,由于函数是一等公民,使用对象字面量(Object Map)映射高阶函数是最轻量、最具表现力的方式:

// 1. 定义算法策略字典
const priceStrategies = {
  regular: (price) => price,
  silver: (price) => price * 0.9,
  gold: (price) => price * 0.8,
  promotion: (price) => Math.max(0, price - 20)
};

// 2. 环境类/结算函数(Context)
function calculateFinalPrice(strategyType, originalPrice) {
  const strategy = priceStrategies[strategyType];
  if (!strategy) {
    throw new Error(`未找到对应的计价策略: ${strategyType}`);
  }
  return strategy(originalPrice);
}

// 3. 调用端按需选定策略
console.log(calculateFinalPrice('gold', 200));       // 输出 160
console.log(calculateFinalPrice('promotion', 50));  // 输出 30
策略模式的核心特征:调用者清楚地知道当前场景该用什么策略,主动做出选择并传入。策略对象各自独立封闭,新增一种策略只需向字典添加新 key,满足开闭原则。

三、工厂模式(Factory Pattern):解耦对象的生产

工厂模式属于创建型模式。其核心意图并非管理“代码如何执行算法”,而是封装对象的创建逻辑。当一个对象的实例化过程包含多步装配、参数校验、环境嗅探或需要根据条件决定实例化哪一个子类时,工厂模式将这些繁琐细节收拢在单一位置。

1. 典型应用场景

网络请求客户端的跨平台适配:在 Node.js 环境下需要使用 http/https 模块包装请求客户端,在浏览器端使用 Fetch,在旧版系统使用 XMLHttpRequest。调用方只希望拿到一个具备 request(url, options) 统一接口的客户端对象,而不关心底层到底实例化的哪一个类。

2. JavaScript 实现示例

// 1. 具体产品类
class FetchClient {
  async request(url) {
    const res = await fetch(url);
    return res.json();
  }
}

class MockClient {
  async request(url) {
    return { status: 'ok', data: `Mock数据针对 ${url}` };
  }
}

// 2. 客户端工厂(Factory)
class HttpClientFactory {
  static createClient(type = 'fetch') {
    switch (type) {
      case 'mock':
        return new MockClient();
      case 'fetch':
      default:
        if (typeof fetch === 'undefined') {
          throw new Error('当前运行环境不支持全局 Fetch 接口');
        }
        return new FetchClient();
    }
  }
}

// 3. 业务调用端
const client = HttpClientFactory.createClient(process.env.USE_MOCK ? 'mock' : 'fetch');
client.request('/api/user/info');

与策略模式的关键分水岭:工厂模式关注的是对象生命周期的起点,其返回的是一个崭新的、配置好的对象实例;而策略模式关注的是业务算法的替换,策略通常以无状态函数或轻量工具类形式存在,重点在于调用算法而不是持有对象。

四、状态模式(State Pattern):行为与内部状态自驱

状态模式的核心是允许一个对象在其内部状态改变时改变它的行为。从外部看,该对象似乎修改了它的类。状态模式解决的是随着生命周期演进,对象内部状态会发生不可逆或受约束的跃迁,且相同的输入在不同状态下对应截然不同的响应动作。

1. 典型应用场景

媒体播放器、工作流工单、电商订单生命周期。以播放器为例,核心操作有 play()、pause()、stop():

  • 在 Stopped(停止) 状态下点击 play():开始从头播放音频,并将状态置为 Playing;
  • 在 Playing(播放中) 状态下点击 play():属于重复动作,忽略或提示;
  • 在 Paused(暂停) 状态下点击 play():从上次断点处恢复播放,并将状态置为 Playing。
如果不使用状态模式,play() 方法内部就会充斥着基于 this.currentState 的复杂分支判断。
JavaScript策略模式与工厂模式、状态模式的区别
图 2:状态模式在音频播放器中的状态自流转与行为委托模型

2. JavaScript 面向对象委托实现

// 抽象基类 / 协议约定
class PlayerState {
  constructor(player) {
    this.player = player;
  }
  play() { console.log('当前状态下无法执行播放'); }
  pause() { console.log('当前状态下无法执行暂停'); }
  stop() { console.log('当前状态下无法执行停止'); }
}

// 具体状态 A:停止状态
class StoppedState extends PlayerState {
  play() {
    console.log('开始从头播放音频...');
    this.player.setState(new PlayingState(this.player));
  }
}

// 具体状态 B:播放中状态
class PlayingState extends PlayerState {
  pause() {
    console.log('暂停当前播放,保留当前时间戳。');
    this.player.setState(new PausedState(this.player));
  }
  stop() {
    console.log('停止播放,进度归零。');
    this.player.setState(new StoppedState(this.player));
  }
}

// 具体状态 C:暂停中状态
class PausedState extends PlayerState {
  play() {
    console.log('从断点处继续恢复播放...');
    this.player.setState(new PlayingState(this.player));
  }
  stop() {
    console.log('从暂停状态直接停止,进度归零。');
    this.player.setState(new StoppedState(this.player));
  }
}

// 上下文环境类 (Context)
class AudioPlayer {
  constructor() {
    this.state = new StoppedState(this); // 初始状态为停止
  }
  setState(nextState) {
    this.state = nextState;
  }
  play() { this.state.play(); }
  pause() { this.state.pause(); }
  stop() { this.state.stop(); }
}

// 客户端使用:外部无须传入任何状态分支,直接触发行为
const player = new AudioPlayer();
player.play();  // 输出:开始从头播放音频... (状态自动变为 PlayingState)
player.pause(); // 输出:暂停当前播放,保留当前时间戳。 (状态自动变为 PausedState)
player.play();  // 输出:从断点处继续恢复播放... (状态自动变为 PlayingState)
player.stop();  // 输出:停止播放,进度归零。 (状态自动变为 StoppedState)
状态模式的核心特征:状态切换是由状态对象在内部行为执行完毕后主动驱动(this.player.setState(...))完成的。调用者只管调用同一个业务方法,外部完全不需要理会当前处于何种状态,也不需要手动判断下一个状态该切去哪里。

五、深入辨析:为什么三者容易混淆?

很多开发者在阅读设计模式教程时,容易将策略模式、工厂模式和状态模式混淆,原因在于它们在代码结构上存在某种“镜像相似度”:

  • 表面相似点 1:都消除了条件语句。策略模式消除了根据业务类型计算的大型 switch;工厂模式消除了根据类型 new 实例的 switch;状态模式消除了判断内部阶段的 switch。
  • 表面相似点 2:结构上都使用了 Context 委托。策略模式和状态模式在 UML 类图上几乎一模一样,都是一个 Context 持有一个接口引用,并在运行时调用对应接口的方法。

但透过代码表象,三者的底层逻辑存在三道本质鸿沟:

1. 控制权流动方向不同

在策略模式中,控制权牢牢掌握在外部客户端手中。客户端主动决定:“这次计算,请使用策略 B”。Context 是被动接收策略的容器。
而在状态模式中,控制权隐藏在内部状态机之中。外部客户端不能随意强行指定“直接跳到状态 C”,而是只能触发 play() 或 submit() 这类意图事件,由当前状态根据规则合法跳转。
在工厂模式中,控制权归属于工厂逻辑。外部提出抽象需求,工厂承担全套生产责任并交付成品。

2. 状态连续性与独立性不同

策略模式中的各个策略是完全平级且互不感知的。折扣策略 A 根本不知道也不关心满减策略 B 的存在,它们之间不存在“前置依赖”或“时序切换”。
而状态模式中的各个状态是互相关联、彼此流转的。StoppedState 明确知道自己在 play() 触发后应该将宿主切换到 PlayingState。状态之间构成了一张有向图(Directed Graph)。

六、工程实战:策略模式与工厂模式的组合搭配

在实际企业级开发中,单一模式往往不是孤立使用的。一个非常常见的经典架构方案是:使用“工厂模式”来负责实例化并返回对应的“策略模式对象”。

例如在复杂表单校验或多渠道支付场景中,如果调用方直接手写 new WechatPayStrategy() 或 new AliPayStrategy(),虽然使用了策略模式,但调用方依然与具体的策略类产生了紧耦合。此时引入工厂模式,便能构建出职责清晰的流水线:

JavaScript策略模式与工厂模式、状态模式的区别
图 3:工厂模式生产策略实例并交由 Context 执行的协同工作推导步骤
// 1. 各支付策略实现
class AliPayStrategy {
  pay(amount) {
    console.log(`正在通过支付宝扣款:¥${amount}`);
    return { success: true, channel: 'alipay' };
  }
}

class WechatPayStrategy {
  pay(amount) {
    console.log(`正在通过微信支付扣款:¥${amount}`);
    return { success: true, channel: 'wechat' };
  }
}

// 2. 策略工厂(负责生产策略单例或实例,消除调用方对具体类的依赖)
class PaymentStrategyFactory {
  static #strategies = new Map();

  static getStrategy(channelType) {
    if (!this.#strategies.has(channelType)) {
      switch (channelType) {
        case 'alipay':
          this.#strategies.set(channelType, new AliPayStrategy());
          break;
        case 'wechat':
          this.#strategies.set(channelType, new WechatPayStrategy());
          break;
        default:
          throw new Error(`不支持的支付通道: ${channelType}`);
      }
    }
    return this.#strategies.get(channelType);
  }
}

// 3. 结算上下文 (Context)
class CheckoutService {
  #strategy;
  
  setPaymentChannel(channelType) {
    // 关键组合点:上下文通过工厂获取策略,自身完全不耦合任何具体类
    this.#strategy = PaymentStrategyFactory.getStrategy(channelType);
  }

  executePayment(amount) {
    if (!this.#strategy) {
      throw new Error('尚未设置支付策略,无法扣款');
    }
    return this.#strategy.pay(amount);
  }
}

// 4. 调用端干净利落的调用
const checkout = new CheckoutService();
checkout.setPaymentChannel('alipay');
checkout.executePayment(500); // 正在通过支付宝扣款:¥500

七、选型决策指南

当你面对复杂的业务逻辑分支时,可以通过以下决策路径进行技术选型:

JavaScript策略模式与工厂模式、状态模式的区别
图 4:设计模式选型决策路径与综合维度矩阵
  1. 问题是“根据条件创建不同的对象实例”吗?
    如果是,并且对象的实例化过程多变、依赖动态配置或平台环境,毫不犹豫选择工厂模式。
  2. 问题是“对象在生命周期中有一系列状态,状态变了之后相同方法的响应完全不同”吗?
    例如审核流程(草稿 -> 待审 -> 驳回 -> 通过)、游戏角色状态(正常 -> 中毒 -> 阵亡)。如果是,毫不犹豫选择状态模式或现代有限状态机(如 XState)。
  3. 问题是“针对同一件事情,有多种不同的算法公式、规则校验或处理方式”吗?
    例如运费计算、压缩解压、表单验证器。调用端明确知道何时用哪种规则,且各个算法之间无时序流转关联。此时选择策略模式。
  4. 如果分支只有 2-3 个且逻辑非常简单稳定呢?
    直接使用普通的 if-else 或简单的三元表达式即可,严禁为了“模式”而增加多余的对象层,避免过度设计。
注意事项与避坑指南:
  • 不要在 JavaScript 中过度类化(Class-itis):传统 Java/C++ 中因为缺乏一等公民函数和对象字面量,必须为策略和工厂建一堆 Class 和 Interface。在 JavaScript 中,能够用对象映射 const strategies = { a: fnA, b: fnB } 解决的策略模式,不需要写一整套 StrategyContext 类。
  • 状态模式防死循环与非法跃迁:状态模式最容易出现的问题是状态之间循环跃迁或跃迁到非法节点。在构建复杂业务流时,推荐在 Context 中加入状态流转白名单校验(Transition Guard),确保从状态 A 只能跃迁到合法的下一个状态集合中。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章