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

好,咱们接着聊条件逻辑的简化。上一章我们讲了几个比较“轻量”的手法,比如分解条件、合并条件、用卫语句提前返回。这些技巧,说白了就是让 if-else 看起来更清爽。

但有些场景,光靠这些还不够。你想想看,当你的代码里出现了一大堆根据类型做不同处理的 switch-case,或者反复检查某个对象是不是 null,又或者某个条件分支里藏着复杂的业务规则——这时候,就该请出今天这三位“重量级选手”了。

以多态取代条件表达式

先说说多态。我个人习惯把多态看作是“面向对象的终极武器”。为什么?因为它能干掉一大片条件逻辑。

举个例子。假设你有一个订单系统,需要根据订单类型计算运费。常见的写法是这样的:

public double calculateShippingCost(Order order) {
    if (order.getType().equals("STANDARD")) {
        return order.getWeight() * 0.5;
    } else if (order.getType().equals("EXPRESS")) {
        return order.getWeight() * 1.2 + 10;
    } else if (order.getType().equals("OVERNIGHT")) {
        return order.getWeight() * 2.0 + 20;
    } else {
        throw new IllegalArgumentException("Unknown order type");
    }
}

这段代码有什么问题?嗯,问题不少。每次新增一种订单类型,你都得往这个函数里加一个 else-if。时间一长,这个函数会变得又臭又长。而且,不同订单类型的计算逻辑耦合在一起,改一个可能影响另一个。

用多态怎么改?很简单。把每种订单类型变成一个子类,让它们各自实现自己的运费计算逻辑。

public abstract class Order {
    protected double weight;
    public abstract double calculateShippingCost();
}

public class StandardOrder extends Order {
    @Override
    public double calculateShippingCost() {
        return weight * 0.5;
    }
}

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

public class OvernightOrder extends Order {
    @Override
    public double calculateShippingCost() {
        return weight * 2.0 + 20;
    }
}

调用方就变成了:

public double calculateShippingCost(Order order) {
    return order.calculateShippingCost();
}

你看,原来的 if-else 完全消失了。新增一种订单类型,只需要新增一个子类,不需要修改现有代码。这就是开闭原则的体现。

核心要点:多态取代条件表达式的本质,是把“根据类型做不同处理”的逻辑,从调用方转移到类型自身。每个类型只关心自己的行为,互不干扰。

我的经验:我在项目中遇到过好几次这样的重构。有一次是一个支付系统,根据支付方式(微信、支付宝、银行卡)计算手续费。最初也是用 switch-case,后来随着支付方式越来越多,代码变得难以维护。重构为多态后,每种支付方式一个类,新增支付方式只需要加一个类,测试也方便多了。

引入特例

接下来聊聊“引入特例”。这个手法,说白了就是处理那些“边界情况”或“特殊值”的。

最常见的场景是什么?null 检查。你写代码的时候,是不是经常要判断某个对象是不是 null?比如:

public String getCustomerName(Customer customer) {
    if (customer != null) {
        return customer.getName();
    } else {
        return "Unknown";
    }
}

这种代码到处都是,看着就烦。而且,如果 customer 为 null 的情况在很多地方都要处理,那你就得在每个地方都写一遍 null 检查。

引入特例的做法是:创建一个“特例对象”,代表这个 null 的情况。这个特例对象和正常对象有相同的接口,但行为不同。

public class Customer {
    private String name;
    
    public String getName() {
        return name;
    }
}

public class NullCustomer extends Customer {
    @Override
    public String getName() {
        return "Unknown";
    }
}

// 使用
public String getCustomerName(Customer customer) {
    return customer.getName();
}

调用方不再需要检查 null 了。如果 customer 是 NullCustomer 的实例,getName() 直接返回 "Unknown"。干净利落。

注意:引入特例不是让你在所有地方都用特例对象。它适用于那些“空值有统一处理逻辑”的场景。如果每个地方对空值的处理都不一样,那引入特例反而会增加复杂度。

我曾经在一个项目中接手过一个遗留系统,里面到处都是 if (obj != null) 的判断。有些地方甚至嵌套了三层 null 检查。我花了两个迭代,逐步引入了特例对象,把那些 null 检查全部干掉了。代码的可读性提升了一大截。

引入断言

最后说说“引入断言”。这个手法比较轻,但很实用。

断言是什么?说白了,就是在代码里明确声明“这里应该是什么情况”。如果实际情况和声明不符,程序就报错。

举个例子。你写了一个函数,要求传入的参数必须大于 0:

public double calculateDiscount(double amount) {
    // 这里假设 amount 一定大于 0
    if (amount <= 0) {
        throw new IllegalArgumentException("Amount must be positive");
    }
    // 业务逻辑...
}

这种检查,其实就是在做断言。但更优雅的方式是使用专门的断言机制:

public double calculateDiscount(double amount) {
    assert amount > 0 : "Amount must be positive";
    // 业务逻辑...
}

Java 里的 assert 关键字,默认是关闭的。你需要用 -ea 参数开启。所以,它更适合用来做“开发期检查”,而不是“运行期校验”。

关键区别:断言用于检查“不应该发生的情况”,比如程序内部的逻辑错误。而异常处理用于检查“可能发生的情况”,比如用户输入错误、网络超时等。

我个人习惯在以下场景使用断言:

  • 函数入口处,检查参数是否满足前置条件
  • 函数出口处,检查返回值是否满足后置条件
  • 循环或递归中,检查不变量是否被破坏

举个例子,我在写一个二分查找算法时,会在循环开始前断言数组是有序的:

public int binarySearch(int[] arr, int target) {
    assert isSorted(arr) : "Array must be sorted";
    // 二分查找逻辑...
}

这样,如果哪天有人传了一个无序数组进来,程序会立刻崩溃,而不是返回一个错误的结果。嗯,早点崩溃比晚点发现要好得多。

知识体系总览

为了让你更直观地理解这三个手法的关系,我画了一张图:

简化条件逻辑(下)知识体系 以多态取代条件表达式 引入特例 引入断言 适用场景 根据类型做不同处理 switch-case 分支过多 适用场景 null 检查泛滥 边界值统一处理 适用场景 前置/后置条件检查 程序不变量验证 核心原则:消除重复的条件判断,让代码自己说话 每个分支只关心自己的事,调用方不再需要知道内部细节

总结一下

这三个手法,各有各的用处:

手法 解决的问题 核心思想
以多态取代条件表达式 根据类型做不同处理的 switch-case / if-else 让类型自己决定行为,消除调用方的分支判断
引入特例 null 检查、边界值处理 用特例对象代替空值,统一处理逻辑
引入断言 程序内部逻辑错误的检查 明确声明前置/后置条件,尽早暴露问题

我个人觉得,这三个手法里,多态是最强大的,但也是最有侵入性的。引入特例最实用,几乎每个项目都能用上。引入断言最轻量,适合作为辅助手段。

你在实际项目中,可以根据具体情况选择合适的手法。记住,我们的目标不是消灭所有条件逻辑,而是让条件逻辑变得清晰、可维护、易于扩展。

避坑指南:我曾经在一个项目中过度使用多态,把一些只有两三个分支的简单条件也重构成了多态。结果代码反而变得更复杂了。后来我总结了一个经验:如果分支数量不超过 3 个,而且不太可能增加,那就保持原样。不要为了重构而重构。

好了,这一章的内容就到这里。希望这三个手法能帮你写出更干净的代码。

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