简化条件逻辑(下):以多态取代条件表达式、引入特例、引入断言
好,咱们接着聊条件逻辑的简化。上一章我们讲了几个比较“轻量”的手法,比如分解条件、合并条件、用卫语句提前返回。这些技巧,说白了就是让 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 / if-else | 让类型自己决定行为,消除调用方的分支判断 |
| 引入特例 | null 检查、边界值处理 | 用特例对象代替空值,统一处理逻辑 |
| 引入断言 | 程序内部逻辑错误的检查 | 明确声明前置/后置条件,尽早暴露问题 |
我个人觉得,这三个手法里,多态是最强大的,但也是最有侵入性的。引入特例最实用,几乎每个项目都能用上。引入断言最轻量,适合作为辅助手段。
你在实际项目中,可以根据具体情况选择合适的手法。记住,我们的目标不是消灭所有条件逻辑,而是让条件逻辑变得清晰、可维护、易于扩展。
避坑指南:我曾经在一个项目中过度使用多态,把一些只有两三个分支的简单条件也重构成了多态。结果代码反而变得更复杂了。后来我总结了一个经验:如果分支数量不超过 3 个,而且不太可能增加,那就保持原样。不要为了重构而重构。
好了,这一章的内容就到这里。希望这三个手法能帮你写出更干净的代码。