作为后端开发,我这两年最怕听到的一句话就是:这个需求很简单,加个if-else判断一下就行了。等加到二三十个分支的时候,一个方法几百行,看代码全靠滚轮,测试用例写到怀疑人生。后来我算是想明白了,if-else本身没有错,错的是我们把会变化的东西全部焊死在了判断逻辑里。这篇文章我就想聊聊我用得最顺手的四种设计模式——策略、工厂、状态、责任链,它们是怎么把我从if-else泥潭里捞出来的。
该踩的坑我一个都没少踩,所以这篇文章不只是讲概念,更多是讲我在真实项目里怎么决策、怎么落地、怎么避免“为了模式而模式”。如果你也在为一个越来越膨胀的业务方法头疼,这篇文章应该能给你一些思路。
1. 先看清敌人到底是谁:if-else的4个底层痛点
1.1 你讨厌的不是if-else,而是变化
先说个反直觉的事情:if-else本身并不坏。它是最直白的条件控制语句,CPU喜欢,人脑也容易理解。真正让人头疼的,是那些“今天加一种类型、明天加一个渠道、后天加一个状态”的业务代码。
比如我见过一个相当经典的坏味道:
java复制public String process(String payType) {
if ("WECHAT".equals(payType)) {
return doWechatPay();
} else if ("ALIPAY".equals(payType)) {
return doAlipayPay();
} else if ("CARD".equals(payType)) {
return doCardPay();
} else if ("BALANCE".equals(payType)) {
return doBalancePay();
}
return "UNSUPPORTED";
}
看着好像还不算糟?问题是,每个分支里面可能还有几十行甚至上百行的业务逻辑,包括查表、调外部接口、写流水、发消息。然后业务方又来了:新增一个“数字人民币”支付方式。你得先从接口层找参数类型,再一头扎进这个方法的中间,找到合适的位置,小心翼翼地把新分支插进去,还要祈祷没碰坏别人。这还只是单个方法来来回回改,如果同样的判断散落在好几个地方,那就更酸爽了。
所以,要消灭的不是if-else这个语法,而是消灭“我们亲手埋进去的变化炸弹”。设计模式之所以能解决这个问题,核心思路只有一个:把那些可能会变的东西从判断逻辑里抽离出去,让代码对扩展开放、对修改关闭。
1.2 四个痛点拆开看
我把if-else代码的痛苦总结成四个层面,方便对照检查你自己项目里的代码:
- 可读性差:方法越来越长,阅读代码时必须同时维护一个“当前分支上下文”的脑内缓存。一旦超过一屏,记忆就开始失真。
- 维护成本高:同样的类型判断散落在多个方法里,改一个分支要全局搜索,很容易漏改某处。
- 测试爆炸:每增加一个分支,核心方法的测试用例数量就指数上涨。分支之间还可能产生组合关系,例如支付方式乘以订单状态,用例多得根本写不完。
- 违反开闭原则:新增需求必然要修改已有代码。改旧代码就意味着回归风险,说不定哪次上线就是因为“加了个else if”把别的分支带崩了。
当然,这四个痛点不是每条if-else都触发。什么时候该重构、什么时候保留,我后面会用一整节来讲,先不急着动手。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 策略模式:把“选算法”变成“换插件”
2.1 核心思路:分离“做什么”和“怎么做”
策略模式,我愿称之为“if-else消除领域的第一功臣”。它解决的问题是:同一件事(比如支付、计费、发送消息)有多个算法/实现,调用方希望在运行时选择用哪一个。
思路非常简单:定义一个统一的策略接口,每种实现写一个类,然后让上下文持有接口引用。调用方只面向接口编程,不再关心具体用了哪个实现。
2.2 用支付场景落地一遍
从上面的支付代码出发,第一步,先定义一个策略接口:
java复制public interface PayStrategy {
String payType();
void pay(BigDecimal amount);
}
第二步,把每个if分支提取成独立的策略实现类:
java复制@Component
public class WechatPayStrategy implements PayStrategy {
@Override
public String payType() {
return "WECHAT";
}
@Override
public void pay(BigDecimal amount) {
// 微信支付的具体逻辑
}
}
@Component
public class AlipayPayStrategy implements PayStrategy {
@Override
public String payType() {
return "ALIPAY";
}
@Override
public void pay(BigDecimal amount) {
// 支付宝支付的具体逻辑
}
}
第三步,在Spring环境中,把所有的策略实现注入到一个Map里:
java复制@Service
public class PayService {
private final Map<String, PayStrategy> strategyMap;
public PayService(Map<String, PayStrategy> strategyMap) {
this.strategyMap = strategyMap;
}
public void pay(String payType, BigDecimal amount) {
PayStrategy strategy = strategyMap.get(payType);
if (strategy == null) {
throw new IllegalArgumentException("Unsupported pay type: " + payType);
}
strategy.pay(amount);
}
}
Spring会把所有PayStrategy的实现类收集成一个Map,key是Bean名字。不过我不推荐直接依赖Bean名字,所以通常会在实现类里再加一个payType()方法,然后利用这个返回值显式注册。最简洁的做法是直接在Controller或者配置类里初始化这个Map,和Spring解耦。
2.3 为什么这版代码“优雅”
重构完,最大的变化是:新增支付方式的时候,你不需要再碰PayService了。加一个新类,实现PayStrategy接口,在Map里注册一下,完事。你不是在修改旧代码,而是在添加新代码,这就是开闭原则。
另外,每个策略类可以单独写单测,单独mock外部依赖,再也不用为了测某个分支把前置条件堆半天。代码结构还带来了一个隐性好处:新人接手时,看一眼接口和实现类列表,就能数清楚一共有多少种支付方式,比在一大坨if-else里捞分支要清楚得多。
2.4 注意:策略模式不是万能药
策略模式最适合的,是“平级、互斥、按类型选择实现”的分支。如果分支之间不是平级关系,而是有先后顺序、有状态流程,那该考虑责任链或者状态模式。
我自己踩过的一个坑是:把策略实现类搞得太细碎。如果一个策略实现里还有超过三个if-else,就要小心,这可能是策略内部又出现了新的变化维度。这时候该进一步分析,而不是硬塞进策略模式里。策略模式解决的是“选哪个”,解决不了“每个策略内部又臭又长”的问题。
另外,如果策略数量很少而且永远不会变,比如就两三种,那用if-else其实也行。策略模式的价值必须在“频繁扩展”的前提下才能体现出来。
3. 工厂模式:把“造对象”变成“查字典/注册”
3.1 核心思路:把创建逻辑收拢
工厂模式跟策略模式不一样。策略模式解决的是“怎么执行”,工厂模式解决的是“怎么创建”。很多业务代码里,我们会在if-else里面做new操作:
java复制if ("SMS".equals(channelType)) {
return new SmsChannel();
} else if ("EMAIL".equals(channelType)) {
return new EmailChannel();
}
这就是典型的创建逻辑散落各处。如果创建过程只有一行new,问题还不大。但现实中一个对象的构造往往伴随着配置加载、依赖组装、参数校验,这些逻辑散落着写,就是灾难。
工厂模式的核心,是把“创建对象”的责任收敛到一个地方。用户传一个类型参数进来,工厂负责判断并返回正确的对象,调用方不需要知道对象是怎么组装的。
3.2 用“通知渠道工厂”演示标准做法
假设我们的业务有短信、邮件、App推送三种通知渠道。定义好渠道接口和实现类之后,工厂可以这么写:
java复制public class NotifyChannelFactory {
private static final Map<String, Supplier<NotifyChannel>> CHANNEL_MAP = new HashMap<>();
static {
CHANNEL_MAP.put("SMS", SmsChannel::new);
CHANNEL_MAP.put("EMAIL", EmailChannel::new);
CHANNEL_MAP.put("PUSH", PushChannel::new);
}
public static NotifyChannel getChannel(String type) {
Supplier<NotifyChannel> supplier = CHANNEL_MAP.get(type);
if (supplier == null) {
throw new IllegalArgumentException("Unknown channel: " + type);
}
return supplier.get();
}
}
用Supplier这个函数式接口,是在Java 8之后比较顺手的写法。如果创建逻辑不只是一个new,比如还需要从配置中心拉配置,那可以换成Function<String, NotifyChannel>,或者干脆用一个专门的创建方法。
使用的时候,客户端代码变成一行:
java复制NotifyChannel channel = NotifyChannelFactory.getChannel(channelType);
channel.send(message);
if-else彻底消失了。新增一种渠道,只需要加一个实现类,并且在工厂的Map里加一行。这样把变化点收束到了工厂的注册表里,而不是散落在业务代码中。
3.3 和Spring容器的结合
其实Spring的IoC容器本身就是一个超大型工厂。所以我们经常可以不做自己的工厂,直接让Spring来管理:
java复制@Service
public class NotifyService {
private final Map<String, NotifyChannel> channelMap;
public NotifyService(Map<String, NotifyChannel> channelMap) {
this.channelMap = channelMap;
}
public void send(String channelType, String message) {
NotifyChannel channel = channelMap.get(channelType);
if (channel == null) {
throw new IllegalArgumentException("Unknown channel: " + channelType);
}
channel.send(message);
}
}
所有实现NotifyChannel接口的Bean会被自动收进Map,key是Bean名称。只要你在每个实现类上指定了和渠道类型一致的Bean name,就能直接用。这个方案的优点是连工厂类都省了,整个应用里没有一个显式的if-else。
不过要注意,Spring的Bean名称默认是类名首字母小写,如果你希望key是“SMS”这种大写格式,要显式用@Component("SMS")指定。
3.4 工厂模式别滥用
工厂模式是“创建对象”场景下的利器,但对那种“new出来就用”的简单对象,强行套工厂就是浪费。
举个例子,你有一个User类,构造器就一个new User(name, age),没有复杂依赖。你在代码里new一下就完事了,非要搞一个UserFactory,那纯属画蛇添足。工厂模式的价值体现在:创建过程复杂、类型注册动态、调用方不希望感知具体实现类。没有这些诉求,不要硬上。
实际工作里,如果遇到“工厂类本身也在膨胀”的情况,比如工厂里面的Map越来越大、加类型还要改工厂代码,说明工厂也开始违反开闭原则了。这时候可以考虑用注解+反射扫描,或者把注册表外置到配置文件,让类型注册变成配置驱动,彻底不用改工厂代码。
4. 状态模式:把“状态判断”变成“状态对象自驱”
4.1 核心思路:让状态对象自己决定下一步
如果说策略模式是if-else平级分支里的急救包,那状态模式就是状态机场景里的手术刀。订单系统、审批流、工单流转,这类业务的核心就是状态迁移。
不用状态模式的代码长什么样?我一个订单状态判断写出来可以让你感受一下:
java复制public void handle(Order order, Action action) {
if (order.getStatus() == OrderStatus.PENDING_PAYMENT) {
if (action == Action.PAY) {
// 执行支付逻辑
} else if (action == Action.CANCEL) {
// 取消订单
}
} else if (order.getStatus() == OrderStatus.PAID) {
if (action == Action.REFUND) {
// 退款
} else if (action == Action.DELIVER) {
// 发货
}
} else if (order.getStatus() == OrderStatus.SHIPPED) {
// ...
}
}
当状态有5个、动作有6个,这个矩阵就是5×6=30个分支。代码还没写完,脑子里已经烧起来了。
状态模式的处理手法是:把“当前状态允许做什么行为”以及“行为执行之后切换到哪个状态”这两个信息,封装到状态对象内部。上下文对象只负责持有一个当前状态的引用,并把请求委托给这个状态对象。
4.2 用订单状态机手写一遍
首先,定义一个状态接口:
java复制public interface OrderState {
void pay(OrderContext context);
void cancel(OrderContext context);
void deliver(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());
}
@Override
public void deliver(OrderContext context) {
throw new IllegalStateException("待支付状态不能发货");
}
}
已支付状态类里同样实现这三种方法,支付时会告诉你“已经支付过了”,发货时才会真正启动物流逻辑,并且把状态流转到已发货。
上下文类也很简单:
java复制public class OrderContext {
private OrderState state;
public OrderContext() {
this.state = new PendingPaymentState();
}
public void setState(OrderState state) {
this.state = state;
}
public void pay() {
state.pay(this);
}
public void cancel() {
state.cancel(this);
}
public void deliver() {
state.deliver(this);
}
}
看到没有,客户端调用的时候就是一个极简接口。所有“当前状态能不能做这个动作”的判断,全部被压进了状态对象内部。
4.3 状态模式和策略模式到底怎么区分
这是我在给团队做分享时经常被问到的。从类图上来看,策略模式和状态模式长得几乎一模一样,都是一个接口加多个实现类,加上一个上下文。
两者的核心区别在意图:策略模式侧重“算法互换”,上下文本身的状态和策略选择无关,你只是想让一个行为在运行时可以替换;状态模式侧重“状态流转”,每个状态对象都明确知道“下一个状态是谁”,上下文在调用过程中会被动地切换状态。
简单来说,策略模式是“选择”,状态模式是“迁移”。用策略模式改的代码,你调用完pay()后,策略还是那个策略;用状态模式改的代码,调用完pay()后,状态已经变了,下一次行为由新的状态对象来定义。
4.4 什么时候别用状态模式
状态模式也不是没有代价。它把判断逻辑分散到了多个状态类里,类数量会明显增加。如果状态只有两三个,动作也少,那用switch矩阵反而更直观。我一般在状态超过四个、状态动作组合超过十种之后才考虑状态模式。
另外一个忠告:如果状态机比较复杂,状态迁移规则变化频繁,不要用Java类来硬编码状态流转。把状态迁移表放到数据库或配置中心里,由状态机引擎来驱动,这样业务人员调整状态机规则的时候,根本不需要发版本。这一点在订单、审批流这类业务里格外重要。
5. 责任链模式:把“层层if嵌套”变成“流水线传单”
5.1 核心思路:每个处理器只处理自己关心的那一段
责任链模式是我在处理“需要经过多道关卡”的业务时最喜欢用的。它把原本层层嵌套的if条件拆成一条链,请求在链上逐个节点传递,每个节点自己决定是处理、拦截,还是丢给下一个节点。
典型场景包括:参数校验、权限校验、敏感词过滤、审批流程、中间件管道。比如一个下单请求,要经过风控校验、库存校验、价格校验等多个关卡。如果用if-else嵌套,代码是长这样的:
java复制public void createOrder(OrderRequest request) {
if (checkRisk(request)) {
if (checkStock(request)) {
if (checkPrice(request)) {
// 创建订单
} else {
throw new BizException("价格校验失败");
}
} else {
throw new BizException("库存不足");
}
} else {
throw new BizException("风控拦截");
}
}
这种嵌套代码一多,缩进就是灾难。你以为是金字塔,其实是洋葱,一层套一层。
5.2 用审批链路示例落地
定义抽象处理者:
java复制public abstract class ApprovalHandler {
protected ApprovalHandler next;
public void setNext(ApprovalHandler next) {
this.next = next;
}
public abstract void handle(ApprovalRequest request);
}
写三个具体的处理器:
java复制public class DepartmentLeaderHandler extends ApprovalHandler {
@Override
public void handle(ApprovalRequest request) {
if (request.getAmount().compareTo(new BigDecimal("5000")) > 0) {
throw new BizException("超过部门主管审批额度");
}
System.out.println("部门主管审批通过");
if (next != null) {
next.handle(request);
}
}
}
public class FinanceHandler extends ApprovalHandler {
@Override
public void handle(ApprovalRequest request) {
// 财务审核逻辑
System.out.println("财务审批通过");
if (next != null) {
next.handle(request);
}
}
}
客户端这边,把链构建好,然后从头节点发起请求:
java复制ApprovalHandler departmentLeader = new DepartmentLeaderHandler();
ApprovalHandler finance = new FinanceHandler();
departmentLeader.setNext(finance);
departmentLeader.handle(new ApprovalRequest(new BigDecimal("3000")));
这样,请求的完整处理路径就清楚了。每个节点只负责自己的事,不再关心前一道和后一道具体是谁,只是把请求往后传。
5.3 进阶玩法:让Spring接管链路组装
手写链表在项目里有一个明显的缺点:链的组装逻辑散落在客户端,如果新增节点,很容易漏接。我通常会配合Spring用注解来管理顺序。
定义一个注解,标注处理器顺序:
java复制@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlerOrder {
int value();
}
然后每个处理类上标顺序,用一个配置类统一收集并排序。
java复制@Component
public class ApprovalChainConfig {
private final List<ApprovalHandler> handlers;
public ApprovalChainConfig(List<ApprovalHandler> handlers) {
this.handlers = handlers.stream()
.sorted(Comparator.comparingInt(h -> h.getClass().getAnnotation(HandlerOrder.class).value()))
.collect(Collectors.toList());
}
public ApprovalHandler buildChain() {
for (int i = 0; i < handlers.size() - 1; i++) {
handlers.get(i).setNext(handlers.get(i + 1));
}
return handlers.get(0);
}
}
这样做的好处是:新增审批节点时,业务代码一行都不用改,只需要新增一个处理器类并标注顺序。以后想调整顺序,改注解的值就行了。
5.4 责任链的坑
责任链用不好,最大的问题是调试困难。一个请求在链上走了一圈,你很难追踪它到底在哪个节点卡住了。我的习惯是,每个处理器在入口和出口都打一条结构化日志,带上请求ID,这样排查问题的时候直接grep日志就能看到完整链路。
还有一个坑是共享状态。责任链上的处理器如果修改了同一个共享对象,后面节点的判断就可能被前面的影响,而且这种影响非常隐晦。我的建议是处理器之间尽量通过请求对象传递数据,不要使用静态变量或者外部可变状态。请求对象作为数据载体是OK的,但也不能被多个线程同时使用,要注意线程安全。
6. 模式之外:别把所有if-else都当成怪兽
6.1 有些if-else,就不该被干掉
前面用了大量篇幅讲怎么消灭if-else,但作为在工程一线摸爬滚打的人,我必须泼点冷水:不是所有if-else都需要设计模式。
什么情况下继续用if-else是完全正确的?首先是分支数量少且固定,比如判断性别、判断页面是PC端还是移动端,这类需求基本不会扩展,新写一个策略接口反而让代码变得绕。其次是原型验证阶段,目标是快速跑通业务,先写if-else把逻辑调对,之后再谈重构。还有一个场景是条件之间高度耦合,比如if (a && b || c),这种复合条件下的语义很难抽象成一个干净的接口,硬抽象只会得到牵强的代码。
我见过最离谱的过度设计:为了用一个策略模式,把两个永远不会扩展的分支包装成了六个类文件。代码不是越抽象越好,抽象是有成本的,类多了、调用链长了,都增加阅读负担。
6.2 重构前必问自己的一句话
在动手消灭if-else之前,我会问自己一个问题:将来这个分支条件真的可能会变吗?如果答案是“这个需求写死不会变”,那就别动;如果答案是“业务方说下个月会有新类型”,现在就值得重构。
设计模式的本质是封装变化点。模式本身不是目的,可维护才是目的。我见过太多人为了“显得高级”而用模式,结果代码结构是高级了,业务逻辑反而看不懂了。正确的做法是:先用最简单的代码把需求跑通,当发现第二个“新增类型就要改原方法”的信号出现时,再开始考虑模式化。
6.3 最近在AI Agent设计里看到的“主从模式”,和设计模式惊人地呼应
写这篇文章的时候,我刚好也在研究多Agent系统的设计。最新的多Agent设计里有个主从模式,主Agent负责拆解任务,子Agent负责执行具体子任务。很多设计者会把子Agent视为一种另类的工具调用——主Agent根据任务类型,从工具清单中选择对应的子Agent。这不就是策略模式/工厂模式在AI领域的一次换皮吗?
抽象出统一接口,按类型注册实现,运行时查找并调用,这套思路不管是在传统后端代码里,还是在AI Agent组件编排里,底层是相通的。设计模式之所以能跨越不同的技术领域反复出现,正是因为它沉淀的是对“变化点”和“扩展点”的识别经验。这也是为什么我建议所有写代码的人都要把设计模式吃透,实际收益远不止于消灭if-else。
6.4 80%的if-else,用Map加接口就能杀掉
说了四种设计模式,但很多读者应该已经发现了,我落地每一种模式时,最关键的其实是那个Map加接口的注册表结构。策略模式用Map存策略,工厂模式用Map存创建逻辑,Spring自动注入也是把Bean装到Map里。
所以在实际工作中,我不会一上来就摆出模式图。我的习惯是:先把if-else里的分支看一遍,如果它们能抽出共同接口,赶紧定义接口;如果类型是字符串或枚举,赶紧想想怎么用一个Map把类型映射到实现。这套组合拳能干掉80%的平级if-else,而且代码量也不多。教科书里的模式提供了思想,但落地时完全可以简化、灵活发挥。
7. 实战踩坑:重构if-else可能遇到的5个拦路虎
7.1 典型问题速查表
我在几个项目里做过if-else重构,也带团队做过Code Review,总结出下面这张高频问题表,帮你对号入座:
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 策略类爆炸,一个类型一个类 | 粒度拆得太细,策略内部还有相同逻辑 | 用函数式接口+Lambda缩小粒度,或对策略做分组 |
| 状态流转越改越乱 | 状态对象之间互相引用,形成了环 | 把状态迁移表抽到配置中心/数据库,用引擎驱动 |
| 责任链节点顺序混乱 | 链路拼接散落在调用方 | 用注解标记顺序,配合Spring容器统一构建 |
| 工厂类越来越臃肿 | 所有创建逻辑都堆在工厂里 | 用Bean注册表+配置驱动,让工厂瘦身 |
| 重构完成后原有逻辑出错 | 过多关注结构,忽略了边界条件 | 重构前先把旧逻辑的测试用例补齐,用行为验证兜底 |
7.2 从暴力改写到平稳落地的五步走
如果你已经决定要重构一段if-else,我建议按这个顺序来,别一上来就大动干戈。
第一步,找到if-else密度最高的类,用一个指标来衡量:分支超过十个,或者方法超过一百行,就可以列入候选名单。
第二步,梳理所有分支条件,画出概念模型。比如支付方式、订单状态、审批动作,把这些抽象成接口名和模型字段。
第三步,先定义接口和数据结构,不要动手写实现。这一步的核心是看清变化点和稳定点的边界。
第四步,一个分支一个分支地替换。每次只改一个分支,把它迁移到策略类或状态类中,然后跑一遍对应测试,确认行为不变再继续下一个分支。切忌同时迁移多个分支,出了错你根本不知道谁引入的。
第五步,用测试兜底。如果旧代码没有测试,先补测试再动手。别嫌麻烦,重构没有测试的保护,就像走钢丝不系安全带。
7.3 我亲手做过的一次“400行到80行”重构
有一次我们项目里的计费核心方法,因为计费规则不断叠加,膨胀到了400多行,if-else分支大概有40个左右,每次业务方提新规则,改这个方法的人都战战兢兢。我接手后没有直接上模式,而是先给这个方法补了30多个测试用例,把现有行为全部固话。
然后我按支付方式、计费周期、折扣类型三个维度拆出来三个策略接口,用Map做注册表。原来一个大方法被拆成了十几个策略类,每个类几十行,职责清晰。最终核心方法从400多行降到了80行,大部分逻辑变成了查表和委托。最关键的是,整个重构过程在测试的保护下,上线一次通过,没有出现线上问题。
那次经历让我确信,设计模式的威力不在类图的华丽,而在你终于把一个“别人不敢动”的方法,改成了“新人也敢改”的模块。
最后说点实在的
琢磨设计模式这十年,我最大的体会是:干掉if-else从来不是目的,让代码在面对变化时足够从容才是目的。策略、工厂、状态、责任链,是我日常工作里使用频率最高的四件武器,它们几乎覆盖了平级分支、对象创建、状态迁移、流程编排这四类让人头大的场景。
当然,工具永远是工具。真正值钱的,是你一眼就能识别出“这是个策略场景”还是“这是状态机场景”的直觉。这种直觉没有捷径,就是多看、多改、多踩坑。希望这篇文章能帮你在if-else的泥潭里,找到一个适合你的出口。
