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

好,我们接着聊条件逻辑的简化。上一讲我们聊了分解条件、合并条件这些基础手法。今天要讲的这三个手法——多态取代条件、引入特例、引入断言——说实话,是我在实际项目中用得最频繁的。尤其是多态,几乎每次重构大型业务系统都会用到。

以多态取代条件表达式

先问个问题:你见过那种switch-case写了几十行的代码吗?我见过。有一次接手一个电商项目,光是订单状态的处理就用了三个switch,分布在不同的类里。每次加一个新状态,得改三四个地方。这种代码,说白了就是欠重构。

多态取代条件表达式的核心思想很简单:把条件分支变成多态调用。每个分支对应一个子类,条件判断的逻辑被分散到各个子类的方法中。

来看个例子。假设我们有这样一个计算员工奖金的函数:

function calculateBonus(employee) {
  if (employee.type === 'engineer') {
    return employee.salary * 0.1;
  } else if (employee.type === 'manager') {
    return employee.salary * 0.2 + 1000;
  } else if (employee.type === 'salesman') {
    return employee.salary * 0.15 + 500;
  } else {
    throw new Error('未知员工类型');
  }
}

这段代码的问题在哪?每次加新类型,都得改这个函数。而且如果其他地方也有类似的判断,你很可能忘了同步修改。我在项目中遇到过这种情况——加了一个新角色,结果奖金计算对了,但休假天数计算还是旧的,线上出了bug。

用多态重构后:

class Employee {
  constructor(salary) {
    this.salary = salary;
  }
  calculateBonus() {
    throw new Error('子类必须实现');
  }
}

class Engineer extends Employee {
  calculateBonus() {
    return this.salary * 0.1;
  }
}

class Manager extends Employee {
  calculateBonus() {
    return this.salary * 0.2 + 1000;
  }
}

class Salesman extends Employee {
  calculateBonus() {
    return this.salary * 0.15 + 500;
  }
}

调用方就变成了:

function calculateBonus(employee) {
  return employee.calculateBonus();
}

你看,原来的条件判断消失了。每个类型自己负责自己的计算逻辑。加新类型?直接加个子类就行,不用改任何现有代码。这就是开闭原则的体现。

什么时候该用多态取代条件?

  • 条件表达式根据类型做不同处理,且类型经常增加
  • 多个地方有相同的类型判断逻辑
  • 每个分支的逻辑比较复杂,不适合放在一个函数里

我的习惯:如果switch-case超过3个分支,我就会考虑用多态。但也不是绝对的——如果分支逻辑很简单,比如就一行赋值,那用多态反而过度设计了。

引入特例

接下来聊一个很实用但容易被忽视的手法——引入特例。说白了,就是处理那些「边界情况」。

你想想看,代码里最常见的重复是什么?我猜是判空。每次调用一个可能为null的对象,都得先检查一下。代码里到处都是if (xxx === null) return defaultXXX; 这种模式。

举个例子:

function getCustomerName(customer) {
  if (customer === null) {
    return '未知客户';
  }
  return customer.name;
}

function getCustomerAddress(customer) {
  if (customer === null) {
    return '地址不详';
  }
  return customer.address;
}

function getCustomerPhone(customer) {
  if (customer === null) {
    return '无联系方式';
  }
  return customer.phone;
}

每个函数都要判空,重复了三遍。而且如果以后要加一个新字段,很容易忘记判空。

引入特例的做法是:创建一个「特例对象」,它和正常对象有相同的接口,但返回默认值。

class Customer {
  constructor(name, address, phone) {
    this.name = name;
    this.address = address;
    this.phone = phone;
  }
}

class NullCustomer {
  get name() { return '未知客户'; }
  get address() { return '地址不详'; }
  get phone() { return '无联系方式'; }
}

// 使用
function getCustomerInfo(customer) {
  const safeCustomer = customer ?? new NullCustomer();
  console.log(safeCustomer.name);
  console.log(safeCustomer.address);
  console.log(safeCustomer.phone);
}

这样一来,调用方就不需要判空了。特例对象自己知道该返回什么。

我曾经踩过的坑:特例对象一定要实现完整的接口。有一次我只实现了几个常用方法,结果某个新来的同事调了一个没实现的方法,直接抛异常了。所以建议给特例对象加个类型标记,或者在基类里提供默认实现。

引入特例不仅适用于null,也适用于其他边界情况。比如:

  • 空集合(空列表、空Map)
  • 默认配置
  • 未登录用户
  • 测试环境下的模拟数据

引入断言

最后一个手法,引入断言。这个手法其实不是「简化」条件逻辑,而是「明确化」条件逻辑。

什么是断言?就是明确告诉读者:这里我假设某个条件一定成立。如果条件不成立,那就是程序出错了,应该立刻报错。

来看个例子:

function applyDiscount(price, discountRate) {
  // 折扣率应该在0到1之间
  if (discountRate < 0 || discountRate > 1) {
    throw new Error('折扣率不合法');
  }
  return price * (1 - discountRate);
}

这段代码用if判断来检查参数合法性。但问题是,这个检查到底是「业务逻辑」还是「防御性检查」?从代码上看,它和业务逻辑混在一起,读者分不清。

用断言改写:

function applyDiscount(price, discountRate) {
  assert(discountRate >= 0 && discountRate <= 1, '折扣率必须在0到1之间');
  return price * (1 - discountRate);
}

assert函数在开发环境会抛出异常,在生产环境可以关闭。这样就把「假设条件」和「业务逻辑」分开了。

断言的使用场景:

  • 方法入口的参数合法性检查(但不是用户输入校验)
  • 某个分支执行前的前置条件
  • 某个方法执行后的后置条件
  • 循环不变量的检查

我个人习惯在以下情况使用断言:

  • 某个值理论上不可能为null,但为了保险加个检查
  • 某个枚举值只有三种可能,但switch里我写了default分支
  • 某个计算结果应该在某个范围内

注意:断言不是用来替代用户输入校验的。用户输入校验应该用if+错误提示,因为用户输入错误是「预期内」的情况。断言是用来捕获「不应该发生」的程序错误。

三种手法的对比

手法 解决的问题 适用场景 风险
多态取代条件 类型判断导致的重复条件 类型经常增加、多处判断 过度设计,简单场景反而复杂
引入特例 边界值导致的重复判空 null、空集合、默认值 特例对象接口不完整
引入断言 隐含假设不明确 前置条件、后置条件、不变量 误用为输入校验

知识体系图

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

简化条件逻辑(下)知识体系 多态取代条件 引入特例 引入断言 类型分支 → 子类 消除switch-case null → 特例对象 边界值统一处理 假设 → 显式检查 防御编程 核心思想 把条件逻辑从「判断」变成「结构」 让代码自己表达「该做什么」,而不是「如果怎样就怎样」

总结

这三个手法,本质上都是在做同一件事:把条件逻辑从「判断」变成「结构」。多态用类结构取代类型判断,特例用对象结构取代边界判断,断言用语言机制取代隐含假设。

你想想看,好的代码是什么样的?好的代码不是没有条件判断,而是条件判断被合理地组织起来,每个判断都有明确的目的,不会重复,不会散落得到处都是。

嗯,今天就到这里。这三个手法你可以在下次重构时试试看,尤其是多态和特例,效果立竿见影。


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