合并条件表达式
合并条件表达式(Consolidate Conditional Expression)把几个检查条件不同、结果却一样的 if 合成一个条件,再给合起来的条件起个名字。 它解决的痛是:一串 if 各自返回同一个值,读的人看不出它们其实是同一条规则,改规则的人也不知道该一起改哪几行。
老陈在给一家公司的人事系统写伤残津贴的计算。津贴是月薪的六成,但人事部规定了三种不发的情况:工龄不满两年、伤残超过十二个月(转为长期病休,走另一套流程)、兼职员工。老陈照着规定一条一条写了下来。
"""伤残津贴:重构前。"""from dataclasses import dataclass
@dataclassclass 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.00import 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
@dataclassclass 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 里,读者不用再逐行核对返回值是不是都一样。
怎么一步步做
Section titled “怎么一步步做”- 确认要合并的条件都没有副作用。如果某个条件里调用了一个会写日志、改状态的函数,合并后短路求值可能让它不再被调用,得先用第 42 章将查询函数和修改函数分离把副作用拿出去。
- 取两个相邻的、结果相同的 if,用
or把它们的条件连成一个。并列的 if 用or,嵌套的 if 用and(见下文)。跑测试。 - 重复第 2 步,直到所有结果相同的条件都合进了一个 if。每合一个跑一次测试。
- 对合并后的条件做第 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.5TS 打印数字 1.0 时写成 1,这是语言的打印习惯,和合并无关。如果条件里既有并列又有嵌套,先把嵌套的用 and 合成一个,再和并列的用 or 连起来,必要时加括号。合并完的条件超过三项时,就该提成函数了,不然这一行本身又变成要读三遍的东西。
定义与适用场景
Section titled “定义与适用场景”合并条件表达式:当一系列条件检查的结果相同时,把它们合成一个条件表达式,并提炼成一个函数。
它最常出现在“资格判断”类代码里:能不能领津贴、能不能退款、能不能参加活动。需求文档通常一条条列出例外情况,开发者照着一条条写 if,写完就是本章开头的样子。合并的依据是业务上它们是不是同一条规则。三个条件都在回答“有没有资格”,就该合并;如果某个条件只是碰巧也返回 0,比如“本月津贴已经发过了”,它说的是另一件事,应该留成独立的检查,甚至用第 37 章的卫语句单独写在前面。
合并之后,几个 if 往往只剩一个,接着就适合换成卫语句。第 37 章的薪资计算里,死亡、离职、退休三种情况各有不同的返回值,不能合并;但如果它们都返回 0,第一步就该是合并。
什么时候别用
Section titled “什么时候别用”几个条件虽然返回同一个值,但它们是互不相关的独立检查时,别合并。比如“参数为空返回 0”和“余额不足返回 0”,一个是防御输入,一个是业务规则,合在一起起不出一个诚实的名字,叫 is_zero_case 只是把问题藏起来。起不出名字,就是不该合并的信号。
另一种情况是各个分支将来很可能分道扬镳。如果人事部已经在讨论“兼职员工发三成”,现在把兼职合进“不符合条件”,下个月又得拆出来。这时保持分开,等规则稳定再说。
最后,注意求值顺序和短路行为。原来三个 if 里,后面的条件只在前面的不成立时才求值;按原顺序用 or 合并后仍是这样,短路行为不变。但如果你为了好看把顺序调了,而某个条件依赖前一个条件成立才安全(比如先判断 employee is not None),就会出错。合并时保持原来的顺序。
容易混淆的手法
Section titled “容易混淆的手法”| 手法 | 它关心什么 | 合并条件表达式关心什么 |
|---|---|---|
| 分解条件表达式(第 35 章) | 给一个条件和它的分支起名字 | 先把多个结果相同的条件并成一个,再起名 |
| 以卫语句取代嵌套条件表达式(第 37 章) | 让特殊情况先返回,主流程不缩进 | 让结果相同的几种特殊情况变成一种 |
- 几个 if 返回同一个值,而且业务上是同一条规则时,把它们合成一个条件。
- 并列的 if 用
or合并,嵌套的 if 用and合并;合并时保持原来的顺序。 - 合并之后一定要提成有名字的函数,名字是这个手法的另一半价值。
- 起不出诚实的名字,或者各分支将来要分开,就别合并。