怎么给一个模块定边界
不讲"高内聚低耦合"这类词(说的是"每块只干一件事、块和块别绑在一起";这类词你大概听过,但听完还是不知道明天该动哪一行)。这一篇只干一件事:教你在一个已经乱掉的项目上,划出"这一块归谁管、别人只能从哪几个口子进它"——然后验证你划对了没有。重点是从乱代码出发,不是从零设计。
一个词先说清楚:"模块"就是"一块能单独拿出来说的东西"(可能是一个文件、一个页面上的东西、一个动作,或一个后台服务,§02 会展开)。整篇只需要这一个词加一个"口子",别的术语一个都不用记。
01速览、起手动作、以及这篇假设你有什么
读完这一篇,你能做到一件事:拿你手上任意一个已经乱掉的项目,挑出一块该收口的东西,把它变成"负责一件事、别人只从一个口子进"的样子——而且知道怎么验证你没收错、没收过头。
① 边界不是一个词,是两个问题的答案:这一块负责什么?别人从哪几个口子用它?两句话都答得出来,它就有边界;答不出来,就没有。
② 判断法只有一句:"这东西,别的地方从哪几个口子用它?"——答不上来 = 它没有边界。注意是数"口子的种类",不是数"被引用的次数"(这两者的区别是全文最容易搞错的一处,见 §04)。
③ 从乱代码出发,顺序是反的。不是"先想清楚再动手",而是:先钉住它现在的样子 → 再把口子收窄 → 里面慢慢变干净。而且一次只动一块。
"口子"到底指什么:别人找它的时候,必须穿过的那几样东西——它叫什么名字、你递给它什么、它还你什么、用它的先后顺序、出错时它会怎样。§02 会用一家饭馆把这件事讲透。这篇里你只要记住这一个词就够,不需要任何别的术语。
如果你只有 5 分钟
只看两处:§04 那个判断法(怎么数口子),和 §05 的动作一、动作二(怎么选一块、怎么在动它之前先钉住它)。其余部分是你真动手那天回来查的。
起手动作:读完 60 秒内你要做的第一件事
不要读代码(你不读代码),也不要先想"我的项目该怎么整理结构"。你就打开一个项目,把下面这段话原样发给你的 AI(这段要发给 AI 的话,下文都叫"提示词"——就是你要打给 AI 的那段字):
# 起手提示词(原样复制,把"这个项目"换成你的项目文件夹)
看一遍这个项目。列出被最多地方用到的 5 样东西。
对每一样,写清楚三件事:
1) 它是什么(一句话,用日常话,不要用专业词)
2) 哪些文件用到它
3) 用到它的每一处,各自从它身上"拿了什么"
(是整块拿去用?只拿走里面某一项内容?只调用了它的某个动作?还是依赖它的文件名/格式?)
最后告诉我:这 5 样里,哪一样的"用法种类"最多、最杂。
你只需要看最后那一句的答案。那一样东西,就是你今天要划边界的那一块。(为什么是"用法种类最多"而不是"被用得最多",§04 会讲。)
必须有①:一个已经存在的项目文件夹。不需要能跑起来,不需要你懂里面任何一行。你手上那十几个半途停下的项目,随便挑一个都行。但必须是已经写了一些东西的——空文件夹不行,这一篇讲的是"从已有的乱代码里划",没有代码就没得划。
必须有②:一个能读整个文件夹的 AI。Claude Code、Codex 这类能打开整个项目目录的工具。只会在聊天框里贴代码的那种不行——它能看见一行,看不见"谁用了谁"。
最好有③:一张"哪几块 / 谁用谁"的粗略清单。这篇假设你已经看过 01《怎么给一个已经成型的项目画结构图》,手上有一张图。为什么它重要:看不见现状就划边界,等于闭着眼睛改结构——你会把"其实被三处依赖的东西"当成"没人用的东西"删掉。
没有 ③ 也能读这篇,但要先补 10 分钟。补法在 §04 末尾的"没有图也能开始",那里有一条提示词,能把最小版的那张图直接问出来。先跑那条,再回来。
这篇不适合谁
放在这里说,不占开头的位置:如果你手上还没有任何"写了一半、还想继续改"的项目,这一篇你读完也用不上——它是给"手上有一堆烂摊子、还得继续在上面加东西"的人写的。另外,如果你现在正打算把某个项目推倒重写,也不用划边界:推倒重来是另一条路(而且多半更贵,见 §08)。
02边界长什么样:两句话,不是一个词
先破一个误会。边界不是一个你能"设计"出来的东西,也不是"把东西分分层""让两块互不依赖""设计一套接口"这类说法(接口就是"口子"的正式叫法;这些说法各自都在讲同一件事,只是换了词——§10 有一张对照表)。它是一个能被你肉眼验证的状态——验证方式就是那两句话。
先说"一块"是什么
这篇里说的"一块",不一定是别人嘴里那个"模块"(模块:一块能单独拿出来说的东西)。它可能是:
- 一个文件(比如网页里管"导航栏"的那个文件)
- 一个页面上的东西(比如首页那排文章卡片)
- 一个能被单独调用的动作(比如"把文章发布出去"这一整套)
- 一个后台服务(比如专门管用户登录的那个)
判断标准只有一条:你能把它当成一个东西来说吗?"这块管的是文章卡片"——能这么说,它就能是一块。你不需要先学会技术上的分类,只要认出"这一块"就行。
用一家店来理解"边界"
你见过的小饭馆,几乎都有边界,而且你从来没觉得那是个"设计":
顾客只知道四件事:从哪个门进、怎么点、怎么给钱、怎么拿走。这四件事就是这家店的口子。
后厨里谁切菜、用几口锅、先炒哪个——顾客不知道,也没人问。所以后厨换了主厨,顾客第二天照样来。
顾客要自己进后厨拿酱油、自己找碗、自己开火。每个人进后厨拿的东西还不一样:有人拿酱油,有人拿锅,有人把火关了。
结果:换一口锅,三个老顾客都吃不上饭了。而且没人觉得是自己的错——他们只是"照以前的习惯拿了东西"。
把这张图搬到代码上,"口子"具体是什么?就是别人找它的时候,必须穿过的那几样东西。包括:
- 它叫什么名字(别人靠名字找到它)
- 你递给它什么、它还给你什么
- 用它的先后顺序(必须先做 A 再做 B 这类规矩)
- 出错的时候它会怎样(是直接停,还是还你一个"失败")
这四样加起来,就是这个口子的全部。其中任何一样变了,用到它的地方就可能坏。换句话说——"口子"就是你改它之前必须通知的那些人。这句定义比什么术语都好用,后面全靠它。
所以"有边界"就是三个状态
| 状态 | 长什么样 | 你怎么一眼看出来 |
|---|---|---|
| 说得清它负责什么 | 一句话:这块管文章卡片 | 如果你要用"它管卡片、顺便也管排序、也管日期格式、也管那个报错提示"才能说完整,它就没有边界——它是一团 |
| 所有人都从同样的一两个口子进 | 三个用它的人,都走同一个入口 | 如果三个人进的方式各不相同(一个拿整块、一个只改里面一项内容、一个依赖它的文件名),那它没有边界 |
| 改里面,外面不用知道 | 你换了里面的做法,外面三处一行都不用改 | 这是最实用的验法:你改完,需要通知几个人?答案是 0,就有边界 |
饭馆的门是物理固定的,你开业那天门就在那儿了,改不了。代码里的口子是你自己划的,划错了可以重划——这既是好事(你能救),也是坏事(所以"没人拦着你乱划")。
还有一处不一样:饭馆通常只有一个门。代码里的一"块"常常需要两三个口子,因为"只看不拿"和"要改东西"经常得分开——让一个只想读的人也能改,是很多麻烦的开头。
03没有边界,你已经在付什么钱
你可能觉得"边界"是个讲究人的事。不是。没有边界,你此刻正在为它付四种账,而且每一笔你都以为是自己的问题。
你让 AI 改个文案、调个顺序,它改完了,别的地方却坏了。这不是 AI 笨。是这一处的"里面"被好几个地方同时伸着手。
上个星期修过的那个问题,这星期在别的地方又冒出来。因为同一条规则被三个地方各自写了一遍——你只修了其中一处。
想在卡片上加个"置顶"标记,结果发现要改五个文件。这就是那句老话的日常版:修一个小问题要动三处以上。
这一条对你最贵。你付了 AI 的钱,却只敢让它做"加个按钮"这种不痛不痒的事——因为你不知道它会碰到什么,也不知道碰坏了你能不能发现。
这四笔账有一个共同的来源
同一份东西,被好几处同时改。这正好是你如果做过 01(画结构图)时会画出的第三张表——"谁在改同一份数据"。那张表上连到同一个点最多的那些线,就是这篇文章要处理的那些块。
你有过这种项目吧:代码写得很整齐,缩进漂亮,文件名也规规矩矩,但就是一改就出事。反过来也有:代码乱得像垃圾堆,但你从来只改那一个地方,改了两百次都没事。
所以不要拿"看起来乱不乱"当判断依据。边界只跟一个数字有关:改一件小事,要动几处。
这件事不是新问题:1972 年就有人给出了答案
这正是开头那个"1972"的来历。那一年,一位叫 David Parnas 的人写了一篇论文,专门回答"该怎么把一个系统拆成几块"(出处见文末)。
他的结论跟直觉相反:不要按"先做什么、后做什么"来拆,也不要按"哪几块看起来像一类"来拆。要按"哪些决定以后最可能被改"来拆——把最可能变的那部分收进一块里,只留一个口子露在外面。(他的原话大意是:每个模块负责把一个"以后最可能变的设计决定"藏起来,不让别的模块知道;而它露出来的那个"接口",要尽量少透露里面怎么做的。本文说的"口子",就是他说的"接口"。)
五十多年过去,这条结论没变过——你手上那些项目犯的毛病,也还是同一个。
04判断法:这东西别的地方从哪几个口子用它?
这是全文唯一一个你需要背下来的东西。把它写在一张纸上贴显示器上,比记住任何术语都有用。
先看你会得到哪种答案
| 你的答案 | 说明什么 | 接下来做什么 |
|---|---|---|
| "就一个口子,别的地方都从这一处用它" | 有边界。哪怕它代码很乱、很长、很丑,它也是有边界的 | 先别动它。它是你项目里的好资产,去处理别的块 |
| "有好几个口子,但它们长得差不多" | 半有边界。口子该合并成一个 | 这是最好做的一种:把几个相似的口子并成一个,外面改动很小 |
| "别的地方是直接伸手进去拿里面的东西" | 没有边界。最常见的那种乱,也是账单①的来源 | 这是 §05 要处理的主要对象 |
| "答不上来,我得去搜一下才知道谁在用它" | 也没有边界——而且更严重:连你自己都不知道谁在用它 | 先别改。先跑下面的提示词,把答案问出来 |
最后一行值得停一下。很多人以为"没边界"的意思是"结构不好"。不是。没边界的意思是"没人知道它和谁连着"——包括你,包括 AI。在这种状态下改它,不是勇敢,是赌。
最容易搞错的一处:数"种类",不是数"次数"
这是全文最值钱的一句区分,请慢一点读。
某个"算价格"的东西,项目里 40 个地方用到它。但它们全都是同一种用法:递进去一个购物车,拿回来一个数字。
口子只有一个。你完全可以在里面大改(换算法、换数据结构),40 处一行都不用动——因为它们只知道"递进去、拿回来"。
同样一个东西,只有 3 个地方用它。但是:第一处直接改它里面的某一项内容;第二处依赖它内部各项的排列顺序;第三处依赖它的文件名。
口子有三个,而且三个都不一样。你动其中任何一处,都要单独通知一个人——这三个人还都不知道彼此存在。
所以"被引用得多"根本不是问题。被引用得多、但用法只有一种,是好事——那叫好用,所有人都在用同一个口子。真正的问题是"用法有几种"。一种 = 一个口子 = 有边界。三种 = 三个口子 = 你得改三处、通知三个人。
怎么数:一条可以原样复制的提示词
# 数口子(把 X 换成你上一步圈出来的那一样东西)
我要给 X 定边界,先要知道它现在有几个"口子"。
请在项目里找出所有"用到 X"的地方(除了定义 X 本身的那一处)。
对每一处,写清楚三件事:
1) 是哪个文件、大概哪一段
2) 它从 X 身上拿了什么:是整块拿去用?只读了其中某一项?改了其中某一项?
只调用了它某个动作?还是依赖了它的名字/顺序/文件位置?
3) 如果 X 改一个名字、或者把内部结构换个顺序,这一处会不会坏?
最后按第 2 问的答案分组(不是罗列),告诉我一共分成几组、每组有几个地方。
# 关键:它分出来的"组数",就是口子的数量。组数 > 2 就要收口。
怎么读这个结果:不要看它列了多少个地方,直接跳到最后一行的"分成几组"。几组就是几个口子。如果它只给你一个总数没分组,就回一句:"请按第 2 问的答案分组,不要给我总数。"
没有图也能开始(补 01 那条路)
如果你还没做过 01,手上没有那张图,用下面这条提示词,10 分钟能拿到这次需要的最小版:
# 最小版结构图(没做过 01 就跑这条)
看一遍这个项目,回答我三个问题,每个都尽量短:
1) 这个项目大致分成哪几块?每块用一句日常话说它在管什么。
2) 哪几块会用到别块?谁用谁?
3) 有没有哪一样东西,是好几块都在读、或者都在改的?列出来。
不要给我代码,不要给我建议,只要这三条的答案。
拿到第 3 问的答案之后,把里面"被好几块同时读或改"的那几样排个序——选其中你最常碰到的那一样,进 §05。
不是一张好看的图,也不是一个数字。是你终于能说出"改 X 会波及哪几处"。在第 3 问的答案出现之前,你改任何东西都只能靠猜;出现之后,你是知道。
05从一堆乱代码出发:四步
先把话说死:这一篇讲的不是从零设计。你手上的项目都是已经存在的、一半还跑着、你也不想推倒重来的。所以下面四步的顺序,跟教科书里的顺序是反的:
教科书:先想清楚这块该负责什么 → 设计好口子 → 再写。
你:先把口子收窄(外面改动最小)→ 再让里面慢慢变干净 → 最后才写"它负责什么"那句话。
为什么反着来才对:因为你的目标不是"得到一份好设计",是下一次改它的时候别出事。而下一周就要改,等不到你先想清楚。
一样是 §04 数出来的口子清单(哪几处、各自拿走了什么)。另一样是一个能跑起来的检查——如果不方便跑,至少要有"改完怎么知道没坏"的办法。第二样在动作二里造。
动作一:选出血点,不要选整个项目
你想划的那个东西,几乎一定是"整个项目太乱了"。那就一定做不完。所以第一步是砍范围。
不是挑最重要的,不是挑最乱的,是挑口子最多的那个。理由很实在:口子最多 = 你每改一次它就要通知最多的人 = 它正在花你最多的钱。
如果这样东西太大(比如"整个用户系统"),就往里再选一层——选它里面口子最多的那个小东西(比如"用户头像")。标准是:这一块的所有用法,你今天下午能一个个看完。
一行字:"这次要收口的是 ______,它现在有 __ 个口子。"这句话是你后面所有判断的锚——AI 提出"顺手也改改别的"时,你就看这一行。
动作二:先钉住它现在是什么样,再动它
这是最容易跳过、也最贵的一步。跳过它,前面省下的两小时,会在两周后以"不知道哪儿坏了"的形式还给你。
为什么必须先钉住
改结构的时候,最常发生的失败不是"改坏了",是"改坏了你不知道"。它可能三周后才在某个页面上露出来,那时候你已经改了十几个别的地方,根本不知道是谁干的。
所以要做的是一件很朴素的事:在动它之前,先把"它现在是什么样"记下来。记下来之后,你改完再验一遍,两次结论一样 = 你没改坏行为,只改了结构。
表征测试(characterization test)就是"只描述现状的检查"。拆开看:"表征"就是"描述它现在长什么样","测试"就是一段能被自动跑起来、跑完告诉你"一样 / 不一样"的小程序。
它最反直觉的一点:它不管对错。哪怕它记下来的行为是个毛病,也照样原样记下来——因为你现在的任务不是修毛病,是不让改动出意外。修毛病是另一件事,别混在一起(混在一起你就分不清是哪边出的问题)。
发明这个说法的人(Michael Feathers)自己写得更直白:"它的目的是记录你的系统实际的行为,而不是检查你希望它有的行为。"(出处见文末)
# 动作二:钉住现状(把 X 换成动作一里写下的那个名字)
请给 X 写一组"只描述现状"的检查。
要求:
1) 不要修正任何看起来像毛病的地方,现在是什么样就记什么样。
2) 至少覆盖 3 个正常情况 + 2 个边界情况(空的时候、特别多的时候、缺东西的时候)。
3) 每一条都写清楚:输入什么 → 现在的输出/结果是什么。
4) 写完后跑一遍,把结果给我看。全通过再继续。
5) 这一步不要改 X 里面的任何逻辑。
# 验收:它给你的每条,你都能读懂"输入什么、现在会怎样"。
# 读不懂就让它用日常话重写一遍——读不懂的检查,等于没有检查。
动作三:把口子收窄——加一道门,让旧的走法都改走这道门
现在才是真正的"划边界"。核心做法一句话:
不要先改里面。先在它外面加一个统一入口,规定"外面只能从这个入口拿东西",然后把旧的用法一个一个搬过来。
搬一个 → 跑一次动作二那组检查,结果还是老样子 → 再搬下一个。
只要守住这个节奏,你在任何一刻停下来,项目都还是能用的。这跟"让 AI 大改一通然后祈祷"是两件事。中间状态下新旧两条路同时存在,是正常的,不是失败。
# 动作三:开口子(先只要方案,不要它动手)
我要给 X 加一个统一的入口,让现在所有的用法都改走这个入口。
现在先不要改任何代码,只给我一份方案:
1) 你打算开的这个入口长什么样:外面能递什么进去、能拿什么出来。
要求:口子越少越好、越简单越好。
2) 现在每一处用法,改走这个入口之后分别写成什么样。一处一处列。
3) 哪些用法你搬不过去——因为它拿的是 X 内部的东西,不是入口能给的。这些单独列出来,
并说明它具体拿走了什么。
4) 你打算分几步搬?每步搬完之后,怎么验证行为没变。
# 关键看第 3 问:搬不过去的那些,就是你现在真正的口子清单。
# 它们没有消失,只是以前藏在你没看见的地方。
看完方案,先别急着让它动手。先自己看一眼第 3 问那几条——那几条决定了你这个入口开得对不对。如果第 3 问是空的("全都搬得过去"),那说明口子开得不错。如果第 3 问列了一大堆,往往说明这一块负责的事太多,该考虑拆成两块(这属于"今天先不做"的范畴,记下来就行)。
确认方案没问题之后,再说这句:
# 开始搬(一次一步)
按你上面的方案,只做第 1 步。
做完之后:① 跑一遍动作二那组检查,把结果给我 ② 告诉我这一步动了哪几处、其余没动的地方为什么不受影响。
不要顺手做第 2 步。
动作四:写一句话,贴上去
在你动过的那一块最上面写一行说明(写给人看的、机器不会执行的那种,技术上叫"注释"):
这一块负责:______。
外面只能通过这几个入口用它:______(列出那几个)。
这三行看着像走形式,其实它是全文最硬的验收标准:
- 第一句写不出来 → 这一块负责的事太多,它还是"一团",不是"一块"。
- 第二句写不出来,或者要写七八条 → 口子还没收窄,动作三没做完。
- 两句都写得出来,但你心里知道它不真 → 那说明还有人在从后门进。回去跑一次 §04 的数口子提示词,看看是不是漏了。
还有一个好处:这三行是你以后跟 AI 说话的锚。下次你说"改一下这一块",AI 读到这三行,就知道哪些地方可以动、哪些口子是约定好的不能乱碰。
每一步做完,怎么知道有没有白做
| 检查 | 怎么做(一分钟内) | 什么算过 |
|---|---|---|
| 口子少了吗 | 重跑一次 §04 那条数口子的提示词 | 组数比之前少。没少 = 你只是在旁边加了一层,没真的收口 |
| 行为变了吗 | 重跑动作二那组检查 | 结论和改之前逐条一样。有一条不一样就得说清楚为什么 |
| 那句话写得出吗 | 写动作四那两行 | 两行都写得出来,而且你信。写不出来就别往下走 |
| 真的少动了吗 | 拿一个真实的小改动试一次,数它动了几个地方 | 比上次同类改动少。这是唯一能证明"你赚到了"的检查 |
06一个具体例子:这个站自己的主页卡片
抽象讲完了,来看一个真的、很小的、你能自己去看的例子——就是你现在正在读的这个站(学习笔记)。它的主页上有一排文章卡片,每张卡片上有:标题、一句简介、难度标记、日期。
这一块,有三个地方在用它
| 谁 | 它拿走了什么 | 为什么它必须拿到 |
|---|---|---|
写卡片的说明书(/learn 这个 skill 的文档) |
卡片必须长什么样:用哪些标记名(就是网页里用来标记"这里是什么"的那些记号)、简介最多多少字、日期从哪儿读 | 它是生产方——新文章要上主页时,是它告诉 AI 卡片该怎么写 |
主页的行为脚本(theme.js:一段能被浏览器自动跑起来的小程序) |
去每张卡片里读标题、简介、日期这三样 | 主页上的搜索、按难度筛选、按日期排序,全靠读这三样才能工作 |
检查脚本(check.sh) |
核对:卡片上的日期和文章里写的日期是否一致、顺序是不是倒序、简介有没有超过 140 字 | 它是验收方——每次改完主页,它负责告诉你"这次没搞坏" |
三个消费者,一个生产方,一个验收方。它们之间那个"必须达成一致"的东西,就是这张卡片的口子。数一数,一共三类:
- 口子一:那几个标记名(谁读都得靠名字找到东西)
- 口子二:必须有的三样内容(标题、简介、日期)
- 口子三:简介的长度上限(140 字)
没有边界的时候,这里会怎么坏
假设这三方谁也不管谁,各写各的。那么下面这件事一定会发生,而且它是这个例子里最值得看的一点:
生产方把简介那一项的标记名写成了另一个名字。于是读的那一方读不到简介了——它不会报错,它只会读到"空",然后搜索和筛选就静默地少了一部分能力。你翻主页的时候一切正常,只有在你搜某个词搜不到的时候才会觉得"咦"。
验收方那边可能也不响:如果它只核日期和顺序,简介叫什么名字它根本不看。
结果是:三处对不上,三处都不报错。这就是"没有边界"最典型的样子——不是崩掉,是悄悄不对。
而如果口子定死了(就是上面那三类),情况完全不同:
- 你想改卡片的样子(加个图标、换成两列、换个排版)→ 只要不碰那三样内容、不超 140 字,三方一行都不用改。
- 你想改口子本身(比如把"简介"改名)→ 你知道要同时改三处,因为你数得出来。这不是麻烦,这是麻烦变得可见了。
- 你想知道"改这个会不会影响别的" → 你不用猜。口子那三类,就是全部的影响范围。
这个例子的真实来源
这不是我编的。这个站自己的待写清单里记着一件事:2026-09-16 那天,改主页加一张卡片,最后需要回滚重来(回滚:把改动退回到没改之前的样子——这是"改坏了"最贵的一种代价,因为你前面花的时间全没了)。
而且你可以自己验证"口子确实存在"——就在这个站的文件里:
# 你可以自己跑这两条,看看口子的两端分别长什么样
grep -n "card-title\|card-desc\|card-date" learn/theme.js
grep -n "140" .agents/skills/learn/check.sh
第一条会让你看到读的那一方正指着那几个名字;第二条会让你看到140 这个上限是被谁拦的。这就是"口子"的具体样子——不是概念,是两个文件里对得上的那几行字。
这个卡片没有"被拆成很多文件",也没有"分层"。它就长在主页那个文件里。但它是有边界的——因为那两句话答得出来:它负责"展示一篇文章的摘要";别人只通过"三类口子"用它。
边界跟文件数量、跟目录结构、跟漂不漂亮,都没有关系。
07划完之后,什么变了
你会有一种"好像什么也没变"的错觉——因为文件还是那些文件,行数可能还多了几行。但变的是这四件事:
四条变化,逐条说
① "改一处要动几处"这个数字变小了,而且你能提前说出来
以前你改东西是靠试:改完跑一遍,看看哪儿坏了。现在你能在动手之前说出"这一改会动到哪三处"。这个差别,就是"猜"和"知道"的差别。
② 坏了会响
这是动作二(钉住现状)带来的,也是最容易低估的一条。口子上有了检查,"悄悄不对"就死了。你以前最怕的不是出错,是出错了没人知道——现在有人在门口站着。
③ 你终于能让别人(包括 AI)动它
划边界之前,你交给 AI 一块东西,得先花半小时解释背景:这是干什么的、别碰哪儿、改完要注意什么。划完之后,那三行注释就是背景。你不是变强了,你是把"必须在你脑子里"的东西搬到了纸上。这对"站在审批位上的你"是最实际的一条——你终于能验收了,因为口子给了你一个能对照的东西。
④ 你能回答"这不影响别的吧"
这句话你肯定问过无数次,得到的回答是"应该不会"——那是猜。有了口子,答案变成一个具体的范围:影响到那几处,就这些,其他都不碰。
第一,划完边界,代码总量通常不会变少,有时还会多几行(你加了一个入口)。别用"行数变少了没有"来判断这件事有没有做成。变的不是代码的量,是"改它的时候你要同时照顾的地方"。
第二,边界不是一次划完的。它不是一场大扫除,是"这次改到这儿,顺手把它收一下口"。所以做完一块就停,下个月改到下一块时再收下一个。你每次都只赚一点,但赚到的不会再还回去。
08什么时候不该划
这篇如果不写这一节,就会变成"到处划边界"的怂恿。划边界是有成本的,下面这几种情况划了就是白干。
一个口子只有一个使用者,那这个口子就是纯绕路——多了一层,什么也没挡住。留到有第二个使用者出现的那天再开。
这条有个很好记的说法:一种用法 = 想象出来的口子;两种用法 = 真的口子。别为"以后可能会有人用"先开一个。
边界是给"还要继续改"的东西用的。你手上十几个项目,不是每个都值得——有的你已经不想要了,那就别在它身上花这个钱。
判断法:如果这个项目你已经三个月没打开、而且也没有接下来的计划,跳过它。
如果这一块下周还要大改(要加一堆功能、要换做法),先别划。你现在划的边界,下周会跟着需求一起作废。
等它稳住一个星期,再回来划。边界是给"已经定型的行为"画的。
一次一块。做完一块、验证完、停下来。同时划三块 = 出了事你不知道是哪块引起的——而且你几乎一定会出事。
这条不是保守:一块做完你就有了一次真实经验,第二块的判断会准得多。
还有一个"别当":别把"多写一层"当成划边界
这是"做过头"最常见的样子。你(或者 AI)在原来的东西外面包了一层,看起来很规范,但外面要递进去的东西没变少、要拿出来的也没变少——只是绕了一下路。
外面原来要递进去 6 样东西。包了一层之后,还是要递 6 样——只不过中间多转了一手。
口子没有变少。你付出了一层复杂度,什么也没换到。
外面原来要递 6 样、还要自己按顺序调 3 个动作。收口之后:递进去 1 样,里面的顺序由它自己管。
口子从"6 样 + 3 步顺序"变成"1 样"。这才叫收窄。
一句话的判断法:做完了之后,外面要知道的东西变少了吗?没变少,你就是绕了一圈。
还有一条容易忽略的:口子别定得太死
反过来说,口子也不能收得太窄。如果你把入口定成"只允许这一种用法",那么等第二种用法出现时,别人(或 AI)就只能绕过这个口子——你辛苦划的边界当场作废。
所以留一点余量:口子按"这一类事"来定,不要按"现在这一件事"来定。比如入口叫"读一篇文章的摘要",就比叫"读主页第三张卡片上的简介"耐用得多——后者一改版就废了。
这一条有出处,而且是原话。Ousterhout 在书里列了两种正好相反的错:一种是把太多东西当成重要的(就是口子开得太大、什么都往外露);另一种是没认出某样东西其实很重要——后果是"重要的信息被藏起来了,或者重要的功能拿不到,于是每个开发者都得自己重新造一遍"。口子开得太松和收得太紧,是两种不同的错,不是"越紧越好"。
09五个坑与排错
下面这五个,是这件事最常翻车的地方。每一个都有"症状",你可以拿它当体检表。
坑一:把"拆成更多文件"当成划边界
症状:你把一大块拆成了 5 个文件,感觉自己整理了结构。但改起来还是"一改就坏"。
为什么没用:拆文件只改变了"东西放在哪里",没改变"谁从哪几个口子拿它"。5 个新文件之间还在互相伸手,口子从 1 个变成了 5 个——更糟了。
怎么验:拆完之后跑一次 §04 的数口子提示词。组数如果变多了,说明你拆反了。
坑二:口子开在"里面怎么做的"上
症状:你开了个入口,但外面递进来的东西里,有"内部的顺序""内部的编号""内部的状态"这种只有里面才懂的东西。
为什么是坑:那不是口子,那是把里面伸出来了——只是换了个形式。外面还是得懂里面的规矩,你一点也没减负。
怎么验:把入口的说明书拿给一个完全不懂这块的人看。他看不懂"这个参数该填什么",就是坑二(参数:你要递给它的那样东西)。
坑三:划到一半,新旧两条路都在,然后忘了旧的
症状:半年后你发现,有的地方走新入口,有的地方还在走旧路——而且你已经不记得旧路是干什么的了。
先说清楚:中间状态本身是正常的,甚至是必须的(动作三就是一步一步搬)。问题不在"有两条路",问题在没有截止日期。
怎么办:搬的时候就在动作四那三行注释里多加一行:"旧的走法在 ____ 之后一律不许再用。"有这一行,它就不会烂在那里。
坑四:让 AI 一次性大重构
先说清"重构"这个词:重构 = 不改行为,只改里面的结构。改完之后,东西的长相和能力都该和以前一模一样,只是做法换了。它跟"顺手把毛病也修了"是两件事,混在一起你就分不清是哪边出的问题(这也是动作二存在的理由)。
症状:你跟 AI 说"帮我把这块整理一下,收个口",它给你改了 20 个文件。你跑起来一看,崩了。然后你面对着 20 个文件的改动,不知道从哪儿退回。
为什么是坑:不是 AI 能力问题,是这个任务不适合一次做完。改 20 个文件之后,任何一处出错都淹没在改动里。
怎么办:回到动作三的节奏——一次搬一处,每搬完一处跑一次检查。这在提示词里已经写死了("不要顺手做第 2 步"),照抄就行。
坑五:验收只看"跑起来了没有"
症状:改完,页面能打开,你点了几下没发现问题,就认为成功了。
为什么是坑:这一篇里最危险的坏法叫"悄悄不对"(§06 那个例子)。它能打开、能看、不报错,但某个功能已经少了一半。"跑起来了"完全查不出这种坏。
怎么办:验收时不要只看能不能打开,要拿一个具体的输入去试,并且知道正确结果该是什么。这就是动作二那组检查存在的全部理由。
排错表:症状 → 先做什么
| 你看到的 | 最可能是 | 先做这一个动作 |
|---|---|---|
| 改完没报错,但某个功能好像不灵了 | 悄悄不对——某个口子对不上了,而且没人检查它 | 回去看动作二那组检查覆盖了没有;没覆盖就补一条 |
| AI 说"改好了",但你数不出它动了哪几处 | 改动范围失控,多半是动作三没按"一次一步"来 | 让它列出"改了哪几个文件、每处为什么必须改",列不出就退回重来 |
| 口子收完之后,别的地方要改的反而更多了 | 你开的是坑二那种"把里面伸出来"的口子 | 把入口说明书重写一遍,只留"外面必须知道的事" |
| 你写不出"这一块负责什么"那句话 | 这一块太大,不是一块,是一团 | 回到动作一,往小里再选一层 |
| 数口子数出来一堆,多到不想看了 | 你选的范围太大了 | 只保留"最常被改到"的那一样,其余先记下来放一边 |
10听见这些词别慌:一张对照表
这一节是为了一个很具体的问题:你做这件事的时候,AI 会用一堆词跟你说话。你不需要学会它们,但你要能认出"它其实在说哪句日常话"。认出之后,你就能判断它说的对不对。
| 你会听到的词 | 它其实在说什么 | 为什么它重要 · 别当什么用 |
|---|---|---|
| 模块 / module | 一块能单独拿出来说的东西。可能是一个文件、一个页面上的东西、一个动作、一个后台服务 | 别当"技术上的某种单位"——它的大小由你决定。你说得出口的那一块,就是一块 |
| 边界 / boundary | 那两句话的答案:这块负责什么、别人从哪几个口子进 | 别当"画在文档上的框"。它是可以验证的:改里面,外面要不要跟着改 |
| 接口 / interface | "口子"的正式说法 | 别当"只是那个动作的写法"——它还包括顺序、什么时候不能用、出错会怎样。这是最容易把它想窄的一个词 |
| 实现 / implementation | 里的部分:外面的人不需要知道的那部分 | 别把"实现"当成"不重要的部分"。正好相反——外面越不用知道,里面越可以随便改 |
| 字段 | 一块内容里的"某一项"。比如一张卡片有标题、简介、日期——这三样就是三个字段 | 听到它要警觉:如果外面只拿某一个字段,那往往就是一个独立的口子。§04 数口子时,"只拿了其中某一项"和"整块拿去用"要算成两组 |
| 耦合 | 两个东西"必须一起改" | 别当"两个东西有关系"。有关系不是问题;必须一起改才是问题。判断法就是那句:改一个,另一个要不要跟着改 |
| 单一职责 / SRP | 一个模块只该有一个"需要改它的理由"——注意不是"只干一件事" | 这是最常被误读的一个词,作者本人澄清过:"这个原则讲的是人"——同一批人关心的东西放一起,不同人关心的分开。别当"一个模块只能干一件事",那正是 §08 警告的"做过头" |
| 重构 / refactor | 不改行为,只改结构。长相和能力都一样,只是里面换了做法 | 别当"顺手把毛病也修了"——那是两件事。混在一起,你就分不清是哪边出的问题。这也是动作二存在的理由 |
| 表征测试 / characterization test | 只描述现状的检查。不管对错,只把"现在是这样"钉下来 | 用在改动之前,用来防止"改坏了你不知道"。别当"测试是为了证明代码对"——它只证明"跟以前一样" |
| 接缝 / seam | 另一个说"口子开在哪"的词。原话是:"一个能让你改变程序行为、而不用改这个地方本身的位置"(原文:A seam is a place where you can alter behavior in your program without editing in that place.) | 它比"口子"多强调了一件事:位置是选出来的。同一个东西,口子开在前门还是后门,是两回事 |
| 深模块 / 浅模块 | 一种说"口子小、里面多"的说法 | 听到它先问一句"你说的是哪种算法"——见下面的分歧说明。这个圈子里的两个人对它定义不一样 |
一个真实的分歧:两位作者对"深模块"的定义不一样
下面这段是"你只有真读过两边的原文才写得出来"的东西,也是这张表里最值得看的一格。
一种说法(John Ousterhout,《A Philosophy of Software Design》,第二版 2021 年):深 = 口子比里面简单得多。衡量它的办法是"这点口子的复杂度,换回来多少功能"——口子带来的一点点麻烦,要能换回一大片能力,才叫深;口子几乎和里面一样复杂,就是浅。
另一种说法(codebase-design 这份技能文档,本机装着):它反对的是把深度读成"里面的行数 ÷ 口子的行数"——那样只要往里面多塞行,深度就自动增加了,等于奖励灌水。它改成用杠杆来衡量:一个口子后面能办多少事、能被多少处复用。
有意思的地方来了:被反对的那个"按行数算比例"的读法,Ousterhout 自己也否认——他在书里写过"大小其实没那么重要"(两百行到两千行的类都可以),真正重要的是"这个抽象有多大威力"。所以这更像是一个被简化过的版本,对上它的原意,而不是两个人在吵同一件事。
还有人更彻底:Eric Normand 写过一篇直接叫"模块深度是站不住的"的文章,说"深度"这个比喻经不起细看——同样一段代码,你把框画大一点,它看起来就"深"了;他主张改成问"这个口子提供的控制力,和它带来的复杂度相称吗"。
所以怎么办:三方的结论方向一致(口子要小、复杂性要藏在里面),分歧在"能不能用一个比例把它算出来"。你听到"深模块"时问一句对方用的是哪种说法,就能对上话;而你自己做判断时,用本文那句"改一处要动几处"就够了——它是数出来的,不依赖上面任何一种定义。
"边界"这个词在别的圈子里还有另一个意思(指"一个业务领域的范围",跟这里的"口子"不是一回事)。所以有些 AI 工具或者文档会刻意避开"边界"这个词,改用"接缝"或"接口"。你不需要跟着改口,但听到的时候别以为它们在说两件事。
99一句话总结
边界不是一个设计出来的词,是两个问题的答案:「这一块负责什么」和「别人从哪几个口子用它」——判断法永远只有那一句:"这东西别的地方从哪几个口子用它?"答不上来,它就没有边界(数口子的种类,不是数被引用的次数)。从一堆乱代码出发,顺序是反的:先选一块(口子最多的那个,不是最重要的那个)→ 先钉住它现在的样子(表征测试)→ 再把口子一道一道收窄(一次搬一处,每搬完跑一次检查)→ 最后写下"它负责什么"那句话。一次只动一块,做完就停——因为划完边界你不会得到更少的代码,你得到的是"改它的时候,要同时照顾的地方从 N 个变成一个"。
留给你的最后一件事
这一篇里所有判断,你都能在纸上验证——只有一条不能:你手上那个项目,到底有几块是真的没有边界。那只能你自己去数。