条件逻辑:分支的形状决定手法
1. 这一章讲什么
「程序的大部分威力来自条件逻辑,但很不幸,程序的复杂度(理解与修改的难度)也大多来自条件逻辑」——本章的出发点是一对共生的评价1。原书六个手法不是平列的,它们各自回答一个诊断问题:**这串分支是什么性质的?**性质诊断对了,手法是唯一的;诊断错了,手法互相打架。本章按这六问重新组织,并把「所有条件都该换成多态」的流行观点摆到作者面前接受质询。
2. 顶层全景
拿到一串条件分支,依次问:
① 读不出意图? → 分解条件表达式(条件提炼成函数,分支提炼成函数)
② 检查各异、结果相同? → 合并条件表达式(|| / &&)
③ 有一支是罕见异常? → 卫语句(先检查、立刻返回)
④ 分支按类型裂开? → 以多态取代条件表达式(每类一个子类)
⑤ 到处防同一个特殊值? → 引入特例(Null 对象是它的特例)
⑥ 有说不出口的假设? → 引入断言(把假设写成代码)
图说:六问按「先表达、再结构」排列——①②改的是表达,③改的是重点,④⑤改的是结构,⑥改的是契约。
3. 表达层:分解与合并
分解条件表达式。复杂条件逻辑是「最常导致复杂度上升的地点之一」,它的症状很精确:代码「会告诉我发生的事,但常常让我弄不清楚为什么会发生」——在做什么的层面读得懂,在为什么的层面断线2。药方是双向提炼:条件判断提炼成函数(回答「为什么走这条分支」),每个分支体提炼成函数(回答「这条分支在干什么」)。原书夏季价格的例子:if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) 变成 if (summer()),分支体变成 summerCharge()/regularCharge()——一层函数调用把业务规则翻译了出来。作者承认这手法「其实只是提炼函数的一个应用场景」,单列出来是因为它「经常会带来很大的价值」3。
合并条件表达式解决另一头:一串检查各不相同、结果却一样的分支,该用逻辑运算符合成一条。「顺序执行的条件表达式用逻辑或来合并,嵌套的if语句用逻辑与来合并」4。合并的两个理由值得记住:它让代码说出「实际上只有一次条件检查」,而不是「几条恰好碰在一起的独立检查」;合并后的条件往往正是下一个提炼函数的原料——「把描述“做什么”的语句换成了“为什么这样做”」5。反向判据同样在书里:如果这些检查其实彼此独立、不该算一次检查,就不该合并。原书例子:病残补助的三道资格检查(seniority<2、monthsDisabled>12、isPartTime)合成 isNotEligableForDisability()。
4. 重点层:卫语句
条件表达式有两种风格:两个分支都是正常行为,或者一支正常、一支异常。作者的原则是这两种风格「应该通过代码表现出来」:前者用 if-else,后者用卫语句(guard clause)——罕见条件单独检查、立刻返回6。卫语句的精髓是「给某一条分支以特别的重视」:if-else 对两支一视同仁,卫语句则告诉读者「这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出」7。
原书走查很直观:payAmount 函数先嵌套两层 if 处理离职(isSeparated,金额 0、原因码 "SEP")和退休(isRetired,"RET"),核心算薪逻辑被压到第三层缩进里。改成卫语句后,两行 if (...) return {...}; 站在函数门口,核心逻辑回到第一层。顺带终结一个教条:「每个函数只能有一个入口和一个出口」——单一出口在今天的语言里「其实不是那么有用」,「保持代码清晰才是最关键的」8。多个卫语句指向同一结果时,先合并再提炼。
5. 结构层(一):以多态取代条件表达式
诊断问题:分支是不是按类型裂开的?两个征兆——「有好几个函数都有基于类型代码的switch语句」,或者「有一个基础逻辑,在其上又有一些变体」9。前者每类一个子类;后者基础逻辑放超类,变体放子类「着重强调与基础逻辑的差异」。
主走查(鸟例,数字均为原书示例): 一段程序给鸟算羽毛(plumage)和飞速(airSpeedVelocity),两个函数里各有一个 switch 按鸟型分支:欧洲燕(EuropeanSwallow)羽毛 "average"、飞速 35;非洲燕(AfricanSwallow)羽毛由椰子数决定(>2 则 "tired"),飞速 40 − 2×椰子数;挪威蓝鹦(NorwegianBlueParrot)羽毛看电压(>100 则 "scorched"),飞速看是否被钉住(钉住 0,否则 10 + 电压/10)10。症状正是征兆一:类型分支在两个函数里重复,加一种鸟要同时改两处。改造:每型建一个子类,plumage/airSpeedVelocity 下沉为子类方法,超类留 "unknown"/抛异常的默认;用一个工厂函数按 bird.type 造子类。从此加一种鸟=加一个子类,两处 switch 同时消失——这是第 01 章账单例子(悲剧/喜剧计算器)的同一招在更复杂形态上的重演。
但多态不是默认答案,作者的反调唱得明确:「我曾经遇到有人争论说所有条件逻辑都应该用多态取代。我不赞同这种观点」——他的大部分条件逻辑用普通 if/else 和 switch 就好,「并不需要劳师动众地引入多态」;多态只对「如前所述的复杂条件逻辑」是「有力工具」11。这也呼应第 03 章的立场变化:第 1 版坏味道叫「switch 语句」,第 2 版改成「重复的 switch」——该消灭的是重复,不是 switch 本身。