资讯动态

告别 if/else 噩梦:用表驱动、策略、状态、责任链模式重构代码

发布时间:2026/10/22 10:01:08 来源:尧图企业网站定制
接手过老系统的朋友应该都经历过那种看到某个方法就觉得头皮发麻的时刻一个方法里塞了几十层 if/else外层判断订单类型里层判断支付渠道再里层还要判断用户等级和优惠券来源每个新需求进来就往里面再插一段。最让人难受的不是代码跑不起来而是没人敢动它谁碰谁背锅。最近我重构一个订单流程模块时就面对了这么一堆“祖传分支”这次我没有硬着头皮继续打补丁而是把里面最典型的几类 if/else 分别用表驱动、策略模式、状态模式、责任链模式做了替换。这四种设计模式不是我拍脑袋选的而是它们对应了 if/else 最常出现的四种场景条件就是查表、行为按类型分流、状态递进迁移、多个环节顺序处理。这篇文章把这四种方案的适用场景、代码模板、选型理由和我在实操中踩过的坑一次讲清楚。无论你是刚工作没多久的初级开发还是正在做系统重构的骨干都可以拿这些案例直接参考。1. 为什么 if/else 会失去控制1.1 类型判断与状态判断是重灾区先说一个我自己的观察。业务代码里的 if/else 失控绝大多数不是因为算法复杂而是因为一直在做“判断”。判断又可以分成两类。第一类是类型判断。一个对象或者参数来决定走哪段逻辑比如支付渠道不同、会员等级不同、消息通知方式不同。这类分支往往一开始只有两三个后面产品不断加类型if/else 就像滚雪球一样滚起来。第二类是状态判断。一个对象在生命周期里随着操作不断切换状态比如订单从待付款到待发货再到已完成每一步都要判断当前状态是否允许这个操作以及操作之后应该变成什么状态。这类分支如果只是简单堆 if/else很快就会出现状态交叉、非法迁移防不胜防的问题。我在这次重构的订单模块里两种问题都遇到了。订单类型决定成交价怎么算这是典型的类型判断订单状态又决定了哪些动作允许执行这又是典型的条件判断。两套维度交织在一起的时候代码就像一团打了死结的毛线光看逻辑头就大了。1.2 该怎么判断“该动手了”不是所有 if/else 都要被“消灭”。我见过一些团队为了不用 if/else 硬套设计模式结果代码比原来还难懂。所以动手之前先分辨自己手里的 if/else 是不是真的需要治理。我觉得有几个信号比较直观。某个 if/else 方法开始超过一屏甚至超过几百行。新增一个需求需要改动多个分支位置而且很容易漏掉其中一个。分支里开始出现互相矛盾的状态组合比如“订单未支付但已发货”这种奇葩情况。测试用例很难写因为要把所有分支组合都覆盖到数量呈指数增长。同事之间的口头禅变成了“这段代码先别动改坏了算谁的”。只要中了三个以上基本就可以判断这类代码已经进入“负资产”状态。与其继续在里面加分支不如退出来重新梳理一下。不过梳理之前先别急着选模式要先看清这段 if/else 到底在判断什么。判断的东西不一样该用的方案完全不一样。2. 表驱动把分支变成一张“字典”2.1 一个让我印象深刻的场景表驱动是我在重构里用起来最顺手也是最容易见效的一招。它解决的是最单纯的一类 if/else条件给定的值决定了一个固定的结果分支之间没有依赖也没有副作用。举个例子。原来系统里有一段这样的代码按照用户等级计算折扣。public double calcDiscount(User user) { if (GOLD.equals(user.getLevel())) { return 0.8; } else if (SILVER.equals(user.getLevel())) { return 0.9; } else if (BRONZE.equals(user.getLevel())) { return 0.95; } else { return 1.0; } }这段代码读起来好像还算直观但等级一多问题就来了。你每加一个等级就得在 if/else 链子后面再挂一个分支代码行数不断增加而且每个调用点都要重复一段类似的判断。更头疼的是这个折扣配置很多时候是运营那边在维护他们不可能每次变更都来改代码。表驱动的思路很简单把“判断条件”和“返回结果”的关系做成一张数据表代码只负责查表。private static final MapString, Double DISCOUNT_TABLE Map.of( GOLD, 0.8, SILVER, 0.9, BRONZE, 0.95 ); public double calcDiscount(User user) { return DISCOUNT_TABLE.getOrDefault(user.getLevel(), 1.0); }这样一眼就能看清楚所有等级的折扣配置。以后要新增等级只需要改这一张表代码本身一行都不用动。如果折扣规则经常变还可以进一步把这张表搬到配置文件甚至数据库里让业务人员自己去维护。2.2 表驱动的边界在哪里表驱动也不是万能的。它最适合“一个输入对应一个输出”这种稳定映射。但有些 if/else 不是这么简单比如每个分支后面还跟着一大段逻辑而不只是返回一个值。这时候表驱动的变体是把“函数”作为表的 value也就是做一个函数映射表。private static final MapString, FunctionOrder, Double DISCOUNT_FUNC_MAP Map.of( GOLD, order - order.getAmount() * 0.8, SILVER, order - order.getAmount() * 0.9 );不过我要提醒一句函数映射表虽然更灵活但 debug 的时候要比普通查表麻烦不少。你追踪一个被调用的函数得先去看表里存的是什么函数对象跟读 if/else 相比不是直观。所以这块要克制使用。另外要注意表驱动的映射关系一旦从代码里挪到了配置系统就会失去一部分编译期检查能力。原来 if/else 里写错了字符串直接编译报错或者走 default 分支现在从配置中心拿到一个错误 key排查起来会绕一圈。解决办法有两个一是用枚举而不是裸字符串做 key二是给配置数据加校验在启动时加载一张不存在的 key 就快速失败。我在这次重构里把用户等级、支付渠道提示语、订单来源对应的默认状态这些都改成了表驱动。改完之后最有感觉的变化是新增一个等级时不再需要翻代码找判断位置了加一行映射测试用例也只需要补一条“查表”的用例收益立竿见影。3. 策略模式用接口替换一大片条件逻辑3.1 重构前支付渠道那段“祖传代码”如果说表驱动解决的是“结果固定”的分支那策略模式解决的就是“不同分支要做不同事”的情况。这次重构里遇到的典型案例是支付渠道处理。系统支持好几种支付方式每种方式的参数、回调、对账逻辑都完全不一样。原来的实现就一个超级方法public void pay(Order order, String payType) { if (WECHAT.equals(payType)) { // 微信支付拼接参数、调用接口、处理回调 } else if (ALIPAY.equals(payType)) { // 支付宝支付另一套参数、接口和验签逻辑 } else if (UNIONPAY.equals(payType)) { // 银联支付再一套逻辑 } else { throw new BizException(不支持的支付类型); } }这个方法的行数早就超过了两百行。每次改一种支付方式都要在这个巨型方法里定位自己那一块代码。有时候两个支付分支都想用某个公共方法又只能提取成 private 方法提取完方法的依赖关系也越来越乱。这类代码最合适的重构方向就是策略模式。把“支付”这个行为定义成接口不同支付渠道各自实现一个策略类然后把原来的 if/else 位置替换成从工厂里拿策略并调用。3.2 重构后策略接口 工厂public interface PayStrategy { void pay(Order order); }然后给每种支付渠道写一个实现类。public class WechatPayStrategy implements PayStrategy { Override public void pay(Order order) { // 微信支付逻辑 } } public class AlipayPayStrategy implements PayStrategy { Override public void pay(Order order) { // 支付宝支付逻辑 } }原来的业务代码就不再关心具体支付渠道只依赖 PayStrategy 接口PayStrategy strategy PayStrategyFactory.of(order.getPayType()); strategy.pay(order);这里的 PayStrategyFactory 本身还是需要一个判断。public static PayStrategy of(String payType) { switch (payType) { case WECHAT: return new WechatPayStrategy(); case ALIPAY: return new AlipayPayStrategy(); default: throw new BizException(不支持的支付类型); } }很多刚接触策略模式的人到这里会问这不是还没干掉 if/else 吗没错策略模式并不追求在代码里彻底消除所有分支判断。它真正做的事情是把原来散落在各个业务调用点的 if/else 收拢到工厂这类地方让业务逻辑不再依赖具体实现。支付渠道的 if/else 仍然存在于工厂里但它只出现一次而且不会因为未来新增支付方式而把巨型方法再加长。如果你用的是 Spring 这类框架连工厂里的 switch 都可以省掉。把每种策略注册成一个 Bean再用一个 Map 注入到工厂里新加策略类时就自动生效。Service public class PayStrategyFactory { private final MapString, PayStrategy strategyMap; public PayStrategyFactory(MapString, PayStrategy strategyMap) { this.strategyMap strategyMap; } public PayStrategy of(String payType) { return Optional.ofNullable(strategyMap.get(payType)) .orElseThrow(() - new BizException(不支持的支付类型)); } }3.3 跳出代码层面看策略模式的本质我在这次重构里对策略模式还有一个挺深的感触它不仅仅是把 if/else 换了个写法更是把一段会变化的逻辑隔离出来让外层系统只依赖稳定接口。打个比方原来系统里的业务代码就像一个人要把所有操作技能都记在脑子里你让他打字他也要管让他洗菜他也要管。策略模式相当于把这些技能外包出去交给专门的人负责你只需要告诉他“我要打字”至于具体是哪个打字法实现他根据情况自己决定。用策略模式时最容易犯的错就是把策略类写得特别薄只是把原 if/else 里的代码原封不动搬进去。这样确实是策略模式但价值不大。正确的做法是在迁移的同时把策略之间可复用的部分重新抽取把每个策略真正变成独立而内聚的单元。另外如果多个策略之间只有参数不一样逻辑结构完全一致这种情况用策略模式反而是过度设计。比如不同消息渠道只是模板和发送地址不同直接用一个表驱动把渠道映射到参数配置上更合适。策略模式的核心是“行为不同”不是“参数不同”这两个要分清楚。4. 状态模式有状态迁移时别硬堆 if/else4.1 状态机思路是什么样的订单模块里最让人头疼的另一类 if/else就是订单状态流转的判断。比如一个订单当前处于“待付款”那它可以支付、可以取消但处于“待发货”它不能支付只能发货或者申请退款。如果全部用 if/else 来写大概长这样if (order.getState() ORDER_STATE_UNPAID) { if (pay.equals(action)) { // 执行支付 } else if (cancel.equals(action)) { // 执行取消 } } else if (order.getState() ORDER_STATE_UNSHIPPED) { if (ship.equals(action)) { // 执行发货 } }状态一多这种代码就是一场噩梦。每增加一个状态就要把所有 if 条件翻一遍确认新状态允许哪些操作不允许哪些操作漏掉一个判断就是线上事故。状态模式的做法完全不同。它把状态本身抽象成对象让状态负责它自己“当前允许做什么”。public interface OrderState { void pay(OrderContext context); void ship(OrderContext context); void cancel(OrderContext context); }待付款状态实现自己的行为逻辑public class UnpaidState implements OrderState { Override public void pay(OrderContext context) { // 执行支付逻辑 context.setState(new UnshippedState()); } Override public void ship(OrderContext context) { // 非法操作直接抛异常 throw new IllegalStateException(待付款订单不能发货); } Override public void cancel(OrderContext context) { context.setState(new CancelledState()); } }调用方不需要再关心订单当前是什么状态只需要把动作发给当前的状态对象由它自己决定能不能做、做完以后去哪。这样状态维度的 if/else 就没有了判断逻辑被分散到了各个具体状态类里每个状态类只关心自己那一份规则。4.2 什么情况建议用状态模式状态模式听起来挺高级但也不是所有带状态的业务都适合硬套。我这次重构的时候就发现有一个看起来像状态流转的业务其实用户的身份等级也在同时参与决策光用状态模式反而别扭。我的判断标准是这样的如果一个业务对象的状态数量比较多、每个状态下的操作列表也比较多而且操作会带来状态转移那用状态模式非常合适。比如订单、审批流、工单这些场景天然就是一个状态机用状态模式能带来清晰的结构。但如果状态只有两三个操作也不多直接用 if/else 反而更好。比如一个开关只有开和关两个状态硬要写两个状态类维护起来又啰嗦又没有意义。这就是常说的过度设计。另外状态模式容易出的坑是状态对象之间的关联没有理清楚。比如某个状态在当前操作完成后要流转到另一个状态如果你在多个状态类里都 new 了别的状态实例那状态切换关系就会散落在各个类里看着还是乱。我的建议是状态对象的创建尽量集中在状态类的迁移方法里不要到处 new这样状态机整体的流转关系才清晰。还有一点要提醒状态模式里经常要在上下文里保存很多公共数据如果这些数据被多个状态修改就容易出现“共享变量”混乱。我在重构一个旧系统时遇到过上下文里同时存着订单金额、库存数量、用户优惠券等多个字段不同状态各改各的最后很难排查是谁在哪个环节改了数据。建议在上下文对象上做严格的字段封闭每个字段只允许特定状态修改必要时直接拆成多个独立对象。5. 责任链模式顺着链条逐个尝试5.1 一个典型示例提交订单前的层层校验有另一类 if/else它的特点是顺序感特别强。比如用户提交订单前要做一堆校验基础参数检查、用户状态检查、库存检查、风控检查、限购检查。原来的写法就是一个接一个的 ifpublic void submitOrder(Order order) { if (!checkBase(order)) { throw new BizException(参数错误); } if (!checkUser(order.getUserId())) { throw new BizException(用户状态异常); } if (!checkStock(order)) { throw new BizException(库存不足); } if (!checkRisk(order)) { throw new BizException(风控拦截); } // 下单逻辑 }短期内看没什么问题但实际开发中这种校验链经常变化。今天产品要加一个“是否在限购名单里”的判断明天风控要调整校验顺序后天运营又要插入一个“只允许会员购买”的检查。每次需求一来就得在这个方法中间找位置插入一段 if很快这个方法就变得臃肿而且顺序调整也容易出差错。责任链模式就是把每个校验封装成一个独立的处理器所有处理器串成一条链请求从链头开始依次经过每个处理器直到某个处理器决定中断或全部处理完。public abstract class ValidationHandler { protected ValidationHandler next; public ValidationHandler setNext(ValidationHandler next) { this.next next; return this; } protected void fireNext(Order order) { if (next ! null) { next.validate(order); } } public abstract void validate(Order order); }具体校验类实现public class StockValidationHandler extends ValidationHandler { Override public void validate(Order order) { if (!checkStock(order)) { throw new BizException(库存不足); } fireNext(order); } }调用时只需要把处理器串起来ValidationHandler chain new BaseValidationHandler() .setNext(new UserStatusValidationHandler()) .setNext(new StockValidationHandler()) .setNext(new RiskValidationHandler()); chain.validate(order);以后要新增校验只需要新写一个 handler然后在链上追加或插入即可不需要改动已有的处理器逻辑。5.2 责任链和前面三种的差异责任链模式和策略模式很容易弄混。毕竟从代码结构上看策略模式也是把每个 if 分支变成独立类责任链也是。我觉得它们最大的差异在于意图。策略模式是“根据条件选择一个最终的处理器”分支之间是互斥的条件满足就执行那一套逻辑不再继续往下走。责任链则是“把同一个请求依次交给多个处理器”每个处理器都看一下自己能不能处理、需不需要拦截更看重顺序。打个比方策略模式像餐厅点餐你点菜之后只有一个厨师负责做这道菜。责任链模式像送外卖外卖从商家送到骑手从骑手送到小区驿站再从驿站送到你手里是一环扣一环的接力。在下单校验这个场景里所有校验都得跑一遍顺序很重要所以用责任链比策略模式更贴切。5.3 责任链的常见坑责任链模式用起来比前三种更容易踩坑我整理几个比较常见的。第一个坑链的顺序被外部依赖绑定。有时候两个 handler 之间其实没有严格的先后关系但业务运行久了实际上已经形成了一种隐式契约。比如库存校验必须先于风控因为风控要用到库存计算结果。这种隐式耦合很危险后面的人看着代码以为可以随便调整顺序一旦调整就出问题。我的建议是每个 handler 的输入输出边界要尽量清晰并且在注释里注明它能提供哪些数据给后续节点。第二个坑链里某个 handler 简单地“跳过”下一步。责任链虽然叫责任链但不少节点是没有强拦截能力的它们只是做一些观察或者打日志。如果这些节点没有正确调用 fireNext整条链就会静默断开。我重构的时候遇到过一个诡异 bug某条链走到第二个节点就停住了排了半天发现是这个节点把 fireNext 放到了 return 语句后面。这种问题靠代码 review 很难避免建议每个 handler 统一在 finally 或 try-finally 中调用下一个节点或者干脆用更严格的模板方法把 fireNext 固化成强制调用。第三个坑链上的节点数量过多。我见过把十几个校验都挂在一条链里的设计好处是每个节点都很简单坏处是排查问题时要在十几个类之间跳转阅读成本很高。链不是越长越好如果某些节点逻辑上总是成组出现的可以考虑合并成一个节点类或者像表驱动那样用函数列表的方式让链不要膨胀到夸张的程度。6. 四种模式如何选型决策表与实操经验6.1 一张决策表很多朋友会问学了一堆设计模式之后真到了一堆 if/else 面前还是不知道选哪个。我这次重构完最大的体会是别先想用什么模式先想这段 if/else 到底在表达什么。判断维度不一样模式选择完全不同。我把这次的判断逻辑整理成一张表给大家做个参考。场景特征推荐方案备选方案条件本质上是一个固定映射输入一个值返回一个固定结果表驱动表驱动 函数对象条件决定一段彼此独立的复杂行为行为之间互斥策略模式表驱动存函数配合工厂条件描述对象的生命周期状态并且操作会改变状态状态模式表驱动 状态校验逻辑多个检查或处理按顺序穿过每个节点都可能中断责任链模式策略模式 固定顺序编排如果一段代码同时表现出两类特征比如既要看订单类型也要看订单状态那就不要强求一个模式解决所有问题。这次我的做法是先拆轴把类型维度用策略模式处理把状态维度用状态模式处理两层叠起来但不要混在一个类里。6.2 动手重构的先后顺序我这次重构一个非常大的 if/else 方法时没有直接动手改结构而是先走了一遍小步重构的流程。第一步先给原方法写一批基线测试用例。把每种 if/else 分支的输入输出都覆盖到。没有测试保护就去重构 if/else 就像没有安全绳走钢丝不是不能做而是风险太大。第二步把 if/else 内部的大段逻辑先用 IDE 的“提取方法”功能拆成若干个小方法保证每个小方法只做一件事。这个阶段不改变行为只调结构跑一遍测试确认结果一致。第三步根据“是查表、是行为分流、是状态迁移、是顺序处理”这四个维度锁定每一种分支对应的模式。这一步最重要因为我见过不少人在这步会犯一个错误看到 order 类型不同就让每种类型实现策略接口结果发现这些策略类之间重复代码特别多原因是有些分支其实属于状态维度。第四步逐个替换。每替换完一个分支就运行一次测试。不要指望一次性把整个大方法改完那样很难定位问题。第五步最后清理。删除无用的 default 分支、过时的注释把工厂或者映射表的创建逻辑尽量收敛到一个只做初始化的类里。按这个顺序走下来整个重构过程基本是可控的。哪怕中途出现 bug也能很快通过测试定位到是哪个步骤引入的。6.3 我这次重构踩的坑这次重构里我踩了几个比较典型的坑值得单独写出来。第一个坑是表驱动引入空指针。原来 if/else 里有个 default 分支专门处理未知等级改成 Map 之后我一开始没有用 getOrDefault而是直接 get。结果线上来了一个旧数据用户等级是一个已经下线过的枚举值直接空指针。换成 getOrDefault 并且默认值配成普通折扣后才解决。这提醒我表驱动虽然简洁但“找不到 key”的兜底逻辑一定要在重构时就同步考虑。第二个坑是策略模式里偷懒把所有策略共用一个父类。为了省代码我在基类里写了一些公共方法结果两个策略对“订单金额”的预计算逻辑理解不一致一个认为要含运费一个认为要减掉运费。最后排查的时候才发现是基类这个方法被两处共用各自偷偷 override 了。这次教训是策略之间共享逻辑要非常小心如果你发现几个策略要共享很多逻辑那很可能这几个策略不应该拆得这么碎。第三个坑是状态模式里的非法操作。有些状态在业务上根本不可能发生“支付”这个动作我在状态类里直接抛了 UnsupportedOperationException。但后来测试发现业务上有一种异常订单状态代码希望把这种异常订单自动恢复成待付款结果走到支付时当场崩溃。后来我改成先检查一次订单当前状态如果状态不匹配就记录日志并通知人工介入而不是直接抛异常。这说明状态模式虽然把判断封装得好但边界场景脏状态、异常恢复还是要单独设计一条路不能一刀切。这些坑都不是设计模式本身的问题而是我重构时对领域规则理解得不够仔细。理解业务规则永远比套用设计模式重要模式只是工具业务才是源头。这次把订单模块的 if/else 重构完之后我最直观的感受是代码行数并没有大幅度减少但阅读代码时再也不需要一层层往里跳了。每一种判断都变成了一块独立的、命名清晰的结构新增需求变成新增映射、新增策略、新增状态类、新增链节不用再心惊胆战地改那段别人不敢碰的老代码。如果你现在也面对着一堆怎么都理不清的 if/else我建议先别急着选模式静下来把这段代码当作一段“文字”重新读一遍搞清楚它真正判断的是什么。看清维度选型就不再困难。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑