48、代码坏味道:switch语句 - 实战以多态取代条件表达式
说实话,switch语句本身没什么错。但我在项目中见过太多这样的场景:一个switch散落在代码的七八个地方,每次新增一种类型,就得把所有switch都改一遍。改漏一个,线上就出bug。这种痛,经历过的人都懂。
今天我们就来聊聊,怎么用多态把这个“坏味道”彻底干掉。
先看一个典型的“坏味道”案例
假设我们有个订单系统,订单有不同的状态:待支付、已支付、已发货、已完成。每个状态下,订单能做的操作不一样。
很多人的第一版代码长这样:
public class Order {
private String status;
public void process() {
switch (status) {
case "PENDING_PAYMENT":
// 处理待支付逻辑
System.out.println("等待用户支付...");
break;
case "PAID":
// 处理已支付逻辑
System.out.println("通知仓库发货...");
break;
case "SHIPPED":
// 处理已发货逻辑
System.out.println("更新物流信息...");
break;
case "COMPLETED":
// 处理已完成逻辑
System.out.println("订单已完成,无需操作");
break;
default:
throw new IllegalArgumentException("未知状态: " + status);
}
}
public String getStatusDisplay() {
switch (status) {
case "PENDING_PAYMENT": return "待支付";
case "PAID": return "已支付";
case "SHIPPED": return "已发货";
case "COMPLETED": return "已完成";
default: return "未知";
}
}
}
你发现问题了吗?同一个switch逻辑,散落在不同的方法里。哪天新增一个“已取消”状态,你得找到所有switch,一个一个加case。漏一个,系统就出幺蛾子。
- 同一个switch出现在多个方法中
- 每次新增类型,需要修改多处代码
- switch的条件是类型/状态枚举
重构思路:用多态取代条件表达式
核心思想很简单:把每种状态的行为封装到独立的类中。这样,新增状态就是新增一个类,而不是改一堆switch。
我习惯把这种重构分成三步走:
- 提取状态接口 — 定义所有状态共有的行为
- 实现具体状态类 — 每个状态一个类,各自实现自己的逻辑
- 替换switch调用 — 让Order委托给状态对象
💡 核心原则:让代码对扩展开放,对修改关闭。新增状态时,不需要改已有代码,只需要加新类。
实战:一步步重构
第一步:定义状态接口
public interface OrderState {
void process(OrderContext context);
String getDisplayName();
}
这里我用了OrderContext,而不是直接把Order传进去。为什么?因为状态类可能需要访问订单的某些属性,但又不想暴露整个Order。说白了,就是控制依赖范围。
第二步:实现具体状态类
public class PendingPaymentState implements OrderState {
@Override
public void process(OrderContext context) {
System.out.println("等待用户支付...");
// 可以在这里触发支付超时检查等逻辑
}
@Override
public String getDisplayName() {
return "待支付";
}
}
public class PaidState implements OrderState {
@Override
public void process(OrderContext context) {
System.out.println("通知仓库发货...");
// 触发发货流程
}
@Override
public String getDisplayName() {
return "已支付";
}
}
// 其他状态类类似,省略...
每个状态类只关心自己的逻辑。你想想看,这样测试起来也方便,每个状态可以单独写单元测试。
第三步:改造Order类
public class Order {
private OrderState state;
public Order(String status) {
this.state = StateFactory.create(status);
}
public void process() {
state.process(new OrderContext(this));
}
public String getStatusDisplay() {
return state.getDisplayName();
}
// 状态切换方法
public void changeState(OrderState newState) {
this.state = newState;
}
}
看到没?原来的switch完全消失了。每个方法只有一行委托调用。
📌 小技巧:StateFactory可以用Map来缓存状态实例,避免每次创建新对象。如果状态是无状态的(没有成员变量),甚至可以做成单例。
状态流转图
下面这张图展示了重构后的状态流转关系。每个状态类独立处理自己的逻辑,Order只负责委托。
避坑指南
我曾经在一个项目中,把所有的状态类都设计成了有状态的(包含成员变量),结果在并发场景下出了大问题。多个线程共享同一个状态实例,数据全乱了。
后来我学乖了:状态类尽量设计成无状态的。如果确实需要携带数据,用方法参数传进去,或者用ThreadLocal。
⚠️ 常见陷阱:
- 过度设计 — 如果switch只有两三个分支,且不会频繁变化,没必要硬套多态
- 状态类膨胀 — 如果状态类太多(几十个),考虑用组合模式或策略模式进一步拆分
- 忽略状态切换 — 多态只解决了“根据状态执行不同行为”,状态之间的流转逻辑需要单独管理
什么时候该用?什么时候不该用?
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| switch出现在3个以上方法中 | 用多态 | 避免重复修改 |
| 分支数量固定(如星期几) | 保留switch | 不会新增,没必要抽象 |
| 每个分支逻辑超过10行 | 用多态 | 拆分后更清晰 |
| 分支逻辑只有一行 | 保留switch | 过度抽象反而增加复杂度 |
📌 我的判断标准:如果我在同一个类里写了第二个switch,且条件相同,我就知道该重构了。这是最直观的信号。
总结
用多态取代switch,说白了就是把“判断”变成“选择”。判断是你在代码里写if/switch,选择是让对象自己决定怎么做。
这个重构手法,我几乎在每个项目中都用过。刚开始可能会觉得“多写了好多类啊”,但当你维护到第3个月、第6个月,就会发现:多出来的那些类,每一行都在帮你省钱。
记住:代码的坏味道不是switch本身,而是散落在各处的重复switch。干掉重复,你就赢了。