前阵子做代码评审,遇到一个让我印象特别深刻的类:订单价格计算方法,一个方法里塞了十多个 if-else,从会员等级判断到优惠券叠加,从新人专享到节假日折扣,层层嵌套,最外层还包着一个 for 循环,计算一批订单的总价。读代码的人得顺着每一层 if 的 true 和 false 两条路分别往下追,追到第五层基本就忘了前面到底在判断什么条件。后来新增一个"预售尾款立减"的需求,团队里两个人改了同一个方法,一个人改在 284 行,另一个人改在 351 行,合并时冲突叠着冲突,最后 Code Review 时发现两个分支互相把对方的判断条件覆盖了一半。我说这东西得重构,同事说"重构 = 引入设计模式 = 更复杂",我听完就知道,我们对设计模式的认知可能存在一个很大的偏差。
先说我的结论:设计模式不是用来炫技的,也不是为了消灭 if-else 本身。if-else 的问题从来不在"用了判断",而在"每个新需求都在改同一个方法、同一个分支结构",时间一长,旧分支被新分支牵动,新分支被旧逻辑干扰,改哪都像踩地雷。真正干净的做法是让"变化的业务规则"各自独立,让代码的骨架稳定下来。这篇文章我会直接用四个实战场景,把策略模式、工厂加策略组合、责任链模式、状态模式怎么替换 if-else 讲明白,每一段都附上重构前后的代码对比,把踩过的坑也一并列出来。如果你是后端、客户端同学,或者正在准备设计模式相关的内容,看完应该能直接在自己的项目里动手改造。
1. if-else 真正的问题:不是可读性差,而是每次需求变化都要跟着变
很多人把 if-else 当成过街老鼠,其实这是一个误伤。几层 if-else 的判断逻辑,自己写的当下看肯定无比清晰,就算换成别人来读,只要分支数量少、条件简单,也完全能顺下来。if-else 真正的杀伤力,藏在需求迭代的节奏里。
1.1 一个折扣需求的演变史
假设现在要写一个订单价格计算模块,第一版需求特别简单:普通用户不打折,会员用户打 95 折。你写出来的代码大概是这样:
java复制public BigDecimal calculatePrice(Order order) {
BigDecimal total = order.getGoodsAmount();
if (order.getUser().isMember()) {
total = total.multiply(new BigDecimal("0.95"));
}
return total;
}
这个 if 写得没有任何问题,清晰、简短、可读性极高。第二个月产品说加一个"新人专享 88 折",你开始追加分支:
java复制public BigDecimal calculatePrice(Order order) {
BigDecimal total = order.getGoodsAmount();
User user = order.getUser();
if (user.isMember()) {
total = total.multiply(new BigDecimal("0.95"));
}
if (user.isNewUser()) {
total = total.multiply(new BigDecimal("0.88"));
}
return total;
}
看起来还能接受吧?毕竟也没有嵌套,只是两个平级 if。但很快产品又来了第三版需求:"双十一大促,全场满 300 减 40;会员在满减基础上再打 95 折;新人不参与满减,但享受折上 88 折;礼盒类商品不参与任何促销,走固定价。"
这个时候你再看代码,if 和 else 开始互相嵌套、互相排除,一个用户可能同时命中多个条件,还要考虑条件的优先级。为了表达这套规则,你只能不断往上叠判断,每叠一层,就得重新梳理前面所有分支的顺序是否还正确。等这个类长到四百行、五百行的时候,代码已经不是给人读的了,是给调试器一遍遍捋的。
1.2 判断与规则混杂,改一个分支容易带崩一片
我仔细观察过一堆被 if-else 堆坏的代码,发现它们的共同点是:把一个"规则"和一堆"条件判断"混在同一个方法体里。判断用户是不是会员,是条件;会员打多少折,是规则;判断商品是不是礼盒,是条件;礼盒走固定价,是规则。如果代码把条件和规则完全搅在一起,新的需求进来,你就不只是"加规则",而是在“改条件”,改条件就会影响旧规则,影响旧规则就会产生线上 bug。
设计模式在这里的作用,是把"条件判断"和"业务规则"拆成两个维度:规则各自封装,条件只用来选择一个规则组。这样一来,新增需求时新增的是一个独立的规则实现,而不是在老方法里找位置插入一个新的 if 分支。
判断一个 if-else 是否值得处理,我有一个比较实用的标准:看这个 if 块过去的修改频率。如果一个 if 分支在过去三个月被改了三次以上,说明它对应的业务逻辑处于高频变化中,留在这里就是一颗定时炸弹;如果某个 if 从上线到现在三年没人动它,那它反而可以继续留着,不要为了重构而重构。我见过不少团队把稳定代码强行套上设计模式,最后出现的问题是:类数量暴涨,可读性不升反降,老成员离职以后,新来的同事看着二十个策略类根本不知道哪个才是真正被使用的那个。所以,下面要讲的四种模式,全部针对的是"高频变化、分支复杂、判断交叉"的典型场景,如果不符合这些特征,完全可以按兵不动。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 策略模式:把"各分支该干什么"提取成独立策略
策略模式是干掉 if-else 最基础、也最常用的一招。它的核心思想特别简单:把每个分支里的具体执行逻辑抽取成独立的类,让调用方在运行期选择用哪个类。
2.1 重构前:结算方法里的 if-else
我们看一个最常见的结算场景,不同的订单类型走不同的计价逻辑:
java复制public BigDecimal settle(Order order) {
OrderType type = order.getType();
if (type == OrderType.NORMAL) {
return normalSettle(order);
} else if (type == OrderType.SECKILL) {
return seckillSettle(order);
} else if (type == OrderType.GROUPON) {
return grouponSettle(order);
} else if (type == OrderType.CREDIT) {
return creditSettle(order);
}
throw new UnsupportedOperationException("unsupported type");
}
这个代码的坏味道不在那几行 if,而在于每次新增一种订单类型,你都得打开这个 settle 方法,在最后面再补一个 else if。多个订单类型对应的结算逻辑互相之间其实毫无关系,但是被写在同一个类里以后,它们在物理层面就被绑定到了一起。一个人改秒杀结算逻辑,他要面对的却是整个文件里四种订单类型的代码。
2.2 重构后:策略接口与实现类
用策略模式重构之后,结构变这样:
java复制public interface SettleStrategy {
OrderType supportType();
BigDecimal settle(Order order);
}
然后每种订单类型对应独立的策略实现:
java复制@Component
public class NormalSettleStrategy implements SettleStrategy {
@Override
public OrderType supportType() {
return OrderType.NORMAL;
}
@Override
public BigDecimal settle(Order order) {
// 普通订单结算逻辑
return order.getGoodsAmount();
}
}
