分解条件表达式
分解条件表达式(Decompose Conditional)把一个 if 的条件和每个分支各提成一个有名字的函数,原处只剩“如果是这种情况,就做这件事,否则做那件事”。 它解决的痛是:一段条件逻辑里,判断写成了一串比较,分支写成了一堆算式,读的人得先把它们翻译回业务语言,才知道代码在说什么。
小林在一家电力公司做计费系统。电价规则来自物价局的文件:夏季(6 月 1 日到 8 月 31 日)每度 0.68 元,当月用电超过 400 度的部分每度再加 0.30 元;其余月份每度 0.52 元,另收 5 元固定服务费。夏季的起止日期和单价都写在计费方案 Plan 里,每年可能调整。小林写了一个函数,给定方案、抄表日期和用电度数,算出这一期的电费。
"""电费账单:重构前。"""from dataclasses import dataclassfrom datetime import date
@dataclassclass Plan: summer_start: date summer_end: date summer_rate: float regular_rate: float regular_service_charge: float
PLAN = Plan(date(2026, 6, 1), date(2026, 8, 31), 0.68, 0.52, 5.0)
def charge(plan: Plan, day: date, kwh: float) -> float: if day >= plan.summer_start and day <= plan.summer_end: amount = kwh * plan.summer_rate if kwh > 400: amount += (kwh - 400) * 0.30 else: amount = kwh * plan.regular_rate + plan.regular_service_charge return amount
READINGS = [("2026-07-15", 300), ("2026-07-20", 500), ("2026-03-10", 300)]
if __name__ == "__main__": for day, kwh in READINGS: print(f"{day} {kwh} 度 {charge(PLAN, date.fromisoformat(day), kwh):.2f}")
# 输出:# 2026-07-15 300 度 204.00# 2026-07-20 500 度 370.00# 2026-03-10 300 度 161.00// 电费账单:重构前。export interface Plan { summerStart: Date; summerEnd: Date; summerRate: number; regularRate: number; regularServiceCharge: number;}
export const PLAN: Plan = { summerStart: new Date("2026-06-01"), summerEnd: new Date("2026-08-31"), summerRate: 0.68, regularRate: 0.52, regularServiceCharge: 5,};
export function charge(plan: Plan, day: Date, kwh: number): number { let amount: number; if (day >= plan.summerStart && day <= plan.summerEnd) { amount = kwh * plan.summerRate; if (kwh > 400) { amount += (kwh - 400) * 0.3; } } else { amount = kwh * plan.regularRate + plan.regularServiceCharge; } return amount;}
const READINGS: [string, number][] = [ ["2026-07-15", 300], ["2026-07-20", 500], ["2026-03-10", 300],];
for (const [day, kwh] of READINGS) { console.log(`${day} ${kwh} 度 ${charge(PLAN, new Date(day), kwh).toFixed(2)}`);}
// 输出:// 2026-07-15 300 度 204.00// 2026-07-20 500 度 370.00// 2026-03-10 300 度 161.00测试覆盖夏季的两档、夏季最后一天这个边界,以及平时电价:
import unittestfrom datetime import date
from target import PLAN, charge
class ChargeTest(unittest.TestCase): def test_summer_below_tier(self): self.assertAlmostEqual(charge(PLAN, date(2026, 7, 15), 300), 204.0)
def test_summer_above_tier_adds_surcharge(self): self.assertAlmostEqual(charge(PLAN, date(2026, 7, 20), 500), 370.0)
def test_last_day_of_summer_is_summer(self): self.assertAlmostEqual(charge(PLAN, date(2026, 8, 31), 100), 68.0)
def test_regular_adds_service_charge(self): self.assertAlmostEqual(charge(PLAN, date(2026, 9, 1), 300), 161.0)
if __name__ == "__main__": unittest.main()import test from "node:test";import assert from "node:assert/strict";import { PLAN, charge } from "./target";
function near(actual: number, expected: number): void { assert.ok(Math.abs(actual - expected) < 1e-9, `${actual} != ${expected}`);}
test("夏季不超过 400 度", () => { near(charge(PLAN, new Date("2026-07-15"), 300), 204);});
test("夏季超过 400 度的部分加价", () => { near(charge(PLAN, new Date("2026-07-20"), 500), 370);});
test("8 月 31 日仍算夏季", () => { near(charge(PLAN, new Date("2026-08-31"), 100), 68);});
test("平时电价加固定服务费", () => { near(charge(PLAN, new Date("2026-09-01"), 300), 161);});如果现在要加一个需求:账单上每一期旁边标注“夏季电价”或“平时电价”,让用户知道为什么七月的电费比三月贵。
小林要在生成账单的地方再写一遍 day >= plan.summer_start and day <= plan.summer_end。这个判断没有名字,只能复制。复制之后,计费和标注各有一份“什么算夏季”的定义。哪天物价局把夏季改成“7 月 1 日到 9 月 15 日并且只算工作日”,改的人在 charge 里改对了,账单标注还停在旧规则上,用户会看到“平时电价”旁边写着夏季的价格。
第二个问题是读。charge 的第一行是两个日期比较,读的人要自己把它翻译成“是不是夏季”。接着是三行算式,又要自己翻译成“夏季怎么收费”。这个函数一共只有八行,可读者脑子里要同时装着三件事:判断的含义、夏季的规则、平时的规则。函数里没有一个词直接说出它们,“夏季”两个字只出现在字段名的前缀里。
第三个问题是分支里的变量。amount 在夏季分支里先赋值、再有条件地累加,在平时分支里一次赋值。要确认返回值对不对,得顺着两条路径各跟一遍 amount 的变化。如果以后夏季分支再加一个“居民用户减免”,这个变量会被改第三次,平时分支的人也得读一遍夏季分支,确认自己没被影响。
用第 2 章的名字说,这是过长函数:不是行数长,而是一个函数里塞了三个本该各有名字的概念。而那个呼之欲出的账单标注,一旦复制条件,就会再添一处重复代码。
把条件提成 is_summer,两个分支各提成 summer_charge 和 regular_charge。
"""电费账单:重构后。"""from dataclasses import dataclassfrom datetime import date
@dataclassclass Plan: summer_start: date summer_end: date summer_rate: float regular_rate: float regular_service_charge: float
PLAN = Plan(date(2026, 6, 1), date(2026, 8, 31), 0.68, 0.52, 5.0)
def is_summer(plan: Plan, day: date) -> bool: return plan.summer_start <= day <= plan.summer_end
def summer_charge(plan: Plan, kwh: float) -> float: amount = kwh * plan.summer_rate if kwh > 400: amount += (kwh - 400) * 0.30 return amount
def regular_charge(plan: Plan, kwh: float) -> float: return kwh * plan.regular_rate + plan.regular_service_charge
def charge(plan: Plan, day: date, kwh: float) -> float: return summer_charge(plan, kwh) if is_summer(plan, day) else regular_charge(plan, kwh)
READINGS = [("2026-07-15", 300), ("2026-07-20", 500), ("2026-03-10", 300)]
if __name__ == "__main__": for day, kwh in READINGS: print(f"{day} {kwh} 度 {charge(PLAN, date.fromisoformat(day), kwh):.2f}")
# 输出:# 2026-07-15 300 度 204.00# 2026-07-20 500 度 370.00# 2026-03-10 300 度 161.00// 电费账单:重构后。export interface Plan { summerStart: Date; summerEnd: Date; summerRate: number; regularRate: number; regularServiceCharge: number;}
export const PLAN: Plan = { summerStart: new Date("2026-06-01"), summerEnd: new Date("2026-08-31"), summerRate: 0.68, regularRate: 0.52, regularServiceCharge: 5,};
export function isSummer(plan: Plan, day: Date): boolean { return day >= plan.summerStart && day <= plan.summerEnd;}
function summerCharge(plan: Plan, kwh: number): number { let amount = kwh * plan.summerRate; if (kwh > 400) { amount += (kwh - 400) * 0.3; } return amount;}
function regularCharge(plan: Plan, kwh: number): number { return kwh * plan.regularRate + plan.regularServiceCharge;}
export function charge(plan: Plan, day: Date, kwh: number): number { return isSummer(plan, day) ? summerCharge(plan, kwh) : regularCharge(plan, kwh);}
const READINGS: [string, number][] = [ ["2026-07-15", 300], ["2026-07-20", 500], ["2026-03-10", 300],];
for (const [day, kwh] of READINGS) { console.log(`${day} ${kwh} 度 ${charge(PLAN, new Date(day), kwh).toFixed(2)}`);}
// 输出:// 2026-07-15 300 度 204.00// 2026-07-20 500 度 370.00// 2026-03-10 300 度 161.00输出一个字没变,四个测试照样通过。
账单标注那个需求,现在直接调用同一个判断,不用复制:
label = "夏季电价" if is_summer(PLAN, day) else "平时电价"夏季的定义只有一处。物价局改规则时只改 is_summer,计费和标注一起变。
charge 现在读起来就是一句话:是夏季就按夏季收费,否则按平时收费。想知道夏季怎么收,看 summer_charge;想知道什么算夏季,看 is_summer。三个问题分在三个地方,每次只需要读其中一个。
amount 的来回赋值被关进了 summer_charge 里,平时分支变成了一行返回。以后夏季加居民减免,改动只发生在 summer_charge 内部,平时电价的函数一行都不用看。
还有一个顺手的改动:Python 版的条件从 day >= start and day <= end 写成了链式比较 start <= day <= end,这是提炼成独立函数之后才显得自然的写法,读起来就是“日期落在区间里”。
判断有了一个家,边界上的 bug 也只需要修一次。TS 的 Date 带着时分秒:new Date("2026-08-31") 是那天 UTC 时间的零点,如果抄表系统传进来的是 2026-08-31T15:00,它比 summerEnd 大,day <= plan.summerEnd 为假,夏季最后一天下午抄的表会按平时电价算。重构前,这个 bug 在 charge 和账单标注的复制品里各有一份,修了一处另一处还在;重构后,只要在 isSummer 里先把时间截到日期再比较,计费和标注一起修好,再给 isSummer 单独补一个“最后一天下午”的测试就够了。
怎么一步步做
Section titled “怎么一步步做”每一步之后都跑测试。
- 把条件表达式提成一个函数,按它在业务上的意思命名,
is_summer,不叫check_date。把charge里的条件换成对它的调用。这一步就是第 4 章的提炼函数(IDE 可自动完成)。 - 把 if 分支的代码提成函数
summer_charge。分支里被赋值、之后要返回的amount变成新函数的返回值,原处写成amount = summer_charge(plan, kwh)(IDE 可自动完成)。 - 对 else 分支做同样的事,提成
regular_charge(IDE 可自动完成)。 - 两个分支现在都只剩一句赋值,把整个 if 改写成一个条件表达式(Python 的
x if c else y,TS 的三元运算符),直接返回。如果分支多于两个,或者条件表达式读起来比 if 更费劲,就保留 if 语句,只是每个分支里只有一行调用。 - 回头看提出来的函数名在调用处读起来是否通顺。
summer_charge(plan, kwh) if is_summer(plan, day)念出来就是规则原文,说明名字起对了。
IDE 能自动做的是第 1 到第 3 步的机械部分:选中条件或分支,Extract Function / Extract Method,输入名字。它不会替你判断该叫 is_summer 还是 in_period,也不会提醒你账单那边还有一份要换。
定义与适用场景
Section titled “定义与适用场景”分解条件表达式:把一个复杂条件语句的判断部分和每个分支分别提炼成函数,让条件语句只表达“在什么情况下做什么”。它其实是提炼函数用在条件语句上的特例,单独起一个名字,是因为条件逻辑特别容易写成“判断是一串比较,分支是一堆算式”的样子,而这类代码最常出现两种错:边界写错(< 还是 <=)、规则改了一处漏了一处。
判断要不要分解,看条件和分支能不能用一个业务词说出来。“是否夏季”“会员是否有效”“表单是否填完整”,这些词在需求文档里就存在,代码里却只看得到字段比较,就该分解。条件只有一个比较、而且比较本身就是业务词时(if order.is_paid:),就没必要再包一层。
它常是后续条件重构的第一步。分解完你常会发现几个分支返回的是同一个值,那是第 36 章合并条件表达式的机会;发现同一个判断在好几个函数里出现、并且是按某种类型分支,那是第 38 章以多态取代条件表达式的机会。分支提成函数之后,换成子类的方法只是把函数搬个家。
真实系统里的例子
Section titled “真实系统里的例子”会员系统的到期判断
Section titled “会员系统的到期判断”几乎每个带会员的 Python 后端都有一个“会员能不能用权益”的判断:没冻结,并且没过期或者还在宽限期内。这个条件很容易被写进视图或服务函数的第一行,分支里再算剩余天数、拼提示文案。下面用 dataclass 和 datetime 模拟,benefit_message_v1 是分解前,benefit_message_v2 是分解后。
"""模拟会员系统:会员能否使用权益。"""from dataclasses import dataclassfrom datetime import date, timedelta
GRACE_DAYS = 7
@dataclassclass Member: name: str expires_on: date frozen: bool
def benefit_message_v1(member: Member, today: date) -> str: """分解前:条件和两个分支挤在一起。""" if not member.frozen and today <= member.expires_on + timedelta(days=GRACE_DAYS): days_left = (member.expires_on - today).days if days_left >= 0: return f"{member.name}:会员有效,还剩 {days_left} 天" return f"{member.name}:已过期 {-days_left} 天,宽限期内仍可使用" return f"{member.name}:会员不可用,请续费"
def is_active(member: Member, today: date) -> bool: return not member.frozen and today <= member.expires_on + timedelta(days=GRACE_DAYS)
def active_message(member: Member, today: date) -> str: days_left = (member.expires_on - today).days if days_left >= 0: return f"{member.name}:会员有效,还剩 {days_left} 天" return f"{member.name}:已过期 {-days_left} 天,宽限期内仍可使用"
def inactive_message(member: Member) -> str: return f"{member.name}:会员不可用,请续费"
def benefit_message_v2(member: Member, today: date) -> str: """分解后:一句话说清规则。""" return active_message(member, today) if is_active(member, today) else inactive_message(member)
if __name__ == "__main__": today = date(2026, 10, 7) members = [ Member("王五", date(2026, 12, 31), False), Member("赵六", date(2026, 10, 3), False), Member("钱七", date(2026, 12, 31), True), ] print(all(benefit_message_v1(m, today) == benefit_message_v2(m, today) for m in members)) for m in members: print(benefit_message_v2(m, today))
# 输出:# True# 王五:会员有效,还剩 85 天# 赵六:已过期 4 天,宽限期内仍可使用# 钱七:会员不可用,请续费is_active 提出来之后,会员中心页面显示“续费”按钮、定时任务给快过期的会员发短信,都能用同一个判断,不会出现“页面说你能用、接口说你不能用”的情况。
前端注册表单的校验
Section titled “前端注册表单的校验”TS 前端里,提交按钮的处理函数常常以一个四五个条件相连的 if 开头:邮箱有 @、密码够长、两次密码一致、勾了协议。下面用一个普通对象模拟表单状态,submitV1 是分解前,submitV2 把判断提成 isComplete,把两个分支提成 successMessage 和 errorMessage。
// 模拟注册表单的提交处理。interface SignupForm { email: string; password: string; confirm: string; agreed: boolean;}
// 分解前:一长串条件,else 分支再把条件拆开查一遍。function submitV1(form: SignupForm): string { if (form.email.includes("@") && form.password.length >= 8 && form.password === form.confirm && form.agreed) { return `提交成功:${form.email.trim().toLowerCase()}`; } else { const problems: string[] = []; if (!form.email.includes("@")) problems.push("邮箱"); if (form.password.length < 8) problems.push("密码长度"); if (form.password !== form.confirm) problems.push("两次密码"); if (!form.agreed) problems.push("用户协议"); return `请检查:${problems.join("、")}`; }}
// 分解后:判断和两个分支各有名字。function problemsOf(form: SignupForm): string[] { const problems: string[] = []; if (!form.email.includes("@")) problems.push("邮箱"); if (form.password.length < 8) problems.push("密码长度"); if (form.password !== form.confirm) problems.push("两次密码"); if (!form.agreed) problems.push("用户协议"); return problems;}
function isComplete(form: SignupForm): boolean { return problemsOf(form).length === 0;}
function successMessage(form: SignupForm): string { return `提交成功:${form.email.trim().toLowerCase()}`;}
function errorMessage(form: SignupForm): string { return `请检查:${problemsOf(form).join("、")}`;}
function submitV2(form: SignupForm): string { return isComplete(form) ? successMessage(form) : errorMessage(form);}
const good: SignupForm = { email: " Li@Example.com", password: "s3cret-pass", confirm: "s3cret-pass", agreed: true };const bad: SignupForm = { email: "li", password: "123", confirm: "1234", agreed: true };console.log(submitV1(good) === submitV2(good) && submitV1(bad) === submitV2(bad));console.log(submitV2(good));console.log(submitV2(bad));
// 输出:// true// 提交成功:li@example.com// 请检查:邮箱、密码长度、两次密码分解前,“什么算填完整”写了两遍:if 里一遍正着写,else 里一遍反着写。哪天密码最短长度从 8 改成 10,很容易只改了一边,结果是按钮提交失败、错误提示却说没问题。分解后,isComplete 直接由 problemsOf 推出来,两边不可能对不上,按钮的禁用状态也能直接用 isComplete。
什么时候别用
Section titled “什么时候别用”条件本身已经是业务词时别分解。if user.is_admin: 包成 is_administrator(user) 只多了一次跳转,没多出任何信息。分支只有一行、而且一眼能看出在做什么时,也不必提成函数,return 0 不需要叫 zero_charge()。
别为了分解而起一个只是把条件念一遍的名字。is_day_between_start_and_end 没有比 start <= day <= end 多说什么,读者照样不知道这个区间是什么意思。分解的价值全在业务词上,起不出业务名字,往往说明你还没弄清这个条件为什么存在,这时先去问清规则,比先重构更有用。
一个判断只在一个地方用、规则也很稳定的时候,分解的收益主要是可读性,做不做可以看团队习惯。真正必须分解的信号是:同一个条件要在第二个地方出现了。
容易混淆的手法
Section titled “容易混淆的手法”| 手法 | 它关心什么 | 分解条件表达式关心什么 |
|---|---|---|
| 提炼函数(第 4 章) | 任何一段代码的意图和实现分离 | 专门对付 if 的条件和分支,产出“判断函数 + 分支函数”这种固定形状 |
| 提炼变量(第 5 章) | 给条件的一部分起个局部名字,不离开当前函数 | 把条件提到函数外面,别处也能复用 |
| 合并条件表达式(第 36 章) | 把结果相同的几个条件并成一个 | 把一个条件和它的分支拆开起名;常常先分解再合并 |
下面这个停车场计费函数,夜间和周末按次收费,白天按半小时计费。把判断提成“是否非高峰时段”,两个分支各提成一个函数。提炼完测试应当照常通过。
"""练习:停车费。weekday 0 是周一,6 是周日。"""
def parking_fee(weekday: int, hour: int, minutes: int) -> int: if weekday >= 5 or hour < 7 or hour >= 22: fee = 10 if minutes > 120: fee += (minutes - 120) // 60 * 2 else: half_hours = (minutes + 29) // 30 fee = min(half_hours * 3, 60) return fee
if __name__ == "__main__": print(parking_fee(5, 10, 250)) print(parking_fee(2, 10, 100)) print(parking_fee(2, 23, 60))
# 输出:# 14# 12# 10import unittest
from target import parking_fee
class ParkingFeeTest(unittest.TestCase): def test_weekend_flat_fee(self): self.assertEqual(parking_fee(6, 10, 90), 10)
def test_weekend_long_stay(self): self.assertEqual(parking_fee(5, 10, 250), 14)
def test_weekday_daytime_by_half_hour(self): self.assertEqual(parking_fee(2, 10, 100), 12)
def test_weekday_daytime_capped(self): self.assertEqual(parking_fee(2, 9, 1000), 60)
def test_weekday_night(self): self.assertEqual(parking_fee(2, 23, 60), 10)
if __name__ == "__main__": unittest.main()// 练习:停车费。weekday 0 是周一,6 是周日。export function parkingFee(weekday: number, hour: number, minutes: number): number { let fee: number; if (weekday >= 5 || hour < 7 || hour >= 22) { fee = 10; if (minutes > 120) { fee += Math.floor((minutes - 120) / 60) * 2; } } else { const halfHours = Math.floor((minutes + 29) / 30); fee = Math.min(halfHours * 3, 60); } return fee;}
console.log(parkingFee(5, 10, 250));console.log(parkingFee(2, 10, 100));console.log(parkingFee(2, 23, 60));
// 输出:// 14// 12// 10import test from "node:test";import assert from "node:assert/strict";import { parkingFee } from "./target";
test("周末按次收费", () => { assert.equal(parkingFee(6, 10, 90), 10);});
test("周末停得久,超出部分按小时加价", () => { assert.equal(parkingFee(5, 10, 250), 14);});
test("工作日白天按半小时计费", () => { assert.equal(parkingFee(2, 10, 100), 12);});
test("工作日白天封顶 60 元", () => { assert.equal(parkingFee(2, 9, 1000), 60);});
test("工作日夜间按次收费", () => { assert.equal(parkingFee(2, 23, 60), 10);});- 分解条件表达式就是对 if 的条件和每个分支各做一次提炼函数,让条件语句读起来像规则原文。
- 判断标准是能不能用一个业务词说出条件和分支;条件本身已经是业务词时不必再包。
- 同一个判断要在第二处出现,是必须分解的信号,否则规则一变就会漏改。
- 分解常是后续重构的第一步:分支返回值相同就合并(第 36 章),同一判断按类型散在多个函数里就换成多态(第 38 章)。