跳到主要内容

条件逻辑:分支的形状决定手法

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 本身

6. 结构层(二):引入特例

诊断问题:代码库里是不是到处在防同一个特殊值?「如果我发现代码库中有多处以同样方式应对同一个特殊值,我就会想要把这个处理逻辑收拢到一处」12。手法是建一个特例对象:它用 isSpecial 之类的属性应答「你是那个特殊情况吗」,并对共用的询问返回固定答案——从此大部分检查代码变成一次普通调用。特例的载体可以很轻:只需要读数据时,一个所有值预填好的字面量对象就够;有行为时才建类;也可以由封装类返回,或在变换中插入。

null 是最常见的特例值,所以这个模式常被叫作 Null 对象模式——作者的表述是「Null对象是特例的一种特例」13。原书例子:场所(site)大多有顾客,但有些场所顾客未知(customer 字段是字符串 "unknown"),于是每个客户端都得先问一句「是不是 unknown」再决定怎么显示;引入特例对象后,"unknown" 顾客自己会回答「我是未知的、我的计费计划是基本的」。

7. 契约层:引入断言

最后一种条件逻辑不分支,只声明:「只有当某个条件为真时,该段代码才能正常运行」——平方根只对正数有定义、这组字段至少一个非 null。这类假设通常没写在代码里,「你必须阅读整个算法才能看出」14。断言把它写成一行代码:条件恒为真,失败说明程序员犯了错,失败不许被任何地方捕捉,甚至可以在编译期整个关掉——程序行为必须与有没有断言无关。

作者给断言的定位偏交流而非排错:「断言是一种很有价值的交流形式——它们告诉阅读者,程序在执行到这一点时,对当前状态做了何种假设」;测试网普及后断言的调试价值下降了,交流价值不掉15。第 05 章迁移式改签名时用断言抓漏传参数、第 08 章删派生字段前用断言验证「能即时算」,都是这个手法在别处的伏兵。原则一句:「加入断言」永远行为保持——它必须不可能改变程序的可观察行为。

8. 作者的判断与证据

判断性质
多态只该用于「复杂条件逻辑」,反对一刀切立场声明,与第 1 版「switch 语句」味道的改名互证11
卫语句与 if-else 表达两种不同的分支关系风格论:代码应表现出分支的相对重要性7
单一出口规则没什么用对传统教条的明确否定,判据换成「清晰」8
断言的价值在交流,测试降低其调试价值但不动摇交流价值对工具演进的诚实评估15

判断(我们的,不是书里的): 六个手法连起来看,条件逻辑的重构有一个隐藏的总原则——让分支结构与领域结构同构:领域里「一次检查的多个理由」就该是一行合并条件,「一条正常路径加若干异常」就该是卫语句,「按物种分裂的行为」就该是子类。坏味道出现在领域结构与代码结构错位的时刻。如果错,会错在: 如果某个分支结构本身就是领域规则(比如保险公司真的按「检查表」逐条核保),那么一长串朴素的 if 反而是忠实表达,强行同构化反而失真。

9. 边界与局限

  • 多态手法的前提是能建子类体系:类型码值来自外部数据且会中途变化时,需要先把它包装成对象(具体步骤在第 11 章)。
  • 特例对象一旦开始承载行为,就有一个类的维护成本;只读场景的字面量对象是免费的,该省则省。
  • 断言在运行时默认关闭的语言里只剩文档价值;把业务校验(要阻止用户输入)写成断言是用错了工具——断言防的是程序员的错,不是用户的错(补充,不在书里,来自通用知识)。
  • 合并条件要求各检查无副作用,有副作用先做查询/修改分离。

10. 可带走的

  1. 条件里读得出「发生了什么」却读不出「为什么」,就把条件和分支提炼成函数。
  2. 检查各异、结果相同→合并;合并后顺手提炼成「为什么」函数。
  3. 罕见分支单独检查、立刻返回;别让异常路径把正常路径挤进第三层缩进。
  4. 「单一出口」让位于清晰。
  5. 同一类型分支出现在多个函数里→子类;只出现一次的 switch 留着没问题。
  6. 到处防同一个特殊值→特例对象;null 只是它最出名的案例。
  7. 「这里假设 X 恒为真」写下来就是断言;断言永不改变行为。
  8. 想消灭所有 if/switch 之前,先确认它真的「重复」。

11. 原文地图

主题原书章原文位置
威力与复杂度第10章 开篇text/18-ch10.txt:3(搜「复杂度也大多来自条件逻辑」)
知其然不知其所以然第10章 §10.1text/18-ch10.txt:30(搜「弄不清楚为什么会发生」)
提炼的应用场景第10章 §10.1text/18-ch10.txt:38(搜「一个应用场景」)
合并的归并口诀第10章 §10.2text/18-ch10.txt:152(搜「逻辑或来合并」) · text/18-ch10.txt:138(搜「为什么这样」)
卫语句两种风格第10章 §10.3text/18-ch10.txt:242(搜「两种风格」) · text/18-ch10.txt:247(搜「卫语句」)
给分支不同的重视第10章 §10.3text/18-ch10.txt:250(搜「不是本函数的核心逻辑所关心」)
单一出口的终结第10章 §10.3text/18-ch10.txt:255(搜「单一出口」)
多态的征兆第10章 §10.4text/18-ch10.txt:457(搜「switch语句」) · text/18-ch10.txt:462(搜「着重强调与基础逻辑的差异」)
鸟例第10章 §10.4text/18-ch10.txt:494(搜「plumage」) · text/18-ch10.txt:491(搜「airSpeedVelocity」)
反对一刀切多态第10章 §10.4text/18-ch10.txt:465(搜「我不赞同这种观点」)
特例收拢第10章 §10.5text/18-ch10.txt:1188(搜「收拢到一处」)
Null 是特例的特例第10章 §10.5text/18-ch10.txt:1199(搜「特例的一种特例」)
假设藏在算法里第10章 §10.6text/18-ch10.txt:1769(搜「阅读整个算法才能看出」)
断言是交流第10章 §10.6text/18-ch10.txt:1776(搜「交流形式」)

Footnotes

  1. 出处:「简化条件逻辑」第 3 段(text/18-ch10.txt:3,搜「复杂度也大多来自条件逻辑」)。

  2. 出处:「简化条件逻辑」第 30 段(text/18-ch10.txt:30,搜「弄不清楚为什么会发生」);「最常导致复杂度上升」在第 27 段(text/18-ch10.txt:27,搜「最常导致复杂度上升」)。

  3. 出处:「简化条件逻辑」第 38 段(text/18-ch10.txt:38,搜「一个应用场景」);夏季价格例子在第 50-53 段(text/18-ch10.txt:50,搜「summerStart」)。

  4. 出处:「简化条件逻辑」第 152 段(text/18-ch10.txt:152,搜「逻辑或来合并」)。

  5. 出处:「简化条件逻辑」第 138 段(text/18-ch10.txt:138,搜「为什么这样」)。

  6. 出处:「简化条件逻辑」第 247 段(text/18-ch10.txt:247,搜「卫语句」)。

  7. 出处:「简化条件逻辑」第 250 段(text/18-ch10.txt:250,搜「不是本函数的核心逻辑所关心」)。 2

  8. 出处:「简化条件逻辑」第 256 段(text/18-ch10.txt:256,搜「更清楚易读」);「单一出口」在第 255 段(text/18-ch10.txt:255,搜「单一出口」)。 2

  9. 出处:「简化条件逻辑」第 457 段(text/18-ch10.txt:457,搜「switch语句」)与第 462 段(text/18-ch10.txt:462,搜「着重强调与基础逻辑的差异」)。

  10. 出处:「简化条件逻辑」第 437 段(text/18-ch10.txt:437,搜「plumage」)与第 491 段(text/18-ch10.txt:491,搜「airSpeedVelocity」)。

  11. 出处:「简化条件逻辑」第 465 段(text/18-ch10.txt:465,搜「我不赞同这种观点」)。 2

  12. 出处:「简化条件逻辑」第 1188 段(text/18-ch10.txt:1188,搜「收拢到一处」)。

  13. 出处:「简化条件逻辑」第 1199 段(text/18-ch10.txt:1199,搜「特例的一种特例」)。

  14. 出处:「简化条件逻辑」第 1769 段(text/18-ch10.txt:1769,搜「阅读整个算法才能看出」)。

  15. 出处:「简化条件逻辑」第 1776 段(text/18-ch10.txt:1776,搜「交流形式」)与第 1779 段(text/18-ch10.txt:1779,搜「交流方面的价值」)。 2