尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

JavaScript策略模式实战:告别if-else地狱,轻松重构复杂业务逻辑

发布时间:2026/9/24 18:37:31

资讯中心
01
ARTICLE

JavaScript策略模式实战:告别if-else地狱,轻松重构复杂业务逻辑

JavaScript策略模式实战:告别if-else地狱,轻松重构复杂业务逻辑
不知道你有没有过这样的经历一个算价格或者做校验的函数里面写了七八层if-else每次产品经理说“再增加一种优惠类型”或者“再加一个校验规则”的时候你就得小心翼翼地在最后一个else if后面补一段逻辑生怕漏掉某个分支条件。写的时候感觉还挺有成就感过两周回来看自己都想骂人——这段代码逻辑到底是什么鬼这就是典型的if-else地狱。我在前公司维护过一个营销中台的计费模块促销类型从最初的3种膨胀到20多种主流程里的if-else嵌套了四层每次改动都要人肉跑一遍所有分支生怕改漏一个。后来我用JavaScript策略模式做了两次大规模重构才彻底把这块代码从“屎山”里捞出来。这篇文章就从这两个案例入手讲讲策略模式到底怎么解决if-else地狱哪些场景适合用、哪些场景不要硬套以及我在真实项目里踩过的坑。1. if-else地狱的真实面貌先从一段促销代码聊起1.1 一段让所有人都不愿意碰的计价逻辑先看一段我在项目里见过的代码它长这样function calcPrice(order) { if (order.vip) { // 会员固定打95折 return order.amount * 0.95; } else if (order.fullReduction) { // 满减活动满500减120满300减60满200减30 if (order.amount 500) { return order.amount - 120; } else if (order.amount 300) { return order.amount - 60; } else if (order.amount 200) { return order.amount - 30; } else { return order.amount; } } else if (order.coupon) { // 优惠券又分为立减券和折扣券 if (order.coupon.type cash) { return order.amount - order.coupon.value; } else if (order.coupon.type discount) { return order.amount * order.coupon.discount; } else { return order.amount; } } else if (order.seckill) { // 秒杀价直接使用活动价 return order.seckill.price; } else { return order.amount; } }这段代码看起来还能跑但内部已经出现了二级嵌套的if-else。更麻烦的是真实业务里还会有“满减和优惠券同时存在”的叠加场景有“不同城市运费不同”的地区策略有“黄金会员和白金会员折扣不同”的会员分级。所有这些逻辑堆在一起函数会膨胀到一二百行。1.2 这个函数到底犯了什么错用一个函数把各种促销规则塞在一起表面上看似省事实际埋下了几个雷第一违反开闭原则。每新增一种促销类型你都必须打开这个函数在末尾或中间插入一个新的else if。改过的函数总会引入新的风险哪怕只是加一行代码也要把整个流程重新测试一遍。第二条件判断和业务算法耦合在一起。代码里“选择哪种规则”和“规则具体怎么算”混在一起读代码的人要先理清分支关系才能看到每个分支里的具体逻辑。判断一多脑子就转不过来。第三难以复用和测试。满减算法只存在这个大函数内部其他地方想复用这个算法只能复制粘贴。测试时为了覆盖一个分支要构造包含各种字段的order对象分支之间还互相干扰单元测试写起来特别痛苦。第四分支冲突不可控。这个例子还好实际业务中一旦出现“既是会员又有满减券还参与了秒杀”的组合这段代码只能按照顺序命中其中一个分支其他策略直接失效。这类隐藏bug在产品上线后很难被发现。所以问题不在于“要不要用if-else”而在于把策略选择和策略实现绑死在同一个地方。策略模式要做的就是把这两件事拆开。2. 策略模式核心概念把“怎么算”从“什么时候算”里抽出来2.1 三个参与者分别是谁策略模式在GoF里的定义是定义一组算法将每个算法都封装起来并且使它们之间可以互换。放到JavaScript的日常开发里我习惯把它理解成三个角色策略对象每一个算法、每一条规则都封装成一个独立的方法或对象。所有策略对外暴露相同的入口比如接收相同的参数返回相同结构的结果。上下文Context持有一个策略引用负责调用策略。它不关心策略内部的实现细节只关心“我调用这个策略能得到正确的结果”。客户端决定当前使用哪个策略传给上下文。举个最简单的例子如果我要封装加法和减法两种计算方式const strategies { add: (a, b) a b, subtract: (a, b) a - b, }; function calculate(strategyName, a, b) { const strategy strategies[strategyName]; return strategy(a, b); } calculate(add, 1, 2); // 3 calculate(subtract, 5, 3); // 2strategies里每个方法就是一个策略calculate就是上下文。将来要增加乘法、除法往strategies里加一个方法就行calculate一行都不用动。这个简单的思想就是整个策略模式的核心。2.2 一个生活化类比导航选路我用一个类比给自己和非技术同事解释策略模式你打开导航APP从家到公司系统会给你三条路线——用时最短、距离最短、不走高速。这三条路线的计算方式完全不同但导航APP本身上下文知道要怎么调用它们。如果你选择“用时最短”它就调用对应的路线算法如果选择“不走高速”它再调用另一个算法。如果不用策略模式导航APP就得写出一个几千行的if-else链如果用户选A返回路线A如果用户选B返回路线B。听起来就很荒唐。但很多业务代码恰恰就是在这种“荒唐”的模式下写出来的。2.3 策略模式到底解决了if-else的什么问题策略模式的核心价值不是帮你消灭所有if-else而是把“选择分支”变成“查表”把“每个分支里的复杂逻辑”拆成独立单元。判断该走哪个分支本身就是程序正常运行的一部分这没错。错的是把判断和算法混在一起让二者没法独立变化。策略模式强调的是把算法从使用场景中解耦。当多个算法处理同一类问题时每个算法应当是独立、可插拔的上下文只需要面向约定的接口编程。这样带来的直接好处是新增算法不需要修改上下文扩展从“改代码”变成“加代码”回归测试的成本大大降低。3. 案例一电商促销计价系统重构实录3.1 第一步把每种计价方式封装成策略回到第一节的计价函数。第一步我不要去动calcPrice里的任何逻辑先把每个促销分支抽成一个独立的函数塞进一个策略对象里const PROMOTION_STRATEGIES { // 无促销原价 normal(amount) { return amount; }, // 会员价统一95折 vip(amount) { return amount * 0.95; }, // 满减分阶梯 fullReduction(amount) { if (amount 500) { return amount - 120; } if (amount 300) { return amount - 60; } if (amount 200) { return amount - 30; } return amount; }, // 优惠券立减 / 折扣 coupon(amount, order) { if (order.coupon.type cash) { return amount - order.coupon.value; } if (order.coupon.type discount) { return amount * order.coupon.discount; } return amount; }, // 秒杀直接用固定价 seckill() { return 99; }, };这段代码里我做了几个调整把原来分散的order.vip、order.fullReduction、order.coupon这些布尔状态统一转换成一个策略名每个策略的入参我尽量保持统一至少第一个参数都是金额有的策略可能需要更多上下文信息比如优惠券那我就把整个order对象作为第二个参数传进去。这个约定很重要它保证了所有策略在调用侧看起来是“同构”的。3.2 第二步用策略注册表替代else if有了策略对象calcPrice就可以被彻底重写从一坨分支判断变成一行查表加一行调用function calcPrice(order) { const strategyName resolveStrategyName(order); const strategy PROMOTION_STRATEGIES[strategyName] || PROMOTION_STRATEGIES.normal; return strategy(order.amount, order); } // 根据订单信息解析出当前应该使用哪种促销策略 function resolveStrategyName(order) { if (order.seckill) return seckill; if (order.coupon) return coupon; if (order.fullReduction) return fullReduction; if (order.vip) return vip; return normal; }这里我特意把resolveStrategyName拆成了一个独立函数。它只负责“判断现在是哪种促销”不管“怎么算价格”PROMOTION_STRATEGIES只负责“怎么算价格”不管“现在是哪种促销”。两个函数职责单一加促销类型的时候你只需要对照当前订单命中的策略名是什么对应的算法是否已经存在。关于查表我用的是PROMOTION_STRATEGIES[strategyName]这本身就是一个O(1)的对象属性查找。从性能角度它比一串if-else不会有任何劣势。更重要的是策略表读起来一目了然有哪些策略、每个策略叫什么扫一眼就知道。注意resolveStrategyName本身仍然是if-else。这没问题因为“条件选择”这一步总得有人做。好的设计不是消灭所有分支而是让每个函数只做一件事让分支和算法分离。3.3 第三步新增促销类型只加函数不改原逻辑重构完成后最关键的变化体现在“加需求”的时候。有一天产品经理说“我们再增加一个‘买二送一’的促销规则吧。”如果是原来的if-else我需要在函数末尾找一个合适的位置插入一个新的else if然后祈祷别影响前面的分支。现在我只需要做两件事// 1. 在策略表里新增一个策略 PROMOTION_STRATEGIES.buyTwoGetOne function(amount, order) { if (order.quantity 2) { const freeCount Math.floor(order.quantity / 2); const unitPrice amount / order.quantity; return amount - freeCount * unitPrice; } return amount; }; // 2. 在解析函数里新增一个判断 function resolveStrategyName(order) { if (order.buyTwoGetOne) return buyTwoGetOne; // 新促销优先级最高 if (order.seckill) return seckill; if (order.coupon) return coupon; if (order.fullReduction) return fullReduction; if (order.vip) return vip; return normal; }注意calcPrice函数一行代码都没有改。改动被限制在“新增”的范围内这就是开闭原则的落地。更妙的是如果以后促销类型继续膨胀到100种calcPrice还是这三行不会变成一个怪物函数。3.4 这个案例里的几个关键决策这个案例里最有价值的一步其实不是把算法抽出来而是定义了策略的输入输出约定。我当时和团队定了一条铁律所有促销策略的第一个参数必须是订单金额第二个参数是原订单对象返回值必须是最终价格。有了这个约定策略之间才能互相替换、自由组合这是策略模式能跑起来的前提。没有约定策略表很快就会变成另一种if-else的藏身之处。另外我还在策略表里保留了normal作为兜底策略。凡是没有命中任何促销类型的订单都会走normal保证程序永远有一条可执行的路。后面第6节会专门讲兜底策略的重要性。4. 案例二表单校验规则集——策略模式的另一个高频战场4.1 传统校验代码为什么越写越乱促销计价是策略模式的经典案例但我在实际项目里用得最多的地方其实是表单校验。因为一个复杂表单的校验规则往往比促销类型还多还容易叠加。先看一段传统的校验代码function validateUser(form) { const errors []; const username form.username; const email form.email; const phone form.phone; // 用户名规则 if (!username) { errors.push(用户名必填); } if (username.length 3) { errors.push(用户名不能少于3个字符); } if (username.length 12) { errors.push(用户名不能超过12个字符); } if (username.includes(admin)) { errors.push(用户名不能包含敏感词 admin); } // 邮箱规则 const emailReg /^[\w.-][\w-](\.[\w-])$/; if (!emailReg.test(email)) { errors.push(邮箱格式不正确); } if (email.length 50) { errors.push(邮箱不能超过50个字符); } // 手机号规则 const phoneReg /^1[3-9]\d{9}$/; if (!phoneReg.test(phone)) { errors.push(手机号格式不正确); } return errors; }这段代码和促销计费犯了同样的毛病每一条校验规则和字段判断混在一个大函数里。当我需要增加“身份证号码校验”的时候又得打开这个函数找到对应字段的位置插入一段新逻辑。当10个字段各有5条规则时这个函数会有50段逻辑块超过一千行不是梦。还有个更隐蔽的问题规则不可配置。如果产品说“相同规则不同场景要求不同”比如后台管理系统允许用户名长度为20前台C端限制为12你只能复制出另一个函数或者加一个参数分支。配置和代码之间的鸿沟会越来越大。4.2 把每条规则变成一个策略策略模式对这个问题的解法是把“规则”本身变成可独立配置的策略。我定义一个包含所有规则的策略表const validators { required(value) { return value ? null : 该字段必填; }, minLength(value, min) { return value.length min ? null : 不能少于${min}个字符; }, maxLength(value, max) { return value.length max ? null : 不能超过${max}个字符; }, email(value) { return /^[\w.-][\w-](\.[\w-])$/.test(value) ? null : 邮箱格式不正确; }, phone(value) { return /^1[3-9]\d{9}$/.test(value) ? null : 手机号格式不正确; }, notContain(value, word) { return value.includes(word) ? 不能包含${word} : null; }, };每个策略的约定是接收字段值和若干参数如果校验通过返回null不通过返回错误提示字符串。这个约定的好处是统一了返回值形态后续可以做统一的错误处理。然后我定义一张规则表描述每个字段要应用哪些策略const userRules { username: [ { name: required }, { name: minLength, params: [3] }, { name: maxLength, params: [12] }, { name: notContain, params: [admin] }, ], email: [ { name: required }, { name: email }, { name: maxLength, params: [50] }, ], phone: [ { name: required }, { name: phone }, ], };最后写一个通用的执行函数遍历规则表并调用对应策略function validateField(value, rules) { for (const rule of rules) { const validator validators[rule.name]; if (!validator) { console.warn(未知的校验规则${rule.name}); continue; } const error validator(value, ...(rule.params || [])); if (error) return error; } return null; } function validate(form) { const errors {}; Object.keys(userRules).forEach((field) { const error validateField(form[field] || , userRules[field]); if (error) { errors[field] error; } }); return errors; }这段代码把原来的“规则实现”和“字段规则配置”拆开了。如果产品说“这个页面的用户名最多20字符”我不用改任何验证逻辑只需要在规则表里改一个数字const adminRules { username: [ { name: required }, { name: minLength, params: [2] }, { name: maxLength, params: [20] }, ], // ... };同样的策略集合不同的规则表可以组合出完全不同的校验逻辑。这就是行为可配置化。4.3 进阶实战组合策略、带参数策略与异步校验上面的基础版已经能覆盖大多数场景但真正让策略模式在表单校验里发挥威力的是它的扩展能力。比如异步校验。最常见的场景是“用户名是否已被注册”必须请求后端接口。异步策略可以这样写const validators { // ... async uniqueUsername(value) { if (!value) return null; const res await fetch(/api/check-username?name${encodeURIComponent(value)}); const data await res.json(); return data.ok ? null : 用户名已被占用; }, };执行入口改为async版本async function validateField(value, rules) { for (const rule of rules) { const validator validators[rule.name]; if (!validator) continue; // 这里用 await同步策略返回普通字符串异步策略返回Promise统一处理 const error await validator(value, ...(rule.params || [])); if (error) return error; } return null; }因为所有策略的返回值约定统一同步和异步策略可以共存于同一个规则表里执行函数不需要关心每个策略内部是不是异步。await天然兼容同步返回值这部分非常顺滑。再比如组合策略。有的场景要求“用户名必须同时满足A、B、C规则”但错误信息要一次性返回所有不满足项而不是只返回第一个。这时候可以把“多条规则”封装成一个复合策略这在策略模式里叫策略的组合使用。前面userRules其实已经是策略组合的一种形态更复杂的场景可以自己封装一个例如allOf的策略const validators { // ... allOf(value, { rules }) { const failures []; for (const rule of rules) { const error validators[rule.name](value, ...(rule.params || [])); if (error) failures.push(error); } return failures.length ? failures.join() : null; }, };4.4 案例小结配置化带来的意外收获重构完校验模块后我最大的感觉不是代码量少了而是测试和评审都简单了。每个策略是独立函数可以直接针对单个规则写单元测试评审代码时看到的是规则表和数据流不再是一千行逻辑堆叠。更重要的是后端同事也开始参考这套规则表设计接口校验前后端的字段规则第一次做到了基本对齐。这个场景再次印证了策略模式的核心价值算法可以独立变化使用算法的客户端不感知变化。校验规则增加了字段配置增加了但执行引擎不用变。5. 策略模式什么时候该用什么时候只是自我感动5.1 出现这些信号可以考虑上策略模式我在代码评审时发现符合下面这些特征的地方用策略模式基本都不会错信号说明if-else链长度超过5个分支一多函数就开始难以阅读需求频繁新增“类型”或“规则”产品每俩月加一种促销/玩法代码每俩月改一次主流程多个算法处理同一类问题比如多种计价方式、多种校验规则、多种日志格式化方式分支里的代码块很大每个分支内部有大量逻辑抽成独立函数后主流程会非常清爽相同条件判断在多个地方重复出现多个函数都在判断同一组促销类型说明判断逻辑应该收敛只要命中两三条策略模式就是一个值得考虑的方案。5.2 这些场景请放过策略模式策略模式也不是万金油。我见过一些团队不问青红皂白所有if-else都改成策略表结果代码反而更绕。下面这些场景我建议你放过策略模式分支只有2到3个而且逻辑非常简单一个三元运算符就能搞定。业务逻辑极其稳定几乎没有新增分支的可能性。策略内部只有一行return强行抽出来纯属增加间接层。团队对函数式写法不熟悉对象套对象反而增加理解成本。这里要强调一点简单的if-else不是罪不可扩展的分支结构才是问题。如果你确定未来不会有新分支那就老老实实写if-else省得为了“优雅”而搞出一套需要三天才能看懂的设计。5.3 策略模式与简单工厂、模板方法的配合方式实际开发中我不会孤独地使用策略模式通常会配合其他模式一起用。如果“选择哪个策略”的逻辑特别复杂比如要依赖配置中心、要按用户等级动态决定我会把resolveStrategyName升级成一个简单工厂由工厂负责创建并返回策略对象。工厂和策略模式天生就搭——策略模式负责“算法族”工厂负责“如何实例化算法”。如果策略执行的流程里有一些固定步骤比如所有促销都需要“校验资格、计算价格、记录日志”但中间某一步的具体算法不同我会用模板方法模式定义流程骨架把变化点留给策略去实现。这样流程不会散落到各个分支里。提示设计模式不是独立使用的它们是相互协作的词汇表。策略模式解决“算法选择”工厂模式解决“对象创建”模板方法解决“流程固定”。真实项目里它们经常一起出现。6. 生产环境落地策略模式的五个关键细节6.1 策略集合用什么容器承载我在文章里先后用过对象字面量和Map两种方式。二者各有适用场景对比如下容器类型优点缺点适用场景对象字面量写法简洁直观字段查找O(1)序列化和调试方便不能方便地遍历所有策略key容易被原型污染用Object.create(null)可避免策略数量少、名称固定的场景Map支持动态增删、可按插入顺序遍历、key可以是任意值写法略啰嗦策略需要运行时注册、动态扩展的场景class面向对象风格每个策略可持有自己的状态代码量大this绑定易出错策略内部状态复杂的场景我个人更喜欢对象字面量因为JavaScript的函数本身就是一等公民用普通对象承载策略函数足够简单。只有当策略数量可能动态增加、或者需要热插拔的时候才把容器换成Map并封装注册函数。6.2 兜底策略让程序永远有路可走策略模式里最容易忽视的问题就是传入一个不存在的策略名怎么办。如果不做兜底strategies[type]得到的会是undefined调用时直接抛TypeError。我在第一个案例里已经展示了兜底策略的写法const strategy PROMOTION_STRATEGIES[strategyName] || PROMOTION_STRATEGIES.normal;这种写法的好处是即使解析函数出现了未知状态程序也不会崩溃会回到一个合理的默认行为。如果是表单校验场景未知规则名建议这样做if (!validator) { console.warn(未知的校验规则${rule.name}); continue; }放过未知规则比让整个校验崩溃要好。上线后发现漏写了一个规则最多是校验不到不至于连提交按钮都不可用。当然console.warn必须保留让开发环境能及时暴露问题。6.3 动态注册策略策略模式怎么变成插件化如果你做的是中台系统不同业务线可能有不同的促销规则不太可能把所有策略都写在一个静态对象里。这时可以做一个策略注册表支持运行时注册class StrategyRegistry { constructor() { this.map new Map(); } register(name, strategy) { if (typeof strategy ! function) { throw new Error(策略 ${name} 必须是函数); } this.map.set(name, strategy); } get(name) { return this.map.get(name) || this.map.get(default); } execute(name, ...args) { const strategy this.get(name); if (!strategy) { throw new Error(未注册任何默认策略); } return strategy(...args); } } // 使用 const registry new StrategyRegistry(); registry.register(default, (amount) amount); registry.register(seckill, () 99); registry.execute(seckill, 1000); // 99这样外部业务线可以独立注册自己的策略核心系统不需要知道每个策略的实现细节。这就是把策略模式变成插件系统的思路。我在实际项目里用这种方式改造过优惠券引擎基础规则由中台提供各业务线自行注册叠加规则非常灵活。6.4 命名规范和调试体验很多人只关注策略模式怎么写不关注怎么给策略起名。策略的命名其实很影响可维护性。我总结的规律是策略函数名要能直接对应业务概念比如fullReduction、seckill、vip不要用strategy1、strategy2这种没有语义的名字。策略名也是配置文件里的key业务方和产品经理能直接对上号沟通成本会低很多。调试的时候策略模式其实比if-else更容易。因为每个策略有独立的函数名报错栈会精确指向某个策略函数而不是指向那个几百行的大函数。给策略函数起好名字错误日志的可读性会显著提高。我有一次排查线上促销价格异常就是因为错误栈里出现了fullReduction策略三分钟就定位了问题。换成分支写法可能要在那段巨型函数里来回打断点十分钟起步。6.5 性能与团队协作考量性能方面策略模式几乎不会成为瓶颈。对象属性查找和Map查找都是常数级复杂度和if-else链的逐条判断相比没有劣势甚至更优。真正要注意的是不要在策略内部引入重复开销比如每个策略每次执行都重新创建正则表达式、每次校验都发重复的请求。这和模式无关是代码实现的常识问题。团队协作上我觉得策略模式最大的贡献是让“加代码”变得安全。以前是新人进来面对一个千行函数不敢动现在是新人进来照着策略表的格式“抄一段”风险可控。当然前提是策略表的规范要先写清楚比如输入是什么、输出是什么、命名规则是什么。这些约定不写下来策略模式最终也会慢慢腐化。结尾从我自己的体会来说策略模式是我在JavaScript项目里用得最多、回报率最高的一个设计模式。它不像有些模式那样复杂、难以落地只要把“算法选择”和“算法实现”拆开用一个对象或Map装起来就能立即见效。我接手过的几个烂摊子项目几乎都是靠这一招先把最乱的主流程理顺后面才谈得上继续优化。最后再分享一个小技巧当你犹豫“这里该不该用策略模式”的时候试着问自己一个问题——如果下个月产品要在这里加一个新分支我是改函数还是加一个策略如果答案是“加一个策略”那就别犹豫立刻动手。设计模式的意义不是背熟定义而是让代码在未来的变化中依然站得住脚。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。