ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JavaScript策略模式实战:优雅替代if-else的代码重构指南

2026/9/14 23:08:32 拓冰建站 浏览量
JavaScript策略模式实战:优雅替代if-else的代码重构指南 最近在重构一个老项目里面有一坨if-else每次加需求都要翻半天改完还得提心吊胆。后来趁着迭代我把这块用策略模式重写了一版代码清爽多了同事看了都说好改。今天就围绕Js设计模式里的策略模式把我的实操思路、代码细节和踩过的坑都整理出来给同样被条件分支折磨的同学一个参考。1. 策略模式到底是什么先搞清楚它解决什么问题策略模式说起来并不玄乎它的核心就一句话定义一组算法把它们逐个封装起来并且使它们可以相互替换。这里的“算法”可以理解成一段具体的业务逻辑比如价格计算、表单校验、排序方式、支付渠道选择等等。策略模式让算法的变化不影响到使用算法的客户端听起来挺官方其实用大白话讲就是“把每一套处理逻辑收拾好各自放进独立的格子里需要哪套取哪套”。我在项目里最直观的感受是没有策略模式的时候业务代码里最常见的就是一个巨大的switch或者一连串if-else。比如根据用户类型返回不同折扣代码可能长这样function getDiscount(userType) { if (userType normal) { return 0; } else if (userType vip) { return 0.1; } else if (userType svip) { return 0.2; } else { return 0; } }这种写法在只有两三个分支的时候还能忍一旦分支多起来或者每个分支里不止一个return而是有一大段逻辑函数就会被撑得又长又臭。每次改动都要小心翼翼生怕动到别的分支。而且这种代码几乎没法单独测试可维护性很差。策略模式就是把这段逻辑拆开把每种计算或行为独立成策略在使用时再统一调度。这个模式对应到生活里也特别好理解。比如周末出门如果目的地很近走路去中等距离骑车跨城坐高铁。走路、骑车、坐高铁就是三种策略而“怎么去”这个动作由你根据实际情况选择。选择的前提是不管哪种方式最终都能把你送到目的地只是过程和成本不同。这就是策略模式的核心思想行为的结果一致但具体实现可以千差万别。在JavaScript里因为函数是一等公民策略模式实现起来比其他面向对象语言更轻巧。很多场景下根本不需要建一堆类直接用一个对象或者map把策略存起来就够了。这点后面细说。2. 从需求出发什么场景下你会主动想到用策略模式不是所有地方都适合硬套策略模式它是为解决特定问题而设计的。我在实际开发中通常会从几个明显的信号判断这个需求该不该上策略模式。第一个信号是分支条件很稳定但分支内部的处理逻辑很可能会变化。这里注意区分一下如果分支条件本身经常变比如过两天加一个用户类型、再过两天删一个那核心矛盾其实在于“条件的扩展性”策略模式能帮你减少改动范围但还需要配合配置化的方式才能彻底解决。如果只是每个分支下的逻辑在变那用策略模式就很合适。第二个信号是多个地方在用同一套条件判断。这种情况特别容易遇到比如订单模块里计算价格有一套if-else展示折扣信息又写了一遍if-else后续生成报表又套了一遍。三处代码的逻辑一模一样只是返回的东西不同。这时候把策略抽取出来统一维护能避免改了一处漏了另一处的尴尬。第三个信号是单个函数里代码行数明显失控。我个人的经验值是一个函数如果超过三四十行而且主要逻辑都堆在分支判断里就该考虑拆分。策略模式不一定是唯一解但它往往能帮你理清思路让每个分支的处理逻辑独立成一个短小的函数或类单独看都好理解。还有一类容易被忽略的场景算法有多种实现但它们在运行时才可以确定。比如表格排序用户点了表头可能按时间排、按数值排、按自定义规则排。排序逻辑和列数据本身是解耦的此时用策略模式存一组排序策略点击不同表头执行不同策略代码写起来非常顺手。反过来说有些场景就不太适合。比如只有一两个分支而且分支里的逻辑很简单那直接写if-else更直白。策略模式本质上是引入了一层间接多了一层封装就要多一份理解成本。小函数能解决的问题硬上模式反而显得过度设计。3. JavaScript里实现策略模式的几种写法总有一种适合你讲完了思想重点来看在JavaScript里到底怎么落地。策略模式在传统面向对象的语言里一般长这样定义一个策略接口然后写若干个实现类再写一个上下文类持有当前策略。JavaScript本身没有接口但我们可以用约定俗成的方式模拟而且写法可以非常灵活。3.1 经典类写法适合团队规范要求严格的项目如果你公司的代码规范偏向面向对象或者项目本身是用TypeScript写的那么定义策略类的方式会更清晰。先定义一个策略基类然后各个子类去实现统一的方法。class DiscountStrategy { // 统一入口子类各自实现 calculate(price) { throw new Error(子类必须实现 calculate 方法); } } class NormalDiscount extends DiscountStrategy { calculate(price) { return price; } } class VipDiscount extends DiscountStrategy { calculate(price) { return price * 0.9; } } class SvipDiscount extends DiscountStrategy { calculate(price) { return price * 0.8; } } // 上下文类负责调用策略 class PriceContext { constructor(strategy) { this.strategy strategy; } setStrategy(strategy) { this.strategy strategy; } getPrice(price) { return this.strategy.calculate(price); } } // 使用 const context new PriceContext(new VipDiscount()); console.log(context.getPrice(100)); // 90 context.setStrategy(new SvipDiscount()); console.log(context.getPrice(100)); // 80这种写法的好处是每个策略都符合单一职责原则方法结构一致扩展新策略时不需要改动已有的代码。缺点是类多了以后文件数量也多如果策略本身很简单这种写法会显得有点笨重。3.2 对象字面量写法JavaScript特色实现JavaScript最强大的地方在于对象是动态且开放的可以直接把函数挂到对象属性上。这种写法最推荐在工作用因为它轻量、直观、好维护。const discountStrategies { normal: (price) price, vip: (price) price * 0.9, svip: (price) price * 0.8, }; function getPrice(price, userType) { const strategy discountStrategies[userType]; if (!strategy) { throw new Error(未支持的用户类型: ${userType}); } return strategy(price); }是不是一下子清爽很多。用对象字面量把策略名称和实现映射起来需要的时候直接按key取。这个写法完美利用了JavaScript对象的特性不需要模拟接口也不需要建类。如果对key的拼写不放心还可以用Map好处是key不限于字符串而且遍历顺序有保证性能上也优于普通对象。const discountStrategies new Map([ [normal, (price) price], [vip, (price) price * 0.9], [svip, (price) price * 0.8], ]); function getPrice(price, userType) { const strategy discountStrategies.get(userType); if (!strategy) { throw new Error(未支持的用户类型: ${userType}); } return strategy(price); }这两种写法放到项目里都够用。我倾向于用Map因为如果以后策略key变成枚举对象或者数字标识Map照样能处理普通对象就要多绕一层转换。3.3 高阶函数写法把策略当参数传来传去如果策略只在很局部的地方用到而且不需要长期保存直接用高阶函数传策略也可以。比如一个排序函数接收一个比较函数作为参数。function sortData(data, compareFn) { return data.slice().sort(compareFn); } // 按时间排序 sortData(list, (a, b) a.timestamp - b.timestamp); // 按价格排序 sortData(list, (a, b) a.price - b.price);这种写法在数组方法、回调函数、事件处理里其实非常常见只是很多人没有意识到这就是策略模式的思想。JavaScript里函数可以作为参数传来传去这种“把行为传进去”的能力是策略模式的天然基石。4. 在真实项目中用策略模式表单校验和价格计算两个完整案例光说理论有点虚我把两个自己实际写过的场景完整地拆解一下从需求到实现到最终效果一条龙讲清楚。4.1 案例一多表单校验逻辑解耦表单校验给我的印象最深因为几乎每个后台管理系统都会有。需求往往是这样的用户名必填且不能超过20个字符手机号要符合规则邮箱要符合格式。没有策略模式时大家通常写一个巨大的校验函数function validateForm(form) { if (!form.username) { return { valid: false, msg: 用户名不能为空 }; } if (form.username.length 20) { return { valid: false, msg: 用户名不能超过20个字符 }; } if (!form.mobile) { return { valid: false, msg: 手机号不能为空 }; } if (!/^1[3-9]\d{9}$/.test(form.mobile)) { return { valid: false, msg: 手机号格式不正确 }; } if (!form.email) { return { valid: false, msg: 邮箱不能为空 }; } if (!/^[^\s][^\s]\.[^\s]$/.test(form.email)) { return { valid: false, msg: 邮箱格式不正确 }; } return { valid: true }; }问题很明显每加一个字段就往这个函数里追加一段“if开头return”的代码函数越来越臃肿。更痛苦的是不同的表单会用到不同的校验组合比如登录表单只需要用户名和密码注册表单却要全部字段。直接复用这个函数就很别扭。用策略模式重构之后我把每种校验规则封装成独立的策略函数再用一个调度器根据配置执行。// 1. 定义校验策略集合 const validators { required: (value) (value ? : 该字段不能为空), maxLength: (value, max) (value value.length max ? : 不能超过${max}个字符), mobile: (value) (/^1[3-9]\d{9}$/.test(value) ? : 手机号格式不正确), email: (value) (/^[^\s][^\s]\.[^\s]$/.test(value) ? : 邮箱格式不正确), }; // 2. 定义字段校验规则配置 const rules { username: [ { strategy: required }, { strategy: maxLength, params: [20] }, ], mobile: [ { strategy: required }, { strategy: mobile }, ], email: [ { strategy: email }, ], }; // 3. 执行校验的通用函数 function validate(values) { const errors {}; Object.keys(rules).forEach((field) { const fieldRules rules[field]; for (const item of fieldRules) { const validator validators[item.strategy]; const errorMsg validator(values[field], ...(item.params || [])); if (errorMsg) { errors[field] errorMsg; break; } } }); return Object.keys(errors).length 0 ? null : errors; } // 4. 使用 const result validate({ username: , mobile: 123, email: abc });这样重构之后新增一个校验规则只需要往validators里加一个函数新增一个字段只需要往rules里加配置validate本身基本不用动了。我之前项目里有七八个表单纯复用这套逻辑每个表单只需要定义自己的rules就行代码量少了一大截。沿用这个思路表单校验还能做得更复杂比如字段联动当某个字段等于特定值时另一个字段必填这类逻辑一样可以拆成策略只是规则配置里要多传一个上下文对象进去。4.2 案例二多种计费规则的自由切换计费系统也是策略模式的高频使用场景。之前做过一个项目不同渠道的订单计费规则不一样当时是这么处理的。需求背景商品本身有一个基础价但不同的活动或者不同的客户等级会使用不同的计费方式。有的是直接打折有的是满减有的是阶梯价。而且同一个用户可能同时参与多个活动计算顺序不同结果也不同。我先定义一批计费策略// 计价策略集合 const priceStrategies { // 原价 original: (basePrice) basePrice, // 折扣价discount为0.1表示打9折 discount: (basePrice, discount) basePrice * (1 - discount), // 满减full表示满多少minus表示减多少 fullReduction: (basePrice, full, minus) (basePrice full ? basePrice - minus : basePrice), // 阶梯价thresholds是数组[{ min, max, price }] tiered: (basePrice, thresholds) { for (const item of thresholds) { if (basePrice item.min (item.max null || basePrice item.max)) { return item.price; } } return basePrice; }, };然后封装一个计费上下文function createPriceCalculator(strategies) { const activeStrategies strategies.filter((s) priceStrategies[s.type]); return function calcPrice(basePrice) { return activeStrategies.reduce((price, s) { const strategyFn priceStrategies[s.type]; const params s.params || []; return strategyFn(price, ...params); }, basePrice); }; } // 使用示例 // 普通用户原价 const normalCalc createPriceCalculator([{ type: original }]); // VIP用户打9折后满100减20 const vipCalc createPriceCalculator([ { type: discount, params: [0.1] }, { type: fullReduction, params: [100, 20] }, ]); // 活动价走阶梯价 const eventCalc createPriceCalculator([ { type: tiered, params: [[{ min: 0, max: 100, price: 80 }, { min: 100, max: null, price: 60 }]] }, ]); console.log(normalCalc(150)); // 150 console.log(vipCalc(150)); // 150*0.9 135满100减20 115 console.log(eventCalc(150)); // 60这种组合式的调用方式一下子把计费逻辑从业务代码里面抽离干净了。后续如果需要新增一种计价方式比如“每满100减10”只需要往priceStrategies里加一个函数然后配置上就能用。而且因为是纯函数计费规则可以很方便地做单元测试。5. 策略模式的进阶用法策略组合与动态配置化上面两个案例其实已经触及到一个进阶点策略不只是单独使用还可以组合使用。在实际系统中业务往往不会那么单纯一个规则搞不定所有事情。这时候策略模式可以跟责任链模式、模板方法模式混搭着用但核心还是“策略彼此独立、可以自由组装”。我在一个订单模块里做过更灵活的处理把策略配置直接放到接口返回的数据里后端下发配置前端动态执行。这样改计费规则或者活动规则不用发版刷新一下就行。具体做法是把策略配置改成数组下发给前端// 假设后端返回这样一个配置 const strategyConfig { userType: vip, promoteType: fullReduction, promoteParams: [200, 50], }; // 前端根据配置动态组装策略 function buildStrategies(config) { const strategies []; if (config.userType vip) { strategies.push({ type: discount, params: [0.1] }); } if (config.promoteType fullReduction) { strategies.push({ type: fullReduction, params: config.promoteParams }); } return strategies; }这样做的好处是业务上新增一种活动形式时前端只需要保证priceStrategies里有对应的策略函数剩下的配置都由后端下发完全不用动前端代码。类似的思路也可以用在前端权限控制上。根据用户角色展示不同的操作按钮这本质上也是策略选择。角色和可执行操作之间可以有一张策略映射表加角色加权限都只是加表项。再往深走一步策略模式还经常跟工厂模式搭着用。工厂负责根据条件创建出对应的策略对象上下文只负责执行。举个例子一个支付模块根据用户的支付方式生成不同的支付策略支付策略内部可能还包含不同的下单逻辑。工厂模式帮你决定选哪个策略策略模式负责统一执行入口。6. 策略模式的实际威力代码量下降之后可维护性上升了多少我重构过的那块代码核心改动就是用一个价格策略集合替换掉原来的大switch。原来的代码大概是这样的感觉switch (order.channel) { case app: price handleAppChannel(order); break; case h5: price handleH5Channel(order); break; case miniProgram: price handleMiniProgramChannel(order); break; default: price handleDefault(order); }看switch本身倒还好问题是handleAppChannel等函数里面也有自己的分支而且调用方散落在多个模块里。一旦渠道数量变多整个模块就变成了一锅粥。重构之后结构大致变成三块策略定义集中在一个文件里、调用方从统一入口获取策略、业务参数通过配置传入。改完之后第一感受加新渠道的时候再也不用全局搜索channel字段了。只需要回到策略文件里加一个新策略然后确认调用方的配置里面已经覆盖了渠道和策略的映射。这个改动范围控制得非常小天然避免了改一处坏三处的问题。第二感受单测变得好写了。之前想测某种渠道的价格计算逻辑要先构造一整套order对象再构造渠道环境。现在直接调策略函数传入基本参数就能断言结果。测试用例的可读性和维护成本都下降了。第三感受代码审查的体验变好了。之前review那种大分支代码要一个分支一个分支看下来效率很低。现在策略独立成文件review的时候只需要看新增策略是否满足约定、参数是否符合预期、有没有被注册进映射表几分钟就能搞定。从数字上看重构前的计费模块大概两百多行大部分是嵌套分支和参数处理。重构后策略文件本身一百行左右调度文件三四十行每个策略函数保持在一屏以内。总行数可能没有显著减少但信息密度完全不同改动时可以一眼定位到目标位置。7. 注意这些坑策略模式的误用和隐藏问题策略模式虽然好用但如果不注意方式方法也会带来新问题。我把自己踩过、或者看别人踩过的坑整理成几条供大家参考。7.1 别为了用模式而用模式看到代码里有几个if-else就想着上策略模式往往会把简单问题复杂化。我见过一个项目总共只有三种商品类型每种类型的差异只有价格系数不同结果有人硬生生写了三个策略类又写了一个上下文类启动时还要注册。调试的时候绕来绕去非常麻烦。这里一定要强调策略模式的收益在于应对变化和组合如果变化频率低、逻辑也简单直接if-else更合适。可以把策略模式理解成一种投资当业务复杂度超过某个临界点时它才会持续产生收益白白提前投入只会增加维护成本。7.2 策略粒度要合适拆太细反而难维护有一种常见误区是把每一种微小的动作都拆成一个策略导致策略类满天飞。比如“给字符串去空格”“判断字符串是否为空”这种通用能力也包一层策略。这样的结果是策略集合变得非常琐碎不知道从哪找起。策略的粒度应该跟业务概念对齐一个策略对应一种完整的、业务上有意义的处理方式。还是拿计费举例“打9折”是一个策略“满100减20”是一个策略而不是把“判断金额大于100”和“执行减20”分开成两个策略。7.3 注意策略对象的生命周期如果在策略对象里面保存了状态要格外小心。比如某个策略实现里缓存了一个值这个值在多处调用之间可能会被污染。策略模式的设计初衷是策略之间互不影响如果策略内部有可变状态就破坏了这种独立性。我在一个项目中遇到过一次奇怪的问题两个不同请求调用了同一个策略实例第二个请求莫名其妙拿到了第一个请求的残留数据。排查了半天才意识到策略实例是单例的而里面的缓存数组没有清空。解决方式也很简单策略方法改成纯函数或者每次使用前重置状态。7.4 策略集合越来越大时要有组织意识当策略数量多到一定程度比如超过二三十个管理策略本身也会变成一个问题。我在实际中会做几件事策略文件按业务域拆分比如priceStrategies、validateStrategies、exportStrategies各放一个目录策略的命名遵循统一规范方便搜索策略注册表集中维护避免散落在各个地方。有些项目会引入配置驱动的策略注册机制用装饰器或者注解的方式自动注册策略。这种方式在TypeScript项目里用起来很舒服但JavaScript原生环境需要自己实现一个简单的注册表本质还是把策略函数登记到一个全局Map里面。7.5 别忽略无策略时的兜底逻辑在使用策略模式时很容易把所有注意力放在“命中策略后的行为”上忽略了“没有命中策略该怎么办”。比如上述discountStrategies[userType]取不到值时如果没有处理函数会直接抛错或者返回undefined这在线上可能会引发连锁问题。我的习惯是策略查找不到时提供一个默认策略或者抛出带有明确信息的异常。默认策略在很多业务中就等于“原价处理”“不做处理”这种兜底逻辑能有效避免线上空指针问题。8. 和其它设计模式搭配策略模式的最佳拍档回到开篇提到的那堆热词简单工厂模式、组合模式、模板方法模式都跟策略模式有关系。搞清楚它们之间的边界能帮你在实际编码中做出更合理的选择。简单工厂模式的定位是“创建对象”它根据传入的参数决定创建哪一种对象重点在“创建”。策略模式的定位是“行为选择”它决定使用哪一种算法或行为重点在“执行”。两者经常配合使用工厂负责根据条件创建出策略对象策略对象负责具体执行。用工厂来解耦“创建”用策略模式来解耦“使用”。模板方法模式关注的是“算法骨架固定某些步骤延迟到子类实现”更强调流程的固定性。策略模式则完全不同它没有固定流程多个策略是并列的随意替换。如果你发现多个策略之间执行顺序相同只是其中某一步不同这时候更适合用模板方法模式。如果每个策略是完全独立的一套流程就用策略模式。组合模式解决的是“部分与整体”的关系比如树状结构的数据。策略模式解决的是“条件与行为”的映射关系两者通常不会直接冲突但在复杂业务中可能同时出现比如一棵组合树上的每个节点各自拥有不同的计算策略。状态模式和策略模式也容易混淆。状态模式的侧重点是“一个对象在不同的状态下表现出不同的行为”而且状态之间是可以转移的。策略模式则是客户端主动选择某一种策略策略之间没有联系。简单说策略是选出来的状态是走出来的。就我自己的使用频率来看前端最值得熟练掌握的几个模式就是策略模式、观察者模式和模块模式。策略模式用来处理业务分支观察者模式用来处理事件联动模块模式用来组织代码结构。把这三板斧用好大部分业务需求都能写得清楚明白。9. 最后的实操建议从今天开始怎么落地如果你刚接触策略模式我的建议是别急着在核心业务里重构。先找一个边界清晰的小模块练手比如表单校验或者状态映射把策略模式的结构跑通感觉一下“行为可替换”带来的变化。跑通之后再去审视现有的代码找出那些明显膨胀的条件分支评估一下它们的变化频率和组合需求再决定是不是值得重构。在落地过程中几个比较实用的习惯可以培养起来策略集合统一放在独立的文件或目录里不要散落在页面组件中。策略函数的签名尽量保持一致参数用统一的对象结构这样调用方代码可以非常稳定。策略查找不到时的兜底逻辑在一开始就要设计好别等到线上报错了才补。给策略函数写单元测试保证每个策略独立可用这样组合起来使用的时候才有底气。如果项目里有TypeScript为策略函数定义清晰的类型签名可以让可维护性再上一个台阶。我个人在实际开发中感受最深的一点是策略模式改变了看待代码的方式。以前看到一个复杂函数第一反应是“这逻辑怎么这么多”现在会更自然地想“这里面到底包含了几种可以被独立替换的行为”。思维方式一旦转换很多原本觉得难维护的代码都能找到比自己预期更优雅的重构路径。最后再分享一个实用的小技巧在使用对象字面量或Map管理策略的时候可以顺便给策略集合加一个“插拔”能力也就是注册新策略和删除旧策略的方法。这样做的好处是项目里的其他模块可以在运行时动态调整方案不用改动策略集合本身的代码。很多插件化架构的底层就是靠这种思路实现的从小处用起以后做大架构时自然就熟练了。