重复是税,多余是债 — DRY、KISS、YAGNI 的因果链
这一章讲三件事: 复制粘贴为什么是「税」——每次修改都要再交一遍; 抽象化怎么把 N 处收敛成一处、以及哪类重复收 敛不掉; 「为将来写」的代码为什么几乎总是亏的。 主走查:一段被复制了两次的常量判断,看一次修改在三种代码里分别要动几处。
1. 起点:代码会自己长乱
第 01 章说过代码必然被改。这一章从被改的第一手后果讲起:随意修改会让代码越来越复杂。 作者的推演是一条滑坡:复杂 → 难读难改 → 强改降低质量 → 发布时间失控 → 更强改 → 「代码就会变得没有人能看懂,最终腐化为无用之物」1。
所以简洁不是洁癖,是刹车。作者把它列为编程的指南针: 「尽可能将多余的、过剩的要素从代码中剔除」2。
「多余的复杂性」这个说法要先掰开。第 3 章思想篇里作者给了一个区分: 复杂有两种,一种来自问题本身(要算税,就得懂税法——这种复杂性有价值,砍不掉), 另一种是修改过程留下的痕迹——补丁摞补丁、临时判断越积越多。 「多余的复杂性」专指后者:它不反映任何目标,「不具有任何价值」, 只会阻碍阅读、提高修改难度3。本书整章都在教你怎么不让这种复杂性进 门。
2. DRY:重复是税,每次修改交一遍
这一节回答:同一段逻辑出现两次,代价到底是什么——不是「难看」,是一笔持续征收的税。
DRY(Don't Repeat Yourself,不要重复)的主张一句话:不可以重复写相同的代码4。 作者列了四个重复的来源5:
- 整段复制粘贴——最常见,把一个处理抄到另一处去用;
- 复制条件分支再单改分支体——
if/else的条件相同、每个分支里的动作不同, 一复制,同样的条件散落多处; - 把数值直接写死在代码里(这种写死的数值叫常量的反面——常量指起了名字、集中定义的固定值)—— 意义相同的数字出现在多处;
- 翻译型注释——把代码翻成大白话的注释,和代码重复说一件事。
主走查:一次修改,三种代码各动几处
拿一个最小的例子走一遍修改流程(例子是我们编的,数字是演示值;原书用的是同样的结构6): 电商系统里「会员积分满 16 分可以兑换」,这个判断 16 出现在代码里。
写法 A:16 直接写在两处
函数甲: if (积分 < 16) 不可兑换
函数乙: if (积分 < 16) 不可兑换
改动:规则改成 20 分。
要做的:想起有两处 → 改两处。忘了乙?兑换规则悄悄不一致。
写法 B:16 写死,但出现了细微差别
函数甲: if (积分 < 16) 不可兑换
函数乙: if (积分 <= 16) 不可兑换 ← 谁顺手改的边界
改动:无法全局替换——先逐处判断「这一处的 16 是不是那条规则」,
再逐处决定改成什么。书里特别强调:有细微差别时
「理解的难度就会进一步增大」[^7]。
写法 C:常量「兑换门槛」定义一次,两处引用
改动:改一处定义,两处同时生效。
修改成本:1 处;且不存在「忘了改哪里」。
三种写法,同一个需求,修改成本是 2 处带风险 / N 处带判断 / 1 处零判断。 重复的税不在写的时候交,在每次修改的时候交——这正是它和「多写几行代码」的本质区别。
为什么重复这么毒:三个连锁困难
作者把重复的代价列成三级,一环扣一环7:
- 可读性下降:量变大、复杂度变高,「无法准确理解代码就无法确立修改方针 」;
- 难以修改:每处都要判断「要不要一起改」,稍有不慎就漏;存在细微差别时更要逐处深读;
- 没有测试:重复代码大多是没经过测试的遗留代码——在没有测试的状态下改,新故障的可能性很大。
第二级里藏着一个更深的坑,值得单独说:「全部替换」是不安全的。 同样的代码出现在十处,可能只有七处需要跟着改——必须逐处读上下文判断8。 这意味着重复代码的修改无法机械化,每处都是一次人力判断。这就是「税」的计税方式。
解法:抽象化,把 N 收敛成 1
作者的解法叫抽象化:给重复的东西起名字、收拢到一处9——
- 重复的处理 → 命名成一个函数(或模块),各处改为调用它;
- 重复的数值 → 定义成常量,各处改为引用;
- 重复的条件 → 收拢成一处判断。
写法 C 就是抽象化之后的形态,它在原书的基本技法里有自己的名字:单一引用点—— 模块的各元素只声明和定义一次;落到数据上就是单一赋值:赋过值就不再改, 读的人不必再追踪一个值在程序里变过几轮10。 作者列了抽象化的收益:代码量变小、可读性变高、 修改只改一处、收拢后的东西还容易被下次复用11。
但作者同样诚实,抽象化有三笔当期成本12:转换要花时间、 改「本来能跑」的代码有改坏的风险、麻烦。他的裁决是明确的: 「避免重复这一点没有商量的余地」,从长远看利大于弊, 「这是历史总结出来的结论」13。翻译一下:抽象化是一笔当期支出、长期回本的买卖, 而它的对手(重复)是一笔当期免息、长期复利的债。
重复消不掉的一种:阻抗失配
有一类重复不是懒造成的,是两套体系结构不同造成的。作者借了电学的词叫阻抗失配: 素材之间阻抗不同,能量传不过去;软件里指结构不同的两个东西对接时的别扭14。
最典型的: 面向对象程序里的类和关系数据库(把数据组织成一张张二维表来存的数据库) 天生对不上——同一条「用户」信息,要在表定义、代码端的配置文件、源文件三处各写一遍15。 这种重复消不掉,但可以收敛:把信息集中放到一处,让其余两处自动生成16。 判断一条重复值不值得动手,这就是分界线:人为的重复 → 抽象化消灭; 结构性的重复 → 集中管理。
两个配套概念:遗留代码与数据库版的 DRY
遗留代码值得记住新定义。它过去指「以前留下的难懂的代码」; 现在的通行定义(作者采纳的就是它)是:没有测试程序的代码—— 「就算代码整齐,结构严谨,只要没有测试程序,那它就是遗留代码」17。 它在本章出现是因为因果相连:重复 → 难改 → 没人补测试 → 遗留代码。 破局顺序作者也给了:遇到遗留代码,先补测试,再动它—— 「不论用什么方法,一定要先测试再修改」18。(测试怎么写,本拆解不展开; 它的地位在第 06、07 章还会反复出现。)
数据库这边,DRY 有一条对应原则叫 OFOP(One Fact in One Place,一个事实只存一处): 代码不许重复,表里也不许重复存同一份数据,否则更新时就会顾此失彼; 数据库实现它靠「标准化」(把数据拆到不同的表、每件事只记一次的设计手法)19。
3. YAGNI:为将来写的代码,几乎总是白写
这一节回答:既然要为修改做准备,多写点「将来可能用」的东西总没错吧——为什么恰恰相反。
YAGNI(You Aren't Going to Need It,你不会需要它)的主张: 不能以「可能会用到」为动机写代码;需要的时候,写需要的代码20。
它的论证建立在两个经验事实上21:
- 预想大多不成真。为扩展性提前写的代码,「可惜这些预想大多不会成真」—— 不成真就是白费时间;
- 残留的预埋代码会变成负资产。时间一过,没人记得它为什么存在, 「当初费时费力想出的设计就没有了用武之地,成了碍事的垃圾」。
第 2 条和第 2 节的写法 B 是同一个机制:多余代码和重复代码一样, 抬高了每一次修改的阅读成本——读者要判断这块预埋的逻辑跟这次修改有没有关系。 KISS 一节里作者把「以备将来之需」列为三大多余来源之一,并给出判据: 「现在用不到的东西就不应该现在写,因为在大多数情况下,这些东西将来也用不到」22。
关键推理:简单方案往往比通用方案更好改
YAGNI 最有力的一步,是把「当下简单」和「未来好改」从一个矛盾变成一对盟友。 直觉上,通用方案(各种情况都能套)应该更经得起变化。作者给出相反的裁决: 选方案时看单纯性,不看通用性——「很多时候,简单方案的通用性更强: 即使需求增加,简单的代码也比通用的复杂代码更容易修改」23。
这个推理的根基在第 01 章:我们预测不了变化的方向。 通用方案赌的是「未来按我预埋的方向变」;简单方案不赌方向,把赌注留给「到时候改」—— 而改一段简单代码,比改一段复杂代码便宜得 多。为不可知的未来,最便宜的准备是不预装。
配套的一条 XP 口语叫 DTSTTCPW(Do The Simplest Thing That Could Possibly Work, 选可能行得通的最简单做法):「今天选简单的方案,明天需要时再花时间改」, 胜过「今天选复杂的方案,结果没派上用场」24。 作者也承认这条执行起来难:「人们很难不去思考明天、下周,甚至下个月要写的代码」—— 所以每条都提醒:想得太远,只会增加代码的复杂度25。
4. KISS 的三个日常诱惑:多余从哪进来
这一节回答:代码里的多余要素,是哪几种心理放进来的。
KISS(Keep It Simple, Stupid;或 Keep It Short and Simple,保持简洁)比 YAGNI 更日常: 不管写新代码还是修故障,都优先保证简洁26。 作者点名的三个诱惑,每个都是「看似勤奋,实则加税」27:
| 诱惑 | 心理 | 代价 |
|---|---|---|
| 想用刚学会的新技术 | 学了就想练手 | 无谓的复杂度;「代码并不是用来炫耀聪明才智的」 |
| 以备将来之需 | 趁现在写下,以后省事 | 大多数情况将来用不到(YAGNI 的领地) |
| 擅自增加需求 | 不问用户,自己补全 | 维护时间「像滚雪球一样增加」——需求是用户决定的 |
第三条最值得展开:程序员没有加需求的权力。用户没要的功能,写上了也要维护, 而维护的那笔税是整个团队交的28。
5. 思想篇的浓缩版:简洁与重复的分工
原书第 3 章把同样的道理用更普适的语言再说了一遍,合到本章收尾:
- 「简洁」被定义为消除了多余的复杂性之后的状态;作者给了一个动手意象: 把代码里的「玉」(本质)与「石」(杂质)分清,玉放在显眼处,别让石混进来29;
- 「重复最少化」被并进效应局部化(第 04 章的主题):重复的真正害处, 是让「修改的影响」散布到多处——每处重复都是一个必须排查的引爆点30;
- UNIX 思想的「简约原则」给了最短版本:不写大代码——「大」既指代码量大,又指内部纠缠的程度; 代码变大就尽快分割31。
三条原则(DRY/KISS/YAGNI)在原书里各占一节,但在因果上是同一枚硬币: DRY 管已有的重复,KISS 管正在引入的多余,YAGNI 管尚未发生的预支。 三者共同守住同一条账本:让未来的每一次修改,只面对必要的东西。
6. 作者的判断与证据
书里给了证据的:
- 阻抗失配的三处重复(表定义/配置文件/源文件)是工程现场的真实结构,作者给出了收敛方案;
- 「遗留代码=没有测试程序的代码」引自测试领域的通行重定义,作者给出采纳理由17。
作者的推测与转述,要分开看的:
- 「重复的代码大多没有测试」是经验判断,未给统计;
- 「设计模式是防止重复思考的手法」是作者对 GoF 的个人解读,带演化解彩;
- KISS 出处栏引的是《UNIX 编程艺术》,但「代码会越来越乱」的滑坡论证是作者的展开。