跳转到内容

以卫语句取代嵌套条件表达式

以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)把“特殊情况”从层层嵌套的 if/else 里拎出来,在函数开头一个个检查、一旦命中就立刻返回,剩下的正常流程不再缩进。 它解决的痛是:函数的主要逻辑被埋在第三、第四层缩进里,读者得先读完所有例外,才找得到这个函数平时到底在干什么。

小郑负责公司薪资系统里发工资的那个函数。大多数员工按月薪加加班费发放,加班费按月薪折成时薪再乘 1.5。但有三种特殊情况:员工身故,发两个月月薪作为抚恤金;员工已离职,这个月不发(离职时已结清);员工已退休,也不发(退休金走另一套系统)。函数返回金额和原因,财务对账时要看原因。

"""薪资计算:重构前。"""
from dataclasses import dataclass
@dataclass
class Employee:
name: str
is_dead: bool
is_separated: bool
is_retired: bool
monthly_salary: float
overtime_hours: float
@dataclass
class Pay:
amount: float
reason: str
def pay_amount(employee: Employee) -> Pay:
if employee.is_dead:
result = Pay(employee.monthly_salary * 2, "身故")
else:
if employee.is_separated:
result = Pay(0.0, "离职")
else:
if employee.is_retired:
result = Pay(0.0, "退休")
else:
hourly = employee.monthly_salary / 174
overtime = employee.overtime_hours * hourly * 1.5
result = Pay(employee.monthly_salary + overtime, "正常")
return result
STAFF = [
Employee("张三", False, False, False, 8700, 10),
Employee("李四", True, False, False, 6000, 0),
Employee("王五", False, True, False, 7000, 0),
Employee("赵六", False, False, True, 9000, 0),
]
if __name__ == "__main__":
for e in STAFF:
pay = pay_amount(e)
print(f"{e.name}:{pay.amount:.2f}({pay.reason})")
# 输出:
# 张三:9450.00(正常)
# 李四:12000.00(身故)
# 王五:0.00(离职)
# 赵六:0.00(退休)
// 薪资计算:重构前。
export interface Employee {
name: string;
isDead: boolean;
isSeparated: boolean;
isRetired: boolean;
monthlySalary: number;
overtimeHours: number;
}
export interface Pay {
amount: number;
reason: string;
}
export function payAmount(employee: Employee): Pay {
let result: Pay;
if (employee.isDead) {
result = { amount: employee.monthlySalary * 2, reason: "身故" };
} else {
if (employee.isSeparated) {
result = { amount: 0, reason: "离职" };
} else {
if (employee.isRetired) {
result = { amount: 0, reason: "退休" };
} else {
const hourly = employee.monthlySalary / 174;
const overtime = employee.overtimeHours * hourly * 1.5;
result = { amount: employee.monthlySalary + overtime, reason: "正常" };
}
}
}
return result;
}
function staff(name: string, flags: [boolean, boolean, boolean], salary: number, overtime: number): Employee {
const [isDead, isSeparated, isRetired] = flags;
return { name, isDead, isSeparated, isRetired, monthlySalary: salary, overtimeHours: overtime };
}
export const STAFF: Employee[] = [
staff("张三", [false, false, false], 8700, 10),
staff("李四", [true, false, false], 6000, 0),
staff("王五", [false, true, false], 7000, 0),
staff("赵六", [false, false, true], 9000, 0),
];
for (const e of STAFF) {
const pay = payAmount(e);
console.log(`${e.name}:${pay.amount.toFixed(2)}(${pay.reason})`);
}
// 输出:
// 张三:9450.00(正常)
// 李四:12000.00(身故)
// 王五:0.00(离职)
// 赵六:0.00(退休)

测试覆盖正常发放和三种特殊情况,外加一个“既身故又已离职”的组合,确认身故优先:

import unittest
from target import STAFF, Employee, pay_amount
class PayAmountTest(unittest.TestCase):
def test_normal_pay_includes_overtime(self):
pay = pay_amount(STAFF[0])
self.assertAlmostEqual(pay.amount, 9450)
self.assertEqual(pay.reason, "正常")
def test_special_cases(self):
self.assertEqual([pay_amount(e).reason for e in STAFF[1:]], ["身故", "离职", "退休"])
self.assertAlmostEqual(pay_amount(STAFF[1]).amount, 12000)
def test_dead_takes_priority_over_separated(self):
pay = pay_amount(Employee("钱七", True, True, False, 5000, 0))
self.assertEqual((pay.amount, pay.reason), (10000, "身故"))
if __name__ == "__main__":
unittest.main()
import test from "node:test";
import assert from "node:assert/strict";
import { STAFF, payAmount } from "./target";
test("正常发放含加班费", () => {
assert.deepEqual(payAmount(STAFF[0]), { amount: 9450, reason: "正常" });
});
test("三种特殊情况", () => {
assert.deepEqual(STAFF.slice(1).map((e) => payAmount(e).reason), ["身故", "离职", "退休"]);
assert.equal(payAmount(STAFF[1]).amount, 12000);
});
test("身故优先于离职", () => {
const e = { ...STAFF[0], isDead: true, isSeparated: true, monthlySalary: 5000 };
assert.deepEqual(payAmount(e), { amount: 10000, reason: "身故" });
});

如果现在要加一个需求:停薪留职的员工这个月不发工资,原因写“停薪留职”。

小郑要在三层 else 的最里面再套一层 if,正常发放的那三行代码被推到第五层缩进。他还得想清楚新的 if 该插在哪一层:放在退休的里面还是外面?每一层 else 都对应着前面的某个 if,要对齐括号或者缩进才能确认。Python 版里少缩进一格,代码照样能运行,只是逻辑变了;TS 版里漏一个花括号,编辑器会报错,但报错位置可能在函数末尾,离真正漏掉的地方很远。

读的问题更明显。这个函数最重要的事是“按月薪加加班费发工资”,它占了全公司 99% 的调用。可是读者打开函数,先看到的是身故、离职、退休,正常流程藏在最深处、最靠右的地方。三个特殊情况和正常情况在代码里地位相同,都是 if/else 的一个分支,可业务上它们根本不对等:特殊情况是“先排除掉”,正常情况才是“真正要做的事”。

还有那个 result 变量。每个分支给它赋值,最后统一 return result。读者看到某个分支给 result 赋了值,还得往下扫到函数末尾,确认中间没有别的代码再改它。这个函数里确实没有,但读者不读完就不敢确定。

用第 2 章的名字说,这是过长函数:行数不多,但嵌套让它的阅读成本成倍增加。每加一种特殊情况就多一层缩进,这个函数会越来越长、越来越深。

"""薪资计算:重构后。"""
from dataclasses import dataclass
@dataclass
class Employee:
name: str
is_dead: bool
is_separated: bool
is_retired: bool
monthly_salary: float
overtime_hours: float
@dataclass
class Pay:
amount: float
reason: str
def pay_amount(employee: Employee) -> Pay:
if employee.is_dead:
return Pay(employee.monthly_salary * 2, "身故")
if employee.is_separated:
return Pay(0.0, "离职")
if employee.is_retired:
return Pay(0.0, "退休")
hourly = employee.monthly_salary / 174
overtime = employee.overtime_hours * hourly * 1.5
return Pay(employee.monthly_salary + overtime, "正常")
STAFF = [
Employee("张三", False, False, False, 8700, 10),
Employee("李四", True, False, False, 6000, 0),
Employee("王五", False, True, False, 7000, 0),
Employee("赵六", False, False, True, 9000, 0),
]
if __name__ == "__main__":
for e in STAFF:
pay = pay_amount(e)
print(f"{e.name}:{pay.amount:.2f}({pay.reason})")
# 输出:
# 张三:9450.00(正常)
# 李四:12000.00(身故)
# 王五:0.00(离职)
# 赵六:0.00(退休)
// 薪资计算:重构后。
export interface Employee {
name: string;
isDead: boolean;
isSeparated: boolean;
isRetired: boolean;
monthlySalary: number;
overtimeHours: number;
}
export interface Pay {
amount: number;
reason: string;
}
export function payAmount(employee: Employee): Pay {
if (employee.isDead) return { amount: employee.monthlySalary * 2, reason: "身故" };
if (employee.isSeparated) return { amount: 0, reason: "离职" };
if (employee.isRetired) return { amount: 0, reason: "退休" };
const hourly = employee.monthlySalary / 174;
const overtime = employee.overtimeHours * hourly * 1.5;
return { amount: employee.monthlySalary + overtime, reason: "正常" };
}
function staff(name: string, flags: [boolean, boolean, boolean], salary: number, overtime: number): Employee {
const [isDead, isSeparated, isRetired] = flags;
return { name, isDead, isSeparated, isRetired, monthlySalary: salary, overtimeHours: overtime };
}
export const STAFF: Employee[] = [
staff("张三", [false, false, false], 8700, 10),
staff("李四", [true, false, false], 6000, 0),
staff("王五", [false, true, false], 7000, 0),
staff("赵六", [false, false, true], 9000, 0),
];
for (const e of STAFF) {
const pay = payAmount(e);
console.log(`${e.name}:${pay.amount.toFixed(2)}(${pay.reason})`);
}
// 输出:
// 张三:9450.00(正常)
// 李四:12000.00(身故)
// 王五:0.00(离职)
// 赵六:0.00(退休)

输出不变,三个测试照样通过,包括“身故优先于离职”那个:卫语句的先后顺序就是原来嵌套的由外到内。

左边每种特殊情况多一层嵌套,正常流程在最深处;右边三个卫语句排成一列各自返回,正常流程回到最外层

停薪留职的需求,现在是在退休那行下面加一行 if employee.is_on_leave: return Pay(0.0, "停薪留职"),不碰任何缩进,也不用想它属于哪一层 else。

函数的形状现在和业务的形状一致:前三行是“先排除掉这些人”,后三行是“给其余的人发工资”。读者扫一眼就知道正常流程在哪:最后那几行,没有缩进。

result 变量消失了。每个卫语句命中就直接返回,读者看到 return 就知道这条路到此为止,不用再往下扫。

每改一步跑一次测试。

  1. 找到最外层、最该先排除的那个特殊情况,这里是 is_dead。把它的分支改成直接 return,然后把它的 else 去掉,else 里的代码整体往外缩一层。(IDE 可自动完成:PyCharm 和 VS Code 的部分扩展在 if 上提供 “Invert if condition” 或 “Remove redundant else” 一类的快速修复,但要逐个确认它的结果。)
  2. 跑测试。
  3. 对下一层的特殊情况重复第 1 步,直到所有特殊情况都变成了函数开头的一行 if ...: return ...。
  4. 原来的 result 变量现在只在最后一个分支里赋值、然后立刻返回,把它内联掉,直接 return(第 5 章的内联变量,IDE 可自动完成)。
  5. 如果几个卫语句返回同样的结果、而且业务上是同一条规则,用第 36 章的合并条件表达式把它们并成一个。这里离职和退休都返回 0,但原因不同,所以不合并。

改的时候要守住一条:卫语句的先后顺序必须和原来嵌套的由外到内一致。原来身故在最外层,就意味着“既身故又离职”的人按身故处理;把身故挪到第二行,这个人就会拿到 0 元。本章测试里专门有一个用例防这件事。

另一种常见的形状是:嵌套的条件写的是“满足什么才做事”,而不是“什么时候不做事”。这时要先把条件反过来,再改成卫语句。下面是金融系统里算调整后资本的函数:资本为正、利率和期限都为正时才计算,否则为 0。

"""把条件反过来写成卫语句:重构前。"""
from dataclasses import dataclass
@dataclass
class Instrument:
capital: float
interest_rate: float
duration: int
income: float
adjustment_factor: float
def adjusted_capital(instrument: Instrument) -> float:
result = 0.0
if instrument.capital > 0:
if instrument.interest_rate > 0 and instrument.duration > 0:
result = instrument.income / instrument.duration * instrument.adjustment_factor
return result
if __name__ == "__main__":
print(f"{adjusted_capital(Instrument(100000, 0.05, 12, 1200, 1.5)):.2f}")
print(f"{adjusted_capital(Instrument(0, 0.05, 12, 1200, 1.5)):.2f}")
print(f"{adjusted_capital(Instrument(100000, 0, 12, 1200, 1.5)):.2f}")
# 输出:
# 150.00
# 0.00
# 0.00
"""把条件反过来写成卫语句:重构后。"""
from dataclasses import dataclass
@dataclass
class Instrument:
capital: float
interest_rate: float
duration: int
income: float
adjustment_factor: float
def adjusted_capital(instrument: Instrument) -> float:
if instrument.capital <= 0:
return 0.0
if instrument.interest_rate <= 0 or instrument.duration <= 0:
return 0.0
return instrument.income / instrument.duration * instrument.adjustment_factor
if __name__ == "__main__":
print(f"{adjusted_capital(Instrument(100000, 0.05, 12, 1200, 1.5)):.2f}")
print(f"{adjusted_capital(Instrument(0, 0.05, 12, 1200, 1.5)):.2f}")
print(f"{adjusted_capital(Instrument(100000, 0, 12, 1200, 1.5)):.2f}")
# 输出:
# 150.00
# 0.00
# 0.00
// 把条件反过来写成卫语句:重构前。
export interface Instrument {
capital: number;
interestRate: number;
duration: number;
income: number;
adjustmentFactor: number;
}
export function adjustedCapital(instrument: Instrument): number {
let result = 0;
if (instrument.capital > 0) {
if (instrument.interestRate > 0 && instrument.duration > 0) {
result = (instrument.income / instrument.duration) * instrument.adjustmentFactor;
}
}
return result;
}
const base: Instrument = { capital: 100000, interestRate: 0.05, duration: 12, income: 1200, adjustmentFactor: 1.5 };
console.log(adjustedCapital(base).toFixed(2));
console.log(adjustedCapital({ ...base, capital: 0 }).toFixed(2));
console.log(adjustedCapital({ ...base, interestRate: 0 }).toFixed(2));
// 输出:
// 150.00
// 0.00
// 0.00
// 把条件反过来写成卫语句:重构后。
export interface Instrument {
capital: number;
interestRate: number;
duration: number;
income: number;
adjustmentFactor: number;
}
export function adjustedCapital(instrument: Instrument): number {
if (instrument.capital <= 0) return 0;
if (instrument.interestRate <= 0 || instrument.duration <= 0) return 0;
return (instrument.income / instrument.duration) * instrument.adjustmentFactor;
}
const base: Instrument = { capital: 100000, interestRate: 0.05, duration: 12, income: 1200, adjustmentFactor: 1.5 };
console.log(adjustedCapital(base).toFixed(2));
console.log(adjustedCapital({ ...base, capital: 0 }).toFixed(2));
console.log(adjustedCapital({ ...base, interestRate: 0 }).toFixed(2));
// 输出:
// 150.00
// 0.00
// 0.00

反转条件时,a > 0 变成 a <= 0,x and y 变成 not x or not y(德摩根定律)。稳妥的做法是先机械地写成 if not (instrument.capital > 0): return 0.0,跑过测试,再化简成 <= 0。这一步不是纯形式的:如果字段可能是 NaN,not (x > 0) 为真,x <= 0 却为假,化简就改变了行为。浮点数据来自外部输入时,宁可留着 not (...) 的写法。两个卫语句都返回 0,接着就可以用第 36 章把它们合成一个 if is_not_applicable(instrument): return 0.0。

以卫语句取代嵌套条件表达式:如果某个条件极其罕见,就单独检查它,并在条件为真时立刻从函数返回。这种单独的检查叫卫语句(guard clause)。

条件语句有两种不同的用意。一种是两个分支同等重要,都是正常行为,比如夏季电价和平时电价,这时应该用 if/else,让两条路在代码里对称。另一种是一个分支是正常行为,另一个分支是罕见的例外,这时卫语句告诉读者:“这种情况很少见,遇到了就处理掉,别在它上面花心思。“嵌套条件把这两种用意混在了一起,所有分支看起来都一样重要。

它最常出现在两类地方。一是业务函数开头的资格检查:用户未登录、订单已取消、余额不足。二是 Web 框架的入口:中间件、视图、路由处理器先校验请求,不合格就返回 4xx,合格才进入业务逻辑。后者几乎是现代 Web 代码的标准写法,下面两个例子都是。

有人坚持“一个函数只能有一个出口”,认为多个 return 难以追踪。这条规则来自需要手动释放资源的年代,函数中途返回容易漏掉清理代码。今天 Python 有 with,TS 有 try/finally,资源释放不再依赖单一出口,而嵌套带来的阅读成本是实实在在的。判断标准是:函数是否更清楚了。

Express 的中间件接收 req、res、next,校验不通过就直接写响应,通过才调用 next() 交给下一个处理器。鉴权中间件天然就是一串卫语句:没带令牌、令牌无效、账号被封,各自返回不同的状态码。下面用简化的 Req、Res 和一个内存里的令牌表模拟,authV1 是嵌套写法,authV2 是卫语句写法。

// 模拟 Express 的鉴权中间件。
interface Req {
headers: Record<string, string>;
user?: string;
}
interface Res {
status?: number;
body?: string;
}
type Next = () => void;
const TOKENS: Record<string, { user: string; active: boolean }> = {
t1: { user: "alice", active: true },
t2: { user: "bob", active: false },
};
// 嵌套写法:放行的那一行藏在最里面。
function authV1(req: Req, res: Res, next: Next): void {
const header = req.headers["authorization"];
if (header && header.startsWith("Bearer ")) {
const account = TOKENS[header.slice(7)];
if (account) {
if (account.active) {
req.user = account.user;
next();
} else {
res.status = 403;
res.body = "账号已停用";
}
} else {
res.status = 401;
res.body = "令牌无效";
}
} else {
res.status = 401;
res.body = "缺少令牌";
}
}
// 卫语句写法:每种拒绝各占一行,放行在最后。
function authV2(req: Req, res: Res, next: Next): void {
const header = req.headers["authorization"];
if (!header || !header.startsWith("Bearer ")) {
res.status = 401;
res.body = "缺少令牌";
return;
}
const account = TOKENS[header.slice(7)];
if (!account) {
res.status = 401;
res.body = "令牌无效";
return;
}
if (!account.active) {
res.status = 403;
res.body = "账号已停用";
return;
}
req.user = account.user;
next();
}
function run(auth: typeof authV1, headers: Record<string, string>): string {
const req: Req = { headers };
const res: Res = {};
let passed = false;
auth(req, res, () => {
passed = true;
});
return passed ? `放行 ${req.user}` : `${res.status} ${res.body}`;
}
const cases: Record<string, string>[] = [{}, { authorization: "Bearer xx" }, { authorization: "Bearer t2" }, { authorization: "Bearer t1" }];
console.log(cases.every((h) => run(authV1, h) === run(authV2, h)));
for (const h of cases) console.log(run(authV2, h));
// 输出:
// true
// 401 缺少令牌
// 401 令牌无效
// 403 账号已停用
// 放行 alice

真实的 Express 代码里,卫语句常写成 return res.status(401).send("缺少令牌"),一行完成响应和返回。忘写 return 是 Express 新手最常见的 bug 之一:响应发出去了,代码却继续往下走,最后又调用一次 next(),框架报“响应头已发送”。嵌套写法不会有这个问题,但代价是放行逻辑永远在最深处;卫语句写法要求每个卫语句都以返回结束,这也是本章 Python 版用 return Pay(...) 而不是先赋值的原因。

Django、Flask、FastAPI 的视图函数都要先校验查询参数。下面用字典模拟请求参数、用元组模拟响应的状态码和内容,search_view_v1 是嵌套写法,search_view_v2 是卫语句写法。

"""模拟 Django 视图:搜索接口的参数校验。"""
ARTICLES = ["重构入门", "重构与测试", "设计模式导读"]
def search_view_v1(params: dict) -> tuple[int, str]:
"""嵌套写法。"""
if "q" in params:
q = params["q"].strip()
if q:
page = params.get("page", "1")
if page.isdigit() and int(page) >= 1:
hits = [a for a in ARTICLES if q in a]
return 200, f"第 {page} 页:{'、'.join(hits) or '无结果'}"
else:
return 400, "page 必须是正整数"
else:
return 400, "q 不能为空"
else:
return 400, "缺少参数 q"
def search_view_v2(params: dict) -> tuple[int, str]:
"""卫语句写法。"""
if "q" not in params:
return 400, "缺少参数 q"
q = params["q"].strip()
if not q:
return 400, "q 不能为空"
page = params.get("page", "1")
if not (page.isdigit() and int(page) >= 1):
return 400, "page 必须是正整数"
hits = [a for a in ARTICLES if q in a]
return 200, f"第 {page} 页:{'、'.join(hits) or '无结果'}"
if __name__ == "__main__":
cases = [{}, {"q": " "}, {"q": "重构", "page": "0"}, {"q": "重构"}]
print(all(search_view_v1(c) == search_view_v2(c) for c in cases))
for c in cases:
print(*search_view_v2(c))
# 输出:
# True
# 400 缺少参数 q
# 400 q 不能为空
# 400 page 必须是正整数
# 200 第 1 页:重构入门、重构与测试

嵌套写法里,错误信息和触发它的条件隔着整个 if 体:"缺少参数 q" 在最后一行,检查 "q" in params 在第一行,中间隔了十行。卫语句写法里,每个条件和它的错误信息紧挨着。注意 page 的校验用了 not (...) 包住原条件,而不是手工化简,这正是前面说的稳妥做法。FastAPI 更进一步,用类型注解让框架替你写这些卫语句,但框架做不了的业务校验,最后还是写成同样的形状。

两个分支同等重要时别用。夏季电价和平时电价不是“例外和正常”的关系,写成 if is_summer: return summer_charge() 再接 return regular_charge() 也能跑,但它暗示平时电价才是“正常”的,夏季是个特例,这和业务不符。第 35 章用条件表达式写成一行,两条路对称,才是诚实的形状。

卫语句一多,函数开头会变成一长串 if。超过五六个时,说明这个函数在替太多种情况做决定。如果这些卫语句都在检查同一个对象的“类型”(这是哪种员工、哪种订单),该考虑第 38 章以多态取代条件表达式;如果它们都在检查“有没有这个对象”,该考虑第 39 章引入特例。

函数要在返回前做清理(关文件、释放锁、记审计日志)时,多个出口意味着每个出口都要记得清理。这时先用 with 或 try/finally 把清理收到一处,再改卫语句,否则就是在制造漏掉清理的机会。

手法 它关心什么 以卫语句取代嵌套条件表达式关心什么
合并条件表达式(第 36 章) 结果相同的几个条件合成一个 结果可以不同,只是让每种例外先返回;改完常接着合并
分解条件表达式(第 35 章) 给条件和分支起名字,两条路仍然对称 打破对称,表明一条是例外、一条是主线
引入特例(第 39 章) 消除散落在很多调用方的同一种检查 整理一个函数内部的多种检查

下面这个函数判断一个订单能不能发货,三层嵌套。把它改成卫语句。注意测试里的最后一个用例:未付款又已取消的订单,应当显示“等待付款”,卫语句的顺序要守住这一点。

"""练习:订单发货状态。"""
from dataclasses import dataclass
@dataclass
class Order:
paid: bool
cancelled: bool
address: str
def ship_status(order: Order) -> str:
if order.paid:
if not order.cancelled:
if order.address:
status = f"可发货:寄往{order.address}"
else:
status = "缺少收货地址"
else:
status = "订单已取消"
else:
status = "等待付款"
return status
if __name__ == "__main__":
print(ship_status(Order(True, False, "杭州")))
print(ship_status(Order(True, False, "")))
print(ship_status(Order(False, True, "杭州")))
# 输出:
# 可发货:寄往杭州
# 缺少收货地址
# 等待付款
import unittest
from target import Order, ship_status
class ShipStatusTest(unittest.TestCase):
def test_ready(self):
self.assertEqual(ship_status(Order(True, False, "杭州")), "可发货:寄往杭州")
def test_missing_address(self):
self.assertEqual(ship_status(Order(True, False, "")), "缺少收货地址")
def test_cancelled(self):
self.assertEqual(ship_status(Order(True, True, "杭州")), "订单已取消")
def test_unpaid_wins_over_cancelled(self):
self.assertEqual(ship_status(Order(False, True, "杭州")), "等待付款")
if __name__ == "__main__":
unittest.main()
// 练习:订单发货状态。
export interface Order {
paid: boolean;
cancelled: boolean;
address: string;
}
export function shipStatus(order: Order): string {
let status: string;
if (order.paid) {
if (!order.cancelled) {
if (order.address) {
status = `可发货:寄往${order.address}`;
} else {
status = "缺少收货地址";
}
} else {
status = "订单已取消";
}
} else {
status = "等待付款";
}
return status;
}
console.log(shipStatus({ paid: true, cancelled: false, address: "杭州" }));
console.log(shipStatus({ paid: true, cancelled: false, address: "" }));
console.log(shipStatus({ paid: false, cancelled: true, address: "杭州" }));
// 输出:
// 可发货:寄往杭州
// 缺少收货地址
// 等待付款
import test from "node:test";
import assert from "node:assert/strict";
import { shipStatus } from "./target";
test("可以发货", () => {
assert.equal(shipStatus({ paid: true, cancelled: false, address: "杭州" }), "可发货:寄往杭州");
});
test("缺少地址", () => {
assert.equal(shipStatus({ paid: true, cancelled: false, address: "" }), "缺少收货地址");
});
test("已取消", () => {
assert.equal(shipStatus({ paid: true, cancelled: true, address: "杭州" }), "订单已取消");
});
test("未付款优先于已取消", () => {
assert.equal(shipStatus({ paid: false, cancelled: true, address: "杭州" }), "等待付款");
});
  • 条件语句有两种用意:两条路同等重要用 if/else,一条是罕见例外就用卫语句先返回。
  • 卫语句让函数的形状和业务一致:先排除例外,再做正事,正常流程不缩进。
  • 卫语句的先后顺序必须和原来嵌套的由外到内一致,否则多种例外同时成立时结果会变。
  • 把“满足才做”反写成“不满足就返回”时,先机械地加 not (...),跑过测试再化简,留意 NaN 这类反转不对称的值。
  • 卫语句返回相同结果时接着用合并条件表达式;卫语句太多、都在查类型或查空值时,看第 38、39 章。