说实话,我见过太多被 if-else 逼疯的代码了。刚接手一个支付系统时,核心计算函数里密密麻麻堆了三层 if-else,一个方法六百多行,改一个折扣规则要翻半天,测试用例补了十几个还是觉得心里没底。后来我用四种设计模式把这段代码重构成一组可插拔的对象,if-else 基本消失,新增规则只需要加一个类,改起来再也不心慌。这篇就把我当时重构的思路、踩过的坑,以及四种模式到底怎么选,原原本本讲清楚。适合正在写业务代码、被分支逻辑折磨的同学,也适合想把设计模式真正落地而不是只背概念的开发者。内容偏 Java,但思路完全通用,你用其他语言也一样能套。
1. 为什么 if-else 会越写越痛
1.1 一段正在腐烂的条件判断长什么样
先看一段特别典型的代码。你大概率在某个项目里见过类似的,甚至自己写过:
java复制public BigDecimal calcDiscount(Integer userLevel, BigDecimal price) {
BigDecimal discount;
if (userLevel == 1) {
discount = price.multiply(new BigDecimal("1.00"));
} else if (userLevel == 2) {
discount = price.multiply(new BigDecimal("0.95"));
} else if (userLevel == 3) {
discount = price.multiply(new BigDecimal("0.85"));
} else if (userLevel == 4) {
discount = price.multiply(new BigDecimal("0.75"));
} else {
discount = price.multiply(new BigDecimal("0.90"));
}
return discount;
}
这段代码刚写出来的时候,逻辑并不复杂。四个等级,四种折扣,一眼能看懂。但问题很快就来了:产品经理说用户等级要跟“是否会员”“是否新人”“是否参与满减活动”联动;运营说大促期间折扣要临时覆盖;地推说线下渠道要单独一套规则。于是这个函数开始长出第二层、第三层 if-else,甚至嵌套一个 switch,最后只剩下改代码的人和测试的人在互相安慰。
这还不是最可怕的。最怕的是某个分支里的逻辑特别长,中间夹着数据库查询、外部接口调用、异常处理,一个方法写到两百行以上。这个时候,任何一次改动都是在赌运气:你改了 else if 里的条件,可能影响的是另一个看似毫不相关的分支。
1.2 问题不在 if-else 本身,而在变化方向
先说明白,if-else 本身没错,它是最直观、最可读的分支方式。业务逻辑里本来就存在条件判断,强行把所有 if-else 消灭掉是走火入魔。真正的问题是:当条件分支开始频繁变化、持续增长,if-else 这种写法就会把“变化点”散落在同一个函数里,导致每次新需求都要动老代码,而这个老代码又特别容易被改坏。
换句话说,if-else 的痛点不是“分支”,而是“没有把变化封装起来”。你要判断的条件可能在变,分支对应的行为也可能在变,但代码里这两样东西硬是绑在了一起。设计模式解决的核心问题,就是把这个“绑死”的结构拆开,让你每次需求变化时只改该改的地方,而不是在蜘蛛网里找线头。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 策略模式:把“怎么算”交出去
2.1 适用场景:同一个动作,多种算法或规则
策略模式是我重构 if-else 时第一个用上的,也是上手最快、收益最明显的一个。它的思想一句话就能讲清楚:把“做同一件事”的不同算法,分别封装成独立的策略类,然后在运行时选择用哪一个。
再拿折扣计算来说。不管用户是什么等级,本质上都是在算“最终价格是多少”,只是算法不同。没有策略模式之前,算法写死在 if-else 里;有了策略模式,每个算法变成一个类,调用方只需要告诉上下文“我要用哪个策略”,剩下的事交给具体的策略对象。
这里的关键是:判断条件并没有消失,它从原来的 if (level == 2) 变成了一次查表动作。比如用一个 Map 把用户等级和策略对象关联起来,用 level 直接取,取不到就走默认策略。这样代码里就看不到像山一样的分支了,而且每个策略类都是独立文件,测试也好写。
2.2 支付折扣场景从 if-else 到策略
我这里写一个最简单的重构过程,方便你对照。先定义一个策略接口:
java复制public interface DiscountStrategy {
BigDecimal apply(BigDecimal price);
}
然后给每个用户等级一个实现类:
java复制public class GoldDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal apply(BigDecimal price) {
return price.multiply(new BigDecimal("0.85"));
}
}
public class SilverDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal apply(BigDecimal price) {
return price.multiply(new BigDecimal("0.95"));
}
}
接下来用一个注册表把条件映射到策略实例:
java复制public class DiscountStrategyFactory {
private static final Map<Integer, DiscountStrategy> STRATEGIES = new HashMap<>();
static {
STRATEGIES.put(3, new GoldDiscountStrategy());
STRATEGIES.put(2, new SilverDiscountStrategy());
}
public static DiscountStrategy getStrategy(Integer userLevel) {
return STRATEGIES.getOrDefault(userLevel, new NormalDiscountStrategy());
}
}
调用方就变成这样:
java复制public BigDecimal calcDiscount(Integer userLevel, BigDecimal price) {
DiscountStrategy strategy = DiscountStrategyFactory.getStrategy(userLevel);
return strategy.apply(price);
}
你觉得原本的 if-else 只有几行,重构之后反而多了五个类,是不是过度设计了?我一开始也这么想。但过了一个月,产品说要加一个“企业客户等级”,我只新增了一个 EnterpriseDiscountStrategy 类,再往 Map 里加一行,完全没碰其他策略类。如果是原来的 if-else,我至少要在那个六百行的方法里再插入一段分支,然后继续祈祷别改出问题。这就是策略模式带来的实际收益:新增功能时不需要改动已验证的代码,符合开闭原则。
2.3 策略模式最容易踩的坑
策略模式最大的坑不是难懂,而是“无脑滥用”。我发现很多人在学了策略模式之后,恨不得把所有代码都套上策略,最后接口十几个,实现类几十个,每个类里只有两三行,找个逻辑要跳来跳去,比 if-else 还难读。判断标准很简单:如果这个分支在可预见的未来根本不会变,就别为它建策略类。比如 if (age < 18) 这种硬性规则,写 if-else 反而更清晰。
另一个坑是策略对象内部有状态。我见过有人把临时计算结果存在策略类字段里,多线程环境下直接出事。策略对象最好设计成无状态的,需要参数就通过方法传进去,别把中间状态挂在成员变量上。如果是 Spring 项目,直接用单例 Bean,别手动 new 一堆实例。
3. 工厂模式:把创建对象的判断收口
3.1 不要一听工厂就堆抽象
工厂模式和策略模式经常被放在一起说,因为它们的代码结构长得很像,都是通过一个 Map 或者 switch 来选择一个实现。但它们的职责完全不同:策略模式关注“算法怎么执行”,工厂模式关注“对象怎么创建”。换句话说,策略模式帮你去掉的是业务计算里的 if-else,工厂模式帮你去掉的是“该 new 谁”的 if-else。
很多人一想到工厂,就条件反射地搬出简单工厂、工厂方法、抽象工厂三个概念的异同,然后纠结得睡不着。但在实际业务开发里,百分之八十的场景用简单工厂就够了。你要做的只是把“根据条件创建对象”的逻辑集中到一个类里,别让它散落在业务代码各处。
3.2 用简单工厂干掉创建型 if-else
举一个我实际做过的例子。系统要从不同渠道导入订单文件,有 CSV、Excel、XML 三种格式,每种格式的解析逻辑完全不一样。原来的代码写在一个方法里:
java复制if (fileType.equals("csv")) {
return new CsvOrderParser();
} else if (fileType.equals("excel")) {
return new ExcelOrderParser();
} else if (fileType.equals("xml")) {
return new XmlOrderParser();
} else {
throw new UnsupportedFileTypeException(fileType);
}
单看这段代码,问题不大。但问题是,这几个创建逻辑散落在多个业务方法里,每次导入文件就要抄一遍。后来我把创建过程统一收进工厂:
java复制public class OrderParserFactory {
private static final Map<String, OrderParser> PARSERS = new HashMap<>();
static {
PARSERS.put("csv", new CsvOrderParser());
PARSERS.put("excel", new ExcelOrderParser());
PARSERS.put("xml", new XmlOrderParser());
}
public static OrderParser getParser(String fileType) {
OrderParser parser = PARSERS.get(fileType.toLowerCase());
if (parser == null) {
throw new UnsupportedFileTypeException(fileType);
}
return parser;
}
}
业务方法里的 if-else 直接消失,只剩一行调用。后续新增“json”格式,只需要加一个 JsonOrderParser 类,往 Map 里注册一行,所有使用工厂的地方都自动支持。这个收益比策略模式还明显,因为它把重复代码给收口了。
3.3 工厂与策略搭配使用时怎么设计
实际项目中,工厂模式和策略模式经常成对出现。比如前面折扣场景,DiscountStrategyFactory 既是工厂的职责,也负责注册策略。我见过的优秀写法是:策略类用 Spring 的 @Component 注解注册成 Bean,工厂通过 ApplicationContext 自动收集同一接口的所有实现,放进 Map。这样连 Map 的 put 都可以省掉,新增策略类时连工厂都不用改:
java复制@Component
public class DiscountStrategyFactory {
private final Map<String, DiscountStrategy> strategyMap;
public DiscountStrategyFactory(List<DiscountStrategy> strategies) {
strategyMap = strategies.stream()
.collect(Collectors.toMap(
s -> s.getClass().getSimpleName(), Function.identity()));
}
public DiscountStrategy getStrategy(String beanName) {
return strategyMap.get(beanName);
}
}
这里我建议不要在工厂里塞太复杂的逻辑。工厂的职责就是“给个条件,返回对象”,如果工厂内部还要做一堆数据清洗、转换,那它又会变成一个巨大的 if-else。把简单的判断留在工厂,把复杂的流程拆给策略或责任链。
4. 状态模式:让状态自己决定行为
4.1 状态模式解决什么问题
状态模式是四种模式里最容易和策略模式混淆的。两者结构确实很像,都是接口加多个实现,但语义不同:策略模式解决“同一动作有不同算法”,状态模式解决“同一动作在不同状态下的行为不一样,而且状态会迁移”。
最典型的场景是订单状态机。一个订单可能有待支付、已支付、已发货、已完成、已取消等状态,每个状态能执行的动作不一样,而且一个动作还会导致状态变化。如果全用 if-else 写,你通常会得到两层嵌套:外层判断当前状态,内层判断动作,最后再判断下一个状态。这种代码不仅长得丑,而且特别容易漏掉某个状态转换。
状态模式的做法是:把每个状态封装成一个类,状态类自己负责“当前状态下,某个动作该怎么处理,并且执行后应该迁移到哪个状态”。这样你就把一张复杂的状态转换表,拆成了每个状态类自己的逻辑,每个状态类只需要关心自己范围内的转换,不需要关心其他状态。
4.2 订单状态流转重构示例
用一个简化版的待支付订单举例。状态接口可以这样定义:
java复制public interface OrderState {
void pay(OrderContext context);
void cancel(OrderContext context);
}
待支付状态下,可以付款,也可以取消:
java复制public class PendingPaymentState implements OrderState {
@Override
public void pay(OrderContext context) {
// 执行支付逻辑
System.out.println("订单已支付");
context.setState(new PaidState());
}
@Override
public void cancel(OrderContext context) {
// 执行取消逻辑
System.out.println("订单已取消");
context.setState(new CancelledState());
}
}
已支付状态下,支付动作就变成非法操作,只能发货:
java复制public class PaidState implements OrderState {
@Override
public void pay(OrderContext context) {
throw new IllegalStateException("已支付订单不能重复支付");
}
@Override
public void cancel(OrderContext context) {
throw new IllegalStateException("已支付订单不能直接取消");
}
}
业务调用方根本不用关心订单当前是什么状态,直接调用:
java复制context.getState().pay(context);
如果需要判断能否执行某个动作,也可以在状态类里加校验方法。有人说这不是把 if-else 换成了一堆类吗?确实是,但这些类的边界非常清晰:每个状态一个类,每个动作一个方法,新增状态不需要去改已有状态类,只需要加一个新类。这比在 if-else 里一层层找“当前状态+动作”要稳得多。
4.3 状态模式别硬用,先分清条件分支还是状态流转
我见过有人在只有两三个状态、状态之间几乎没有迁移逻辑的场景里强行套状态模式,结果代码膨胀了两倍,别人根本看不懂。其实判断标准很简单:如果业务里存在“当前状态 + 触发事件 -> 下一状态”这种模型,而且状态数量会持续增长,那么状态模式很有价值;如果只是根据某个字段做行为区分,没有真正的状态流转,那用策略模式就够了。
还有一点要注意,状态模式里的状态对象和策略对象一样,最好是无状态的。状态类可以做成单例,每次状态变更只是把 context 里的状态引用换成另一个单例对象,而不是 new 一个全新对象。另外,如果状态转换逻辑比较复杂,比如需要校验历史记录、调用外部系统,建议把校验逻辑放进状态类的方法里,或者单独抽出校验器,不要让状态类变成无所不包的大杂烩。
5. 责任链模式:把一串判断拆成链
5.1 典型场景:多级校验、审批流、风控
责任链模式是我觉得“最容易被低估”的一个。它适合处理那种需要依次判定、任一环节都能终止业务流程的场景。比如表单校验、多级审批、风控初审、操作审计,都是典型的责任链场景。
我举个最常见的例子。下单接口要做一系列校验:登录状态是否有效、库存是否充足、用户是否被限制下单、金额是否超过限额。如果用 if-else 写,代码会变成一个很长的校验列表,而且校验顺序是固定的,想调整顺序就要动主逻辑。责任链模式把这些校验器串成一条链,请求从头走到尾,哪个节点不通过就终止,全部通过才放行。
5.2 责任链的两种实现方式
第一种是链表式,也就是经典的责任链。每个处理器持有下一个处理器的引用:
java复制public abstract class AbstractHandler {
protected AbstractHandler next;
public void setNext(AbstractHandler next) {
this.next = next;
}
public abstract boolean handle(Request request);
protected boolean doNext(Request request) {
if (next != null) {
return next.handle(request);
}
return true;
}
}
具体节点只管自己的校验,通过就调用 doNext 往后传,不通过就返回 false。主流程不用关心有多少个校验器,只需要把链拼出来:
java复制AbstractHandler authHandler = new AuthHandler();
AbstractHandler stockHandler = new StockHandler();
AbstractHandler riskHandler = new RiskHandler();
authHandler.setNext(stockHandler);
stockHandler.setNext(riskHandler);
return authHandler.handle(request);
第二种是列表式,用 List 按顺序保存所有处理器,然后循环调用。这种方式更容易理解和维护,也更适合处理“所有节点都要执行一边”的场景,比如拦截器、过滤器。如果是审批流这种“只要有一个节点驳回就停止”,我推荐列表式加一个短路判断,效果一样,代码还好读。
5.3 责任链的注意事项
责任链最大的坑是容易写出“链式 if-else”。有些人在每个节点里复制一整段处理逻辑,导致责任链退化成披着模式外衣的 if-else。正确的做法是每个节点只做一件事,而且节点之间不要共享可变状态,请求对象尽量用不可变类,否则链条中途某个节点改了请求参数,后面的节点很难排查。
还有一个容易踩的坑是责任链没有终止条件。如果某个节点忘了调用 doNext,或者循环式责任链在拼接时形成了环,会出现请求在链里打转,最终栈溢出。建议在设计时就约定:每个节点要么返回结果终止链,要么显式调用下一个节点,不允许静默结束。
6. 四种模式到底怎么选
6.1 选型对照表
很多同学学了四种模式后,最头疼的问题不是看不懂,而是不知道在项目里用哪个。这里给一张选型对照表,方便你遇到具体场景时快速定位:
| 场景特征 | 推荐模式 | 判断关键词 |
|---|---|---|
| 同一动作有不同算法/规则 | 策略模式 | 折扣、计税、运费、消息发送 |
| 根据条件创建不同类型对象 | 工厂模式 | 解析器、适配器、转换器 |
| 行为随状态改变,且状态会迁移 | 状态模式 | 订单状态、审批状态、流程状态 |
| 多个环节依次校验,任一环节可终止 | 责任链模式 | 校验链、审批链、风控链 |
实际上,很多复杂业务是多种模式组合使用的。比如订单提交接口:先用责任链做前置校验,校验通过后用工厂创建对应的订单处理器,处理器内部再用策略模式计算价格,最后状态模式管理后续流转。模式之间并不冲突,它们的组合才是真正的工程能力。
6.2 设计模式不是越多越好
我必须反复强调这一点:设计模式是用来消除重复、封装变化的,不是用来炫技的。如果你重构之后的代码,类数量翻了一倍,找逻辑的时候反而要跳来跳去,那么这个重构大概率是失败的。
我给自己定了一个标准:如果某个 if-else 分支在未来三个月内没有出现新增或修改的需求,那这个 if-else 就是合理的,不需要用模式去替换。设计模式的投入应该在“变化”发生之前做,但别在“永远不会变化”的代码上做无用功。重构的时机很重要,最好的时机是需求已经出现变化苗头,但代码还没烂到不能看的时候。
6.3 结合 Spring 的工程化实践
如果你用的是 Spring Boot,不要手动 new 一堆策略对象,直接把它交给容器管理。常见的做法是给每个策略类加 @Component,然后通过 List<T> 或者 Map<String, T> 注入,让框架自动收集同一个接口的所有实现。我在前面工厂模式的例子里已经演示了这种写法,这种方式的好处是新增策略类基本不需要改旧代码,只要实现接口并注册成 Bean 就会被自动发现。
另一个经验是给策略对象一个统一的 type() 或 getType() 方法,返回这个策略对应的业务类型,然后工厂用这个 type 作为 Map 的 key。这样比用类名做 key 更稳,因为类名重构时容易变,而业务 type 是稳定的。初始化的时候用 list.stream().collect(toMap(Strategy::type, Function.identity())) 生成 Map,就会有一个很好的扩展点。
7. 常见问题与排查技巧实录
7.1 策略或工厂取不到对象,NPE 满天飞
这是我自己踩过的坑。用 Map 存储策略后,调用方传了一个未注册的类型,直接返回 null,后面所有的调用全炸。解决方法是工厂里统一用 getOrDefault 给默认实现,或者抛一个业务异常,并在异常信息里把当前支持的类型都列出来,方便排查。千万不要返回 null,一个 null 会让问题延迟到业务代码深处才暴露,排查成本特别高。
另外一个容易忽略的问题是 Map 的 key 大小写。文件类型、用户等级这类字符串,建议在注册和获取时都统一转成小写或者定义常量,否则线上莫名其妙地取不到对象,查半天才发现是大小写不一致。
7.2 状态模式类太多,维护反而吃力
有的同学把状态模式引入订单系统后,发现订单状态有十几个,每个状态还要处理四五个动作,类数量一下子爆炸。这时候可以合并状态类,把行为相似的几个状态共用一个类,用钩子方法区分。比如“待支付”和“支付中”在业务行为上差异不大,就没必要拆两个类。
如果状态流转复杂到已经需要画状态图,建议引入现成的状态机框架,比如 Spring StateMachine,而不要自己手写状态类。框架的好处是状态转换规则集中配置,可视化,排错容易,坏处是引入成本高,代码有学习曲线。我的建议是:状态少于五个,手写状态模式;状态很多或者流转很乱,直接用框架。
7.3 怎么在团队里推动重构 if-else
重构 if-else 不只是技术问题,更是协作问题。我见过很多重构失败的情况,不是模式选错了,而是同事不认可。我的经验是不要试图一次性推翻所有旧代码,而是挑一个业务价值高、痛点明显的模块,比如支付、订单、审核,先做一个示范性重构。重构完把改动前后的代码对比发出来,讲清楚“原来改一个需求要动多少行,现在只要加一个类”。用事实说话,比讲十遍设计原则都管用。
另外,重构时一定要有一套可靠的测试兜底。先把原来的 if-else 行为用单元测试锁住,再动手重构,改完之后测试全绿,才能证明行为没有变化。这也是我在实际项目里最重视的一点:设计模式本身不能保证正确性,测试才能。
如果你也正准备动手清理项目里那片 if-else,我的建议是从最小、最痛的一处开始,先用策略模式把最大的分支拆掉,不要试图一天全部重构。重构的价值不是把代码变短,而是让改代码的人不用再靠翻旧账来猜逻辑。等你真正经历过一次“新需求只新增一个类”的快感,就很难再回去了。
