跳转到内容

合并条件表达式

合并条件表达式(Consolidate Conditional Expression)把几个检查条件不同、结果却一样的 if 合成一个条件,再给合起来的条件起个名字。 它解决的痛是:一串 if 各自返回同一个值,读的人看不出它们其实是同一条规则,改规则的人也不知道该一起改哪几行。

老陈在给一家公司的人事系统写伤残津贴的计算。津贴是月薪的六成,但人事部规定了三种不发的情况:工龄不满两年、伤残超过十二个月(转为长期病休,走另一套流程)、兼职员工。老陈照着规定一条一条写了下来。

"""伤残津贴:重构前。"""
from dataclasses import dataclass
@dataclass
class Employee:
seniority: int
months_disabled: int
is_part_time: bool
monthly_salary: float
def disability_amount(employee: Employee) -> float:
if employee.seniority < 2:
return 0
if employee.months_disabled > 12:
return 0
if employee.is_part_time:
return 0
return employee.monthly_salary * 0.6
if __name__ == "__main__":
print(f"{disability_amount(Employee(5, 3, False, 9000)):.2f}")
print(f"{disability_amount(Employee(1, 3, False, 9000)):.2f}")
print(f"{disability_amount(Employee(5, 3, True, 9000)):.2f}")
# 输出:
# 5400.00
# 0.00
# 0.00
// 伤残津贴:重构前。
export interface Employee {
seniority: number;
monthsDisabled: number;
isPartTime: boolean;
monthlySalary: number;
}
export function disabilityAmount(employee: Employee): number {
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;
return employee.monthlySalary * 0.6;
}
console.log(disabilityAmount({ seniority: 5, monthsDisabled: 3, isPartTime: false, monthlySalary: 9000 }).toFixed(2));
console.log(disabilityAmount({ seniority: 1, monthsDisabled: 3, isPartTime: false, monthlySalary: 9000 }).toFixed(2));
console.log(disabilityAmount({ seniority: 5, monthsDisabled: 3, isPartTime: true, monthlySalary: 9000 }).toFixed(2));
// 输出:
// 5400.00
// 0.00
// 0.00
import unittest
from target import Employee, disability_amount
class DisabilityAmountTest(unittest.TestCase):
def test_eligible_gets_sixty_percent(self):
self.assertAlmostEqual(disability_amount(Employee(5, 3, False, 9000)), 5400)
def test_each_exclusion_pays_nothing(self):
for employee in [Employee(1, 3, False, 9000), Employee(5, 13, False, 9000), Employee(5, 3, True, 9000)]:
self.assertEqual(disability_amount(employee), 0)
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { disabilityAmount, type Employee } from "./target";
const base: Employee = { seniority: 5, monthsDisabled: 3, isPartTime: false, monthlySalary: 9000 };
test("符合条件的发月薪六成", () => {
assert.equal(disabilityAmount(base), 5400);
});
test("三种不发的情况都返回 0", () => {
for (const change of [{ seniority: 1 }, { monthsDisabled: 13 }, { isPartTime: true }]) {
assert.equal(disabilityAmount({ ...base, ...change }), 0);
}
});

如果现在要加一个需求:员工自助页面上要显示“您目前不符合伤残津贴申领条件”,不符合的人干脆不给申请按钮。

老陈得在页面的代码里把三个条件再抄一遍,因为 disability_amount 里没有一个东西叫“不符合条件”,只有三行分散的 return 0。抄过去之后,规则有了两份。下个月人事部加了第四条“试用期员工不发”,改的人在 disability_amount 里加了一个 if,页面那边不会有人记得,于是试用期员工能点申请按钮,提交后却领到 0 元。

读的时候也有问题。三个 if 并排摆着,读者得逐行确认每个分支返回的都是 0,才能知道它们是同一件事的三种说法。万一中间某一行返回的是 employee.monthly_salary * 0.3,扫一眼也看不出来。这三行在说“一条规则”,代码却写成了“三件事”。

用第 2 章的名字说,这是重复代码:三个分支的结果重复,规则的边界没有一个名字,迟早会被复制到别处。

"""伤残津贴:重构后。"""
from dataclasses import dataclass
@dataclass
class Employee:
seniority: int
months_disabled: int
is_part_time: bool
monthly_salary: float
def is_not_eligible_for_disability(employee: Employee) -> bool:
return employee.seniority < 2 or employee.months_disabled > 12 or employee.is_part_time
def disability_amount(employee: Employee) -> float:
if is_not_eligible_for_disability(employee):
return 0
return employee.monthly_salary * 0.6
if __name__ == "__main__":
print(f"{disability_amount(Employee(5, 3, False, 9000)):.2f}")
print(f"{disability_amount(Employee(1, 3, False, 9000)):.2f}")
print(f"{disability_amount(Employee(5, 3, True, 9000)):.2f}")
# 输出:
# 5400.00
# 0.00
# 0.00
// 伤残津贴:重构后。
export interface Employee {
seniority: number;
monthsDisabled: number;
isPartTime: boolean;
monthlySalary: number;
}
export function isNotEligibleForDisability(employee: Employee): boolean {
return employee.seniority < 2 || employee.monthsDisabled > 12 || employee.isPartTime;
}
export function disabilityAmount(employee: Employee): number {
if (isNotEligibleForDisability(employee)) return 0;
return employee.monthlySalary * 0.6;
}
console.log(disabilityAmount({ seniority: 5, monthsDisabled: 3, isPartTime: false, monthlySalary: 9000 }).toFixed(2));
console.log(disabilityAmount({ seniority: 1, monthsDisabled: 3, isPartTime: false, monthlySalary: 9000 }).toFixed(2));
console.log(disabilityAmount({ seniority: 5, monthsDisabled: 3, isPartTime: true, monthlySalary: 9000 }).toFixed(2));
// 输出:
// 5400.00
// 0.00
// 0.00

自助页面现在调用 is_not_eligible_for_disability 决定显不显示按钮,和计算津贴用的是同一个判断。人事部加第四条规则时,只改这一个函数,页面和计算一起生效。读 disability_amount 的人看到的是一句话:“不符合条件就是 0,否则发六成”。三个条件挤在一行 or 里,读者不用再逐行核对返回值是不是都一样。

  1. 确认要合并的条件都没有副作用。如果某个条件里调用了一个会写日志、改状态的函数,合并后短路求值可能让它不再被调用,得先用第 42 章将查询函数和修改函数分离把副作用拿出去。
  2. 取两个相邻的、结果相同的 if,用 or 把它们的条件连成一个。并列的 if 用 or,嵌套的 if 用 and(见下文)。跑测试。
  3. 重复第 2 步,直到所有结果相同的条件都合进了一个 if。每合一个跑一次测试。
  4. 对合并后的条件做第 35 章的分解条件表达式:提成一个按业务意思命名的函数,这里叫 is_not_eligible_for_disability(IDE 可自动完成)。

第 4 步是这个手法的一半价值。只合并不起名,得到的是一行很长的 or,比三个 if 好不了多少。

上面是并列的 if,用 or 合并。另一种形状是 if 套 if,只有内外两层条件都成立时才走到某个结果,这时用 and 合并。下面是员工休假奖金:正在休假并且工龄超过十年,奖金系数是 1.0,其余情况都是 0.5。

"""嵌套条件用 and 合并:重构前。"""
def vacation_bonus_rate(on_vacation: bool, seniority: int) -> float:
if on_vacation:
if seniority > 10:
return 1.0
return 0.5
if __name__ == "__main__":
print(vacation_bonus_rate(True, 12), vacation_bonus_rate(True, 3), vacation_bonus_rate(False, 12))
# 输出:
# 1.0 0.5 0.5
"""嵌套条件用 and 合并:重构后。"""
def vacation_bonus_rate(on_vacation: bool, seniority: int) -> float:
if on_vacation and seniority > 10:
return 1.0
return 0.5
if __name__ == "__main__":
print(vacation_bonus_rate(True, 12), vacation_bonus_rate(True, 3), vacation_bonus_rate(False, 12))
# 输出:
# 1.0 0.5 0.5
// 嵌套条件用 && 合并:重构前。
export function vacationBonusRate(onVacation: boolean, seniority: number): number {
if (onVacation) {
if (seniority > 10) {
return 1.0;
}
}
return 0.5;
}
console.log(vacationBonusRate(true, 12), vacationBonusRate(true, 3), vacationBonusRate(false, 12));
// 输出:
// 1 0.5 0.5
// 嵌套条件用 && 合并:重构后。
export function vacationBonusRate(onVacation: boolean, seniority: number): number {
if (onVacation && seniority > 10) return 1.0;
return 0.5;
}
console.log(vacationBonusRate(true, 12), vacationBonusRate(true, 3), vacationBonusRate(false, 12));
// 输出:
// 1 0.5 0.5

TS 打印数字 1.0 时写成 1,这是语言的打印习惯,和合并无关。如果条件里既有并列又有嵌套,先把嵌套的用 and 合成一个,再和并列的用 or 连起来,必要时加括号。合并完的条件超过三项时,就该提成函数了,不然这一行本身又变成要读三遍的东西。

合并条件表达式:当一系列条件检查的结果相同时,把它们合成一个条件表达式,并提炼成一个函数。

它最常出现在“资格判断”类代码里:能不能领津贴、能不能退款、能不能参加活动。需求文档通常一条条列出例外情况,开发者照着一条条写 if,写完就是本章开头的样子。合并的依据是业务上它们是不是同一条规则。三个条件都在回答“有没有资格”,就该合并;如果某个条件只是碰巧也返回 0,比如“本月津贴已经发过了”,它说的是另一件事,应该留成独立的检查,甚至用第 37 章的卫语句单独写在前面。

合并之后,几个 if 往往只剩一个,接着就适合换成卫语句。第 37 章的薪资计算里,死亡、离职、退休三种情况各有不同的返回值,不能合并;但如果它们都返回 0,第一步就该是合并。

几个条件虽然返回同一个值,但它们是互不相关的独立检查时,别合并。比如“参数为空返回 0”和“余额不足返回 0”,一个是防御输入,一个是业务规则,合在一起起不出一个诚实的名字,叫 is_zero_case 只是把问题藏起来。起不出名字,就是不该合并的信号。

另一种情况是各个分支将来很可能分道扬镳。如果人事部已经在讨论“兼职员工发三成”,现在把兼职合进“不符合条件”,下个月又得拆出来。这时保持分开,等规则稳定再说。

最后,注意求值顺序和短路行为。原来三个 if 里,后面的条件只在前面的不成立时才求值;按原顺序用 or 合并后仍是这样,短路行为不变。但如果你为了好看把顺序调了,而某个条件依赖前一个条件成立才安全(比如先判断 employee is not None),就会出错。合并时保持原来的顺序。

手法 它关心什么 合并条件表达式关心什么
分解条件表达式(第 35 章) 给一个条件和它的分支起名字 先把多个结果相同的条件并成一个,再起名
以卫语句取代嵌套条件表达式(第 37 章) 让特殊情况先返回,主流程不缩进 让结果相同的几种特殊情况变成一种
  • 几个 if 返回同一个值,而且业务上是同一条规则时,把它们合成一个条件。
  • 并列的 if 用 or 合并,嵌套的 if 用 and 合并;合并时保持原来的顺序。
  • 合并之后一定要提成有名字的函数,名字是这个手法的另一半价值。
  • 起不出诚实的名字,或者各分支将来要分开,就别合并。