深入对比JavaScript中策略模式、工厂模式与状态模式的核心区别,从设计意图、控制权归属、执行机制、代码重构实战到组合使用方式,帮助开发者准确选择最合适的设计模式。
JavaScript策略模式与工厂模式、状态模式的区别
时间:2026-11-02
浏览:666
文章目录
在日常 JavaScript 开发中,当代码中出现大量臃肿的 if-else 或 switch-case 时,开发者通常会考虑引入设计模式来重构。策略模式、工厂模式和状态模式常常同时被提及,因为它们在表面形态上都利用多态、委托或键值对查表来消除条件判断分支。然而,这三者的设计初衷、控制权归属以及适用的生命周期阶段截然不同:策略模式解决的是“怎么做(算法可平级替换)”,工厂模式解决的是“造什么(对象的解耦实例化)”,而状态模式解决的是“随态而变(行为与内部状态紧密绑定并自驱流转)”。本文将通过具体的场景、可执行的 JavaScript 代码与图解,系统梳理这三种模式的本质差异与协同方案。
一、三者核心区别总览
为了在面临业务设计时快速做出判断,我们首先从设计意图、控制权来源以及内部机制三个核心维度,对比这三种模式的本质差异:
| 对比维度 | 策略模式 (Strategy) | 工厂模式 (Factory) | 状态模式 (State) |
|---|---|---|---|
| 核心意图 | 封装可相互替换的算法族,使算法独立于调用者变化 | 封装对象的创建过程,将实例化与具体业务使用解耦 | 封装对象的内部状态变化,使对象在不同状态下呈现不同行为 |
| 控制权归属 | 调用端(外部主动决策):调用者根据诉求选定策略并注入 | 工厂内部(按参生成):调用者仅传达需求类型,工厂返回产品 | 状态对象自身(内部自驱流转):外部仅触发动作,状态自主跃迁 |
| 解决的核心痛点 | 同一种任务下,多种并列算法导致的硬编码分支 | 创建复杂对象时,构造细节散落在业务逻辑各处 | 复杂生命周期下,相同方法根据内部变量产生大量条件判断 |
| 状态时序关系 | 无关联:各策略平行独立,不存在前后推导关系 | 一次性:生产完成后立即移交控制权,工厂不介入运行 | 强时序:状态之间存在明确的前驱后继关系与转换规则 |
| JavaScript 惯用形态 | 策略字典映射对象、高阶函数 | 工厂函数、动态模块映射 | 类委托体系、有限状态机(FSM)配置 |
二、策略模式(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
三、工厂模式(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 的复杂分支判断。
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(),虽然使用了策略模式,但调用方依然与具体的策略类产生了紧耦合。此时引入工厂模式,便能构建出职责清晰的流水线:
// 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
七、选型决策指南
当你面对复杂的业务逻辑分支时,可以通过以下决策路径进行技术选型:
- 问题是“根据条件创建不同的对象实例”吗?
如果是,并且对象的实例化过程多变、依赖动态配置或平台环境,毫不犹豫选择工厂模式。 - 问题是“对象在生命周期中有一系列状态,状态变了之后相同方法的响应完全不同”吗?
例如审核流程(草稿 -> 待审 -> 驳回 -> 通过)、游戏角色状态(正常 -> 中毒 -> 阵亡)。如果是,毫不犹豫选择状态模式或现代有限状态机(如 XState)。 - 问题是“针对同一件事情,有多种不同的算法公式、规则校验或处理方式”吗?
例如运费计算、压缩解压、表单验证器。调用端明确知道何时用哪种规则,且各个算法之间无时序流转关联。此时选择策略模式。 - 如果分支只有 2-3 个且逻辑非常简单稳定呢?
直接使用普通的if-else或简单的三元表达式即可,严禁为了“模式”而增加多余的对象层,避免过度设计。
- 不要在 JavaScript 中过度类化(Class-itis):传统 Java/C++ 中因为缺乏一等公民函数和对象字面量,必须为策略和工厂建一堆 Class 和 Interface。在 JavaScript 中,能够用对象映射
const strategies = { a: fnA, b: fnB }解决的策略模式,不需要写一整套StrategyContext类。 - 状态模式防死循环与非法跃迁:状态模式最容易出现的问题是状态之间循环跃迁或跃迁到非法节点。在构建复杂业务流时,推荐在 Context 中加入状态流转白名单校验(Transition Guard),确保从状态 A 只能跃迁到合法的下一个状态集合中。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。