跳转到内容

分解条件表达式

分解条件表达式(Decompose Conditional)把一个 if 的条件和每个分支各提成一个有名字的函数,原处只剩“如果是这种情况,就做这件事,否则做那件事”。 它解决的痛是:一段条件逻辑里,判断写成了一串比较,分支写成了一堆算式,读的人得先把它们翻译回业务语言,才知道代码在说什么。

小林在一家电力公司做计费系统。电价规则来自物价局的文件:夏季(6 月 1 日到 8 月 31 日)每度 0.68 元,当月用电超过 400 度的部分每度再加 0.30 元;其余月份每度 0.52 元,另收 5 元固定服务费。夏季的起止日期和单价都写在计费方案 Plan 里,每年可能调整。小林写了一个函数,给定方案、抄表日期和用电度数,算出这一期的电费。

"""电费账单:重构前。"""
from dataclasses import dataclass
from datetime import date
@dataclass
class 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 unittest
from 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 dataclass
from datetime import date
@dataclass
class 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

输出一个字没变,四个测试照样通过。

重构前判断是两个日期比较、分支是几行算式;重构后判断叫 is_summer,两个分支叫 summer_charge 和 regular_charge

账单标注那个需求,现在直接调用同一个判断,不用复制:

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 单独补一个“最后一天下午”的测试就够了。

每一步之后都跑测试。

  1. 把条件表达式提成一个函数,按它在业务上的意思命名,is_summer,不叫 check_date。把 charge 里的条件换成对它的调用。这一步就是第 4 章的提炼函数(IDE 可自动完成)。
  2. 把 if 分支的代码提成函数 summer_charge。分支里被赋值、之后要返回的 amount 变成新函数的返回值,原处写成 amount = summer_charge(plan, kwh)(IDE 可自动完成)。
  3. 对 else 分支做同样的事,提成 regular_charge(IDE 可自动完成)。
  4. 两个分支现在都只剩一句赋值,把整个 if 改写成一个条件表达式(Python 的 x if c else y,TS 的三元运算符),直接返回。如果分支多于两个,或者条件表达式读起来比 if 更费劲,就保留 if 语句,只是每个分支里只有一行调用。
  5. 回头看提出来的函数名在调用处读起来是否通顺。summer_charge(plan, kwh) if is_summer(plan, day) 念出来就是规则原文,说明名字起对了。

IDE 能自动做的是第 1 到第 3 步的机械部分:选中条件或分支,Extract Function / Extract Method,输入名字。它不会替你判断该叫 is_summer 还是 in_period,也不会提醒你账单那边还有一份要换。

分解条件表达式:把一个复杂条件语句的判断部分和每个分支分别提炼成函数,让条件语句只表达“在什么情况下做什么”。它其实是提炼函数用在条件语句上的特例,单独起一个名字,是因为条件逻辑特别容易写成“判断是一串比较,分支是一堆算式”的样子,而这类代码最常出现两种错:边界写错(< 还是 <=)、规则改了一处漏了一处。

判断要不要分解,看条件和分支能不能用一个业务词说出来。“是否夏季”“会员是否有效”“表单是否填完整”,这些词在需求文档里就存在,代码里却只看得到字段比较,就该分解。条件只有一个比较、而且比较本身就是业务词时(if order.is_paid:),就没必要再包一层。

它常是后续条件重构的第一步。分解完你常会发现几个分支返回的是同一个值,那是第 36 章合并条件表达式的机会;发现同一个判断在好几个函数里出现、并且是按某种类型分支,那是第 38 章以多态取代条件表达式的机会。分支提成函数之后,换成子类的方法只是把函数搬个家。

几乎每个带会员的 Python 后端都有一个“会员能不能用权益”的判断:没冻结,并且没过期或者还在宽限期内。这个条件很容易被写进视图或服务函数的第一行,分支里再算剩余天数、拼提示文案。下面用 dataclass 和 datetime 模拟,benefit_message_v1 是分解前,benefit_message_v2 是分解后。

"""模拟会员系统:会员能否使用权益。"""
from dataclasses import dataclass
from datetime import date, timedelta
GRACE_DAYS = 7
@dataclass
class 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 提出来之后,会员中心页面显示“续费”按钮、定时任务给快过期的会员发短信,都能用同一个判断,不会出现“页面说你能用、接口说你不能用”的情况。

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。

条件本身已经是业务词时别分解。if user.is_admin: 包成 is_administrator(user) 只多了一次跳转,没多出任何信息。分支只有一行、而且一眼能看出在做什么时,也不必提成函数,return 0 不需要叫 zero_charge()。

别为了分解而起一个只是把条件念一遍的名字。is_day_between_start_and_end 没有比 start <= day <= end 多说什么,读者照样不知道这个区间是什么意思。分解的价值全在业务词上,起不出业务名字,往往说明你还没弄清这个条件为什么存在,这时先去问清规则,比先重构更有用。

一个判断只在一个地方用、规则也很稳定的时候,分解的收益主要是可读性,做不做可以看团队习惯。真正必须分解的信号是:同一个条件要在第二个地方出现了。

手法 它关心什么 分解条件表达式关心什么
提炼函数(第 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
# 10
import 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
// 10
import 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 章)。