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。

我习惯把这种重构分成三步走:

  1. 提取状态接口 — 定义所有状态共有的行为
  2. 实现具体状态类 — 每个状态一个类,各自实现自己的逻辑
  3. 替换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只负责委托。

多态取代switch后的状态流转 Order 委托 <<interface>> OrderState PendingPaymentState 待支付 PaidState 已支付 ShippedState 已发货 CompletedState 已完成 新增状态时:只需新增一个实现类,无需修改Order 每个状态类独立测试,互不影响

避坑指南

我曾经在一个项目中,把所有的状态类都设计成了有状态的(包含成员变量),结果在并发场景下出了大问题。多个线程共享同一个状态实例,数据全乱了。

后来我学乖了:状态类尽量设计成无状态的。如果确实需要携带数据,用方法参数传进去,或者用ThreadLocal。

⚠️ 常见陷阱:

  • 过度设计 — 如果switch只有两三个分支,且不会频繁变化,没必要硬套多态
  • 状态类膨胀 — 如果状态类太多(几十个),考虑用组合模式或策略模式进一步拆分
  • 忽略状态切换 — 多态只解决了“根据状态执行不同行为”,状态之间的流转逻辑需要单独管理

什么时候该用?什么时候不该用?

场景 推荐做法 原因
switch出现在3个以上方法中 用多态 避免重复修改
分支数量固定(如星期几) 保留switch 不会新增,没必要抽象
每个分支逻辑超过10行 用多态 拆分后更清晰
分支逻辑只有一行 保留switch 过度抽象反而增加复杂度

📌 我的判断标准:如果我在同一个类里写了第二个switch,且条件相同,我就知道该重构了。这是最直观的信号。

总结

用多态取代switch,说白了就是把“判断”变成“选择”。判断是你在代码里写if/switch,选择是让对象自己决定怎么做。

这个重构手法,我几乎在每个项目中都用过。刚开始可能会觉得“多写了好多类啊”,但当你维护到第3个月、第6个月,就会发现:多出来的那些类,每一行都在帮你省钱

记住:代码的坏味道不是switch本身,而是散落在各处的重复switch。干掉重复,你就赢了。


公众号:蓝海资料掘金营,微信deep3321