跳到主要内容

重复是税,多余是债 — DRY、KISS、YAGNI 的因果链

这一章讲三件事: 复制粘贴为什么是「税」——每次修改都要再交一遍; 抽象化怎么把 N 处收敛成一处、以及哪类重复收敛不掉; 「为将来写」的代码为什么几乎总是亏的。 主走查:一段被复制了两次的常量判断,看一次修改在三种代码里分别要动几处。

1. 起点:代码会自己长乱

第 01 章说过代码必然被改。这一章从被改的第一手后果讲起:随意修改会让代码越来越复杂。 作者的推演是一条滑坡:复杂 → 难读难改 → 强改降低质量 → 发布时间失控 → 更强改 → 「代码就会变得没有人能看懂,最终腐化为无用之物」1

所以简洁不是洁癖,是刹车。作者把它列为编程的指南针: 「尽可能将多余的、过剩的要素从代码中剔除」2

「多余的复杂性」这个说法要先掰开。第 3 章思想篇里作者给了一个区分: 复杂有两种,一种来自问题本身(要算税,就得懂税法——这种复杂性有价值,砍不掉), 另一种是修改过程留下的痕迹——补丁摞补丁、临时判断越积越多。 「多余的复杂性」专指后者:它不反映任何目标,「不具有任何价值」, 只会阻碍阅读、提高修改难度3。本书整章都在教你怎么不让这种复杂性进门。

2. DRY:重复是税,每次修改交一遍

这一节回答:同一段逻辑出现两次,代价到底是什么——不是「难看」,是一笔持续征收的税。

DRY(Don't Repeat Yourself,不要重复)的主张一句话:不可以重复写相同的代码4。 作者列了四个重复的来源5:

  1. 整段复制粘贴——最常见,把一个处理抄到另一处去用;
  2. 复制条件分支再单改分支体——if/else 的条件相同、每个分支里的动作不同, 一复制,同样的条件散落多处;
  3. 把数值直接写死在代码里(这种写死的数值叫常量的反面——常量指起了名字、集中定义的固定值)—— 意义相同的数字出现在多处;
  4. 翻译型注释——把代码翻成大白话的注释,和代码重复说一件事。

主走查:一次修改,三种代码各动几处

拿一个最小的例子走一遍修改流程(例子是我们编的,数字是演示值;原书用的是同样的结构6): 电商系统里「会员积分满 16 分可以兑换」,这个判断 16 出现在代码里。

写法 A:16 直接写在两处
函数甲: if (积分 < 16) 不可兑换
函数乙: if (积分 < 16) 不可兑换
改动:规则改成 20 分。
要做的:想起有两处 → 改两处。忘了乙?兑换规则悄悄不一致。

写法 B:16 写死,但出现了细微差别
函数甲: if (积分 < 16) 不可兑换
函数乙: if (积分 <= 16) 不可兑换 ← 谁顺手改的边界
改动:无法全局替换——先逐处判断「这一处的 16 是不是那条规则」,
再逐处决定改成什么。书里特别强调:有细微差别时
「理解的难度就会进一步增大」[^7]。

写法 C:常量「兑换门槛」定义一次,两处引用
改动:改一处定义,两处同时生效。
修改成本:1 处;且不存在「忘了改哪里」。

三种写法,同一个需求,修改成本是 2 处带风险 / N 处带判断 / 1 处零判断。 重复的税不在写的时候交,在每次修改的时候交——这正是它和「多写几行代码」的本质区别。

为什么重复这么毒:三个连锁困难

作者把重复的代价列成三级,一环扣一环7:

  1. 可读性下降:量变大、复杂度变高,「无法准确理解代码就无法确立修改方针」;
  2. 难以修改:每处都要判断「要不要一起改」,稍有不慎就漏;存在细微差别时更要逐处深读;
  3. 没有测试:重复代码大多是没经过测试的遗留代码——在没有测试的状态下改,新故障的可能性很大。

第二级里藏着一个更深的坑,值得单独说:「全部替换」是不安全的。 同样的代码出现在十处,可能只有七处需要跟着改——必须逐处读上下文判断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:

  1. 预想大多不成真。为扩展性提前写的代码,「可惜这些预想大多不会成真」—— 不成真就是白费时间;
  2. 残留的预埋代码会变成负资产。时间一过,没人记得它为什么存在, 「当初费时费力想出的设计就没有了用武之地,成了碍事的垃圾」。

第 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 编程艺术》,但「代码会越来越乱」的滑坡论证是作者的展开。

7. 边界与局限

  • 抽象化不是免费的。 第 02 章讲过:把「偶然长得一样」的两段代码强行合并, 会制造假依赖——两处各自演进时又得拆开。原书在「语境」一节明确警告过这类「偶然的共同」, 本章的裁决适用:先确认两处修改理由相同,再合并(判断标准见第 04 章变动率)。
  • YAGNI 有对手盘。 原书在「可扩展性原则」里承认设计要留扩展余地(第 08 章§5 的对照表收了这条), 两者的分界线作者给了:YAGNI 禁止预装功能,不禁止给未来的修改留好拆装的接口—— 「可扩展性并不是添加非必要功能的免罪符」32
  • 「三处收拢成一处」依赖工具。 阻抗失配的自动生成方案(ORM 类工具)在书里只是一句设想; 实践中该方案本身也会引入新的复杂度,书未展开。
  • 简洁与交流可能冲突。 思想篇承认:过度简洁会让代码难懂,此时「牺牲一部分简洁性, 把交流放在优先的位置」33——可读性仍是最高优先级(第 02 章的结论在这里又一次压舱)。

8. 可带走的

  1. 复制粘贴不是省时间,是把时间记在未来的账上——每次修改都要为每一处副本付一次判断;
  2. 重复的修改无法全局替换:有细微差别就必须逐处读上下文——这是重复比「多几行」毒得多的原因;
  3. 三步抽象化:重复的处理 → 函数;重复的数值 → 常量;重复的条件 → 收拢成一处;
  4. 分清两类重复:人为的(懒)→ 消灭;结构性的(阻抗失配)→ 集中一处、其余自动生成;
  5. 「没有测试的代码就是遗留代码」——接手任何代码,先补测试再改;
  6. 三个多余的入口:新技术手痒、以备将来之需、擅自替用户加需求——每个都有 KISS 原则挡着;
  7. YAGNI 的底气:预想大多不成真;简单方案往往比通用方案更好改——不为不可知的未来预装;
  8. 「更优解」心态:这三条原则都允许例外,例外要用「当前的修改成本」来辩护,不用「感觉」。

9. 原文地图

主题原书章原文位置
代码越来越乱、腐化为无用之物2.1 KISStext/06-ch02.txt:21(搜「越来越没有秩序」) · text/06-ch02.txt:25(搜「腐化为无用之物」)
简洁是指南针2.1 KISStext/06-ch02.txt:36(搜「指南针」)
多余的复杂性=修改的痕迹3.3 简洁text/09-ch03-03-3-3.txt:10(搜「遗留下来的痕迹」) · text/09-ch03-03-3-3.txt:16(搜「祸根」)
玉与石3.3 简洁text/09-ch03-03-3-3.txt:21(搜「玉」)
DRY 主张、重复来源2.2 DRYtext/06-ch02.txt:111(搜「不可以重复」) · text/06-ch02.txt:117(搜「Proc 2」) · text/06-ch02.txt:139(搜「16」)
翻译型注释也是重复2.2 DRYtext/06-ch02.txt:149(搜「翻译成母语的注释」)
修改遗漏与细微差别2.2 DRYtext/06-ch02.txt:167(搜「遗漏」) · text/06-ch02.txt:170(搜「细微差别」)
遗留代码没有测试2.2 DRYtext/06-ch02.txt:177(搜「遗留代码」)
抽象化与心理障碍2.2 DRYtext/06-ch02.txt:188(搜「抽象化操作」) · text/06-ch02.txt:205(搜「心理方面」)
没有商量余地2.2 DRYtext/06-ch02.txt:209(搜「没有商量的余地」)
阻抗失配、三处重复2.2 DRYtext/06-ch02.txt:243(搜「阻抗失配」) · text/06-ch02.txt:248(搜「交界处」)
遗留代码新定义、先测试再修改2.2 DRYtext/06-ch02.txt:283(搜「没有测试程序的代码」) · text/06-ch02.txt:296(搜「一定要先测试再修」)
OFOP 与标准化2.2 DRYtext/06-ch02.txt:265(搜「One Fact」)
单一引用点(写法 C 的原则名)3.20 单一引用点text/26-ch03-20.txt:8(搜「仅被声明和定义一次」) · text/26-ch03-20.txt:25(搜「单一赋值」)
YAGNI 主张、预想不成真2.3 YAGNItext/06-ch02.txt:322(搜「可能会用到」) · text/06-ch02.txt:333(搜「预想大多不会成真」) · text/06-ch02.txt:339(搜「碍事的垃圾」)
简单方案通用性更强2.3 YAGNItext/06-ch02.txt:348(搜「通用性更强」)
DTSTTCPW、想得太远2.3 YAGNItext/06-ch02.txt:86(搜「最简单」) · text/06-ch02.txt:369(搜「想得太远」)
KISS 三诱惑2.1 KISStext/06-ch02.txt:43(搜「新学会的技术」) · text/06-ch02.txt:50(搜「将来之需」) · text/06-ch02.txt:58(搜「擅自增加需求」)
炫耀与滚雪球2.1 KISStext/06-ch02.txt:46(搜「炫耀聪明才智」) · text/06-ch02.txt:63(搜「滚雪球」)
less is more、奥卡姆剃刀2.1 KISStext/06-ch02.txt:76(搜「少就是多」) · text/06-ch02.txt:85(搜「奥卡姆剃刀」)
重复最少化并入效应局部化3.6 重复最少化text/12-ch03-06-3-6.txt:14(搜「效应局部化」)
简约原则:不写大代码3.43 简约原则text/49-ch03-43.txt:6(搜「不写大代码」) · text/49-ch03-43.txt:9(搜「复杂指数」)

Footnotes

  1. 出处:「2.1 KISS」第 21 段(text/06-ch02.txt:21,搜「越来越没有秩序」)与第 25 段(text/06-ch02.txt:25,搜「腐化为无用之物」)。

  2. 出处:「2.1 KISS」第 36 段(text/06-ch02.txt:36,搜「指南针」)。

  3. 出处:「3.3 简洁」第 10 段(text/09-ch03-03-3-3.txt:10,搜「遗留下来的痕迹」)与第 16 段(text/09-ch03-03-3-3.txt:16,搜「祸根」)。

  4. 出处:「2.2 DRY」第 111 段(text/06-ch02.txt:111,搜「不可以重复」)。

  5. 出处:「2.2 DRY」第 112 段(text/06-ch02.txt:112,搜「复制粘贴」)、第 120 段(text/06-ch02.txt:120,搜「条件相同」)、第 133 段(text/06-ch02.txt:133,搜「常量写入代码」)、第 149 段(text/06-ch02.txt:149,搜「翻译成母语的注释」)。

  6. 出处:「2.2 DRY」第 139 段(text/06-ch02.txt:139,搜「16」)。原书例子是常量 16 在同一函数的两处判断里重复;本节把它扩成跨函数的走查场景,结构一致。

  7. 出处:「2.2 DRY」第 158 段(text/06-ch02.txt:158,搜「可读性下降」)、第 164 段(text/06-ch02.txt:164,搜「难以修改」)、第 175 段(text/06-ch02.txt:175,搜「没有测试」)。

  8. 出处:「2.2 DRY」第 167 段(text/06-ch02.txt:167,搜「遗漏」)。

  9. 出处:「2.2 DRY」第 188 段(text/06-ch02.txt:188,搜「抽象化操作」)。

  10. 出处:「3.20 单一引用点」第 8 段(text/26-ch03-20.txt:8,搜「仅被声明和定义一次」)与第 25 段(text/26-ch03-20.txt:25,搜「单一赋值」)。原文的展开:变量初始化后不再更改,「我们就不用再追踪变量值的变迁过程」;做法一节的名字就叫「单一赋值」。

  11. 出处:「2.2 DRY」第 194 段(text/06-ch02.txt:194,搜「减少了代码量」)。

  12. 出处:「2.2 DRY」第 205 段(text/06-ch02.txt:205,搜「心理方面」)与第 208 段(text/06-ch02.txt:208,搜「太麻烦」)。

  13. 出处:「2.2 DRY」第 209 段(text/06-ch02.txt:209,搜「没有商量的余地」)。

  14. 出处:「不得已的重复」第 243 段(text/06-ch02.txt:243,搜「阻抗失配」)。电学原义见第 246 段(text/06-ch02.txt:246,搜「反射」)。

  15. 出处:「不得已的重复」第 248 段(text/06-ch02.txt:248,搜「交界处」)。

  16. 出处:「不得已的重复」第 254 段(text/06-ch02.txt:254,搜「集中存放在一处」)。

  17. 出处:「遗留代码」第 283 段(text/06-ch02.txt:283,搜「没有测试程序的代码」)与第 288 段(text/06-ch02.txt:288,搜「垃圾代码」)。 2

  18. 出处:「遗留代码」第 296 段(text/06-ch02.txt:296,搜「一定要先测试再修」)。

  19. 出处:「OFOP」第 265 段(text/06-ch02.txt:265,搜「One Fact」)与第 270 段(text/06-ch02.txt:270,搜「标准化」)。

  20. 出处:「2.3 YAGNI」第 322 段(text/06-ch02.txt:322,搜「可能会用到」)。

  21. 出处:「2.3 YAGNI」第 333 段(text/06-ch02.txt:333,搜「预想大多不会成真」)与第 339 段(text/06-ch02.txt:339,搜「碍事的垃圾」)。

  22. 出处:「2.1 KISS」第 54 段(text/06-ch02.txt:54,搜「现在用不到的东西」)与第 55 段(text/06-ch02.txt:55,搜「将来也用不到」)。

  23. 出处:「2.3 YAGNI」第 348 段(text/06-ch02.txt:348,搜「通用性更强」)。

  24. 出处:「DTSTTCPW」第 86 段(text/06-ch02.txt:86,搜「最简单」)。

  25. 出处:「DTSTTCPW」第 369 段(text/06-ch02.txt:369,搜「想得太远」)。

  26. 出处:「2.1 KISS」第 15 段(text/06-ch02.txt:15,搜「简洁性」)。

  27. 出处:「2.1 KISS」第 43 段(text/06-ch02.txt:43,搜「新学会的技术」)、第 50 段(text/06-ch02.txt:50,搜「将来之需」)、第 58 段(text/06-ch02.txt:58,搜「擅自增加需求」)。

  28. 出处:「2.1 KISS」第 62 段(text/06-ch02.txt:62,搜「需求是由用户决定的」)与第 63 段(text/06-ch02.txt:63,搜「滚雪球」)。

  29. 出处:「3.3 简洁」第 21 段(text/09-ch03-03-3-3.txt:21,搜「玉」)。

  30. 出处:「3.6 重复最少化」第 14 段(text/12-ch03-06-3-6.txt:14,搜「效应局部化」)与第 14 段(text/12-ch03-06-3-6.txt:14,搜「修改成本」)。

  31. 出处:「3.43 简约原则」第 6 段(text/49-ch03-43.txt:6,搜「不写大代码」)与第 24 段(text/49-ch03-43.txt:24,搜「分割」)。

  32. 出处:「3.54 可扩展性原则」(text/60-ch03-54.txt:28,搜「免罪符」)。

  33. 出处:「3.3 简洁」第 32 段(text/09-ch03-03-3-3.txt:32,搜「优先的位置」)。