☰
告别 if/else 噩梦:用表驱动、策略、状态、责任链模式重构代码
2026/10/21 6:52:53 网站建设 项目流程

接手过老系统的朋友,应该都经历过那种看到某个方法就觉得头皮发麻的时刻:一个方法里塞了几十层 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 Map<String, 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 Map<String, Function<Order, 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 Map<String, PayStrategy> strategyMap; public PayStrategyFactory(Map<String, 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/else

4.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,它的特点是顺序感特别强。比如用户提交订单前要做一堆校验:基础参数检查、用户状态检查、库存检查、风控检查、限购检查。原来的写法就是一个接一个的 if:

public 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,我建议先别急着选模式,静下来把这段代码当作一段“文字”重新读一遍,搞清楚它真正判断的是什么。看清维度,选型就不再困难。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询