策略模式和 if-else 相比有什么好处?
结论先行:策略模式的好处不在"代码更短",而在变化成本和可测试性:新增一种算法只加一个类(开闭原则),每个算法可独立单测,深层嵌套被摊平成注册表。代价是类变多、跳转变散。所以分支少且稳定就写 if-else,分支会持续增长才上策略。
1. if-else 的真实成本
| 症状 | 表现 | 后果 |
|---|---|---|
| 横向膨胀 | 每加一个渠道/规则就多一个 else if | 方法动辄两百行,改一次要通读全文 |
| 纵向嵌套 | 校验、权限、渠道交叉判断 | if 套四层,圈复杂度爆表 |
| 复用为零 | 同一段逻辑复制到三个方法 | 修一个 bug 要改三处 |
| 无法测试 | 想测某个分支必须先构造整套前置条件 | 单测覆盖率上不去 |
| 容易被改错 | 缺 break、条件写反、优先级错 | 线上事故高发区 |
2. 两种分发方式的本质差别
if-else:顺序判定,命中即止(O(n) 比较) 策略 + 注册表:一次查表(O(1))
amount ──► type == "A"? ──是──► 算法A amount ──► REG.get(type) ──► 算法
│否 ▲
▼ │
type == "B"? ──是──► 算法B REG: {"A":A, "B":B, "C":C}
│否 新增算法= put 一条,不改判定逻辑
▼
type == "C"? ──是──► 算法C
│否
▼
抛异常 / 默认
左边是"逻辑里编码分支",右边是"数据里描述分支"。后者把变化从代码搬到了配置。
3. if-else 版本
public long discount(String type, long amount, User user) {
if ("full_reduction".equals(type)) {
return amount >= 100_00 ? amount - 10_00 : amount;
} else if ("percent_off".equals(type)) {
return amount * 85 / 100;
} else if ("new_user".equals(type)) {
return user.isNew() ? amount - 20_00 : amount;
} else if ("vip".equals(type)) {
return user.isVip() ? (long) (amount * 0.8) : amount;
} else {
throw new IllegalArgumentException("未知优惠类型: " + type);
}
}
看着还行?再加上"叠加规则、互斥校验、按渠道限用、需要埋点统计每个策略的效果"之后,这个方法会变成三百行的怪物——每加一条规则都要动它,每次改动都可能影响其他分支。
4. 策略版本
interface Discount {
long apply(long amount, User user);
String type(); // 自我声明类型,注册表按它建索引
}
class PercentOff implements Discount {
public long apply(long amount, User user) { return amount * 85 / 100; }
public String type() { return "percent_off"; }
}
class VipOff implements Discount {
public long apply(long amount, User user) { return user.isVip() ? (long) (amount * 0.8) : amount; }
public String type() { return "vip"; }
}
class DiscountRegistry {
private static final Map<String, Discount> REG = new HashMap<>();
static void register(Discount d) { REG.put(d.type(), d); }
static Discount get(String type) {
Discount d = REG.get(type);
if (d == null) throw new IllegalArgumentException("未知优惠类型: " + type); // 兜底必须显式
return d;
}
}
// 新增一种"直播专享":只加一个类 + 一行 register,其余代码零改动
5. 客观对比
| 维度 | if-else | 策略模式 |
|---|---|---|
| 新增一种算法 | 改原方法(可能影响老分支) | 新增类 + 注册(不改老代码) |
| 可读性(分支 ≤3) | 更好,一眼看完 | 偏差,要跳文件 |
| 可读性(分支 ≥8) | 灾难 | 更好,每个类只关心自己 |
| 单测 | 难隔离,需造前置状态 | 每个策略独立测试 |
| 复用 | 靠复制粘贴 | 直接注入复用 |
| 性能 | 少量比较,可忽略 | 一次哈希查找,可忽略 |
| 类数量 | 0 新增 | N 个策略类 + 1 个注册表 |
| 调试 | 断点在同一方法,直观 | 需在多文件间跳转 |
6. 什么时候别改成策略
- 分支数量少且稳定:三五个固定类型,
switch比五个类更好维护。 - 分支逻辑只有一两行:抽成类后样板代码比逻辑还多。
- 分支之间有强耦合顺序:此时可能更适合责任链而不是策略。
- 一次性的脚本或管理端小功能:抽象收益抵不过理解成本。
判断标准:这个 if 在过去半年改过几次?未来半年还会不会改? 改过三次以上,就值得抽。
7. 常见追问
Q:策略模式和状态模式相比呢? 策略由外部选(调用方传 type),状态由内部迁(处理完自动转到下一状态)。订单流转用状态,计价规则用策略。
Q:换成枚举 + 函数式接口算不算策略?
算同一种思想。算法短、无外部依赖时,枚举常量持有 Function 更紧凑;算法需要注入 DAO、调用 RPC 时,还是独立类 + Spring 容器管理更合适。
Q:策略模式会不会只是把 if-else 搬到工厂里?
会——如果只写 Map 而不做"每个策略一个类",就是把分支换成了查表,可读性提升有限。真正的收益来自算法被封进独立的类:可单测、可复用、可单独发布。
Q:怎么避免策略类爆炸?
按业务域分组(计价、风控、路由各一套注册表),而不是一个大注册表塞三十个策略;注册表用 Map 而非 List 遍历匹配。
小结
- 策略换掉的是"变化成本",不是代码长度;收益在开闭、隔离测试、消嵌套。
- 用一张对比表说明代价:类数量、跳转、调试成本。
- 分支少且稳定就别改;判断依据是"这段分支的修改频率"。
- 与状态模式的分水岭:谁决定用哪个实现——外部选择 vs 内部迁移。