14、简化条件逻辑(二):以多态取代条件表达式、引入特例(Null Object模式)、引入断言

条件逻辑这东西,写的时候挺爽,改的时候想哭。我见过不少项目,一个方法里塞了七八个 if-else,每次加新需求都得小心翼翼,生怕碰坏哪条分支。今天咱们聊三个重构手法,专门对付这种「条件爆炸」的场景。

以多态取代条件表达式

说白了,就是让对象自己决定怎么做,而不是你替它做判断。你想想看,如果代码里到处都是「if (type == A) doA() else if (type == B) doB()」,那每次加新类型,你都得改这个判断逻辑。改着改着,就改出 bug 了。

我在项目中遇到过这样一个场景:一个订单系统,根据订单类型计算运费。普通订单、加急订单、国际订单,每种计算方式都不一样。一开始就一个 switch 语句,后来加了五六种类型,那个方法膨胀到两百多行。

重构的思路很简单:把每种订单类型做成一个子类,各自实现运费计算方法。

// 重构前
public double calculateShipping(Order order) {
    switch (order.getType()) {
        case "NORMAL":
            return order.getWeight() * 0.5;
        case "EXPRESS":
            return order.getWeight() * 1.2 + 10;
        case "INTERNATIONAL":
            return order.getWeight() * 2.0 + 50;
        default:
            throw new IllegalArgumentException("未知订单类型");
    }
}

// 重构后
public abstract class Order {
    public abstract double calculateShipping();
}

public class NormalOrder extends Order {
    @Override
    public double calculateShipping() {
        return getWeight() * 0.5;
    }
}

public class ExpressOrder extends Order {
    @Override
    public double calculateShipping() {
        return getWeight() * 1.2 + 10;
    }
}

public class InternationalOrder extends Order {
    @Override
    public double calculateShipping() {
        return getWeight() * 2.0 + 50;
    }
}

这样改完,加新类型只需要新增一个子类,不用动现有代码。开闭原则,就这么落地了。

我的习惯:如果条件分支超过三个,而且未来可能增加,我就会考虑用多态。别等到分支爆炸了再动手,那时候重构成本就高了。

引入特例(Null Object模式)

空指针异常,Java 程序员的老朋友了。我记得有一次线上事故,就是因为一个 null 没判断,导致整个支付链路挂了。从那以后,我对 null 特别敏感。

引入特例,说白了就是给「没有值」这种情况一个默认行为。你不需要到处写 if (obj != null),而是让一个空对象自己处理。

// 重构前
public class CustomerService {
    public String getCustomerName(Long id) {
        Customer customer = findCustomer(id);
        if (customer != null) {
            return customer.getName();
        }
        return "未知客户";
    }
}

// 重构后
public interface Customer {
    String getName();
}

public class RealCustomer implements Customer {
    private String name;
    
    @Override
    public String getName() {
        return name;
    }
}

public class NullCustomer implements Customer {
    @Override
    public String getName() {
        return "未知客户";
    }
}

public class CustomerService {
    public Customer findCustomer(Long id) {
        // 如果找不到,返回 NullCustomer 实例
        Customer customer = doFind(id);
        return customer != null ? customer : new NullCustomer();
    }
    
    public String getCustomerName(Long id) {
        return findCustomer(id).getName(); // 再也不怕 null 了
    }
}

这样做的好处很明显:调用方不用再关心 null 的问题,代码干净多了。不过要注意,Null Object 不能滥用。如果业务上需要区分「没有值」和「有值但为空」,那还是得用 Optional 或者显式 null 判断。

我曾经踩过的坑:在一个报表系统里,我把所有可能为 null 的地方都换成了 Null Object。结果调试的时候,根本分不清是数据真的不存在,还是 Null Object 在「假装」有数据。后来我定了个规矩:Null Object 只用在「没有值但可以安全忽略」的场景,比如默认用户、默认配置。

引入断言

断言这东西,很多人觉得可有可无。但我个人习惯,在关键逻辑入口处加断言,能省下大量调试时间。

断言不是用来处理业务异常的,而是用来检查「不应该发生的情况」。比如一个方法要求参数不能为 null,那就在开头断言一下。如果真传了 null,说明调用方有 bug,应该尽早暴露。

public class OrderProcessor {
    public void process(Order order) {
        // 断言:订单不能为 null,金额必须大于 0
        assert order != null : "订单对象不能为 null";
        assert order.getAmount() > 0 : "订单金额必须大于 0";
        
        // 正常业务逻辑
        // ...
    }
}

Java 的 assert 默认是关闭的,需要加 -ea 参数才能启用。所以很多团队更习惯用第三方库的断言工具,或者自己封装一个。

我的建议:断言用在「内部假设」上,比如私有方法的参数校验、循环不变量的检查。对外暴露的 API,还是用正式的参数校验(比如抛出 IllegalArgumentException)更合适。

三种手法的对比

手法 适用场景 核心思想 注意事项
以多态取代条件表达式 根据类型做不同行为,且类型可能增加 让对象自己决定行为 不要过度设计,分支少于三个时没必要
引入特例(Null Object) 需要频繁处理 null 或特殊值 用默认对象代替 null 注意区分「没有值」和「值不存在」
引入断言 检查内部假设,尽早暴露 bug 失败要快,失败要明显 不要用断言处理业务逻辑

知识体系图

下面这张图展示了三种手法在简化条件逻辑中的位置和关系:

简化条件逻辑(二)知识体系 条件逻辑重构 以多态取代条件表达式 消除 switch 开闭原则 子类化 引入特例(Null Object) 消除 null 判断 默认行为 接口统一 引入断言 内部假设 尽早暴露 bug 非业务逻辑 目标:让条件逻辑更清晰、更易扩展、更安全

这三种手法,说白了都是在跟「复杂条件」作斗争。多态让类型判断消失,Null Object 让空值判断消失,断言让隐藏假设显式化。用好了,代码的可读性和可维护性都能上一个台阶。

核心要点:
  • 多态取代条件:让对象自己决定行为,别替它做判断
  • Null Object:用默认对象代替 null,减少防御式判断
  • 断言:检查内部假设,失败要快,失败要明显

嗯,今天就聊到这儿。这三种手法在实际项目中很常用,建议你找个机会练练手。代码这东西,光看是学不会的,得动手改。