这一讲把前面 26~31 讲那些零散的判断,收成了一套方法论。
而且作者掏出了一个之前没给过的东西:一个完全不用 MVC、全部代码揉在一起的版本(v01),
让你亲眼对比"不这么做会怎样"。
这一讲的代码:
| 路径 | |
|---|---|
| v01:全部揉在一个文件里(470 行) | 代码/qpaint源码/v01-全部揉在一起.htm |
| v26:同样功能,MVC 拆成 7 个文件 | 26 讲的 v26/ |
一 · 架构工作分两块
| 是什么 | 考验什么 | |
|---|---|---|
| 基础架构 | 技术选型:选操作系统、编程语言、技术框架、第三方库 | 选择能力——技术前瞻性和判断力 |
| 业务架构 | 业务系统的分解能力——分解领域问题 | 对用户需求的理解 |
作者在这里说了一句挺重的话:
“大部分架构师往往更容易把关注点放到业务架构上,但实际上基础架构的影响面更广,选错产生的代价更高。
架构师之间的差距,更大的是体现在其对待基础架构的态度和能力构建上。”阿里的"大中台、小前台",本质上就是在提倡基础平台建设。
而业务架构的第一步不是画图,是需求分析:
“没有需求分析,就没有业务架构。在业务架构过程中,需求分析至少应该花费三分之一以上的精力。”
记住这个数字:三分之一。 第六节还会回到它。
二 · 分解得好不好,就看两条
① 功能的使用界面(接口),应尽可能符合业务需求对它的自然预期;
② 功能的实现要高内聚,功能与功能之间的耦合尽可能低。
而且这两条在每一层都一样用——子系统分解成模块、模块分解成类、类分解成函数,同一个套路。
2.1 接口自然体现业务需求
这一讲最狠的一句话:
“一个程序员的系统分解能力强不强,其实一眼就可以看出来。你都不需要看实现细节,
只需要看他定义的模块、类和函数的使用接口。如果存在大量说不清业务意图的函数,
或者存在大量职责不清的模块和类,就知道他基本上还处在搬砖阶段。”
这条你在带读里见过它的小号版本:
| 出处 | 同一件事的说法 |
|---|---|
| 带读-01 · 2.6 | addShape(矩形) 是业务动作,_shapes.push(矩形) 是数据结构操作。门要开成业务动作的样子 |
| 带读-04 · 四 | 参数名划定了这个口子将来能长多大(lineStyle → shapeStyle) |
| 带读-06 · 3.2 | 一个设计良好的 URL,读一眼就能画出对方的 Model 树 |
2.2 高内聚:作者自己的两个习惯
“① 一个功能的代码尽可能单独一个文件,不要和其他功能混在一起;
② 一些小功能可能放在同一个文件中,但中间也会用// ------------------这样的注释行
分割成很多逻辑上的"小文件"。”
这个习惯你在源码里到处能看到——dom.js、view.js、menu.js 里那些长长的 // --------- 分割线,
就是他在文件内部又划了一层"小文件"。
“代码高内聚的好处是,多大的团队协作都会很顺畅,代码提交基本上不怎么发生冲突。”
2.3 低耦合:两种依赖
| 依赖谁 | 关注点 |
|---|---|
| 业务无关的基础组件(开源库、公司基础平台) | 稳定:① 成熟度(诞生多久、接口还调不调、缺陷多不多)② 持久性(谁在维护、社区信用、还活跃吗) |
| 底层业务模块 | ① 依赖要**“通用”,尽量不要让底层专门为我定制接口 ② 依赖的接口个数少、调用频次低** |
“不要让底层业务模块专门为我定制接口” 这条很实在——
一旦底层为你开了个专用口子,它就跟你绑死了,别人再来用会发现这个模块长得很怪。
三 · 各种「使用界面」其实是同一件事
| 组织单元 | 它的使用界面是 |
|---|---|
| 函数 | 函数原型(函数名 + 输入参数 + 输出结果) |
| 类 | 公开属性 + 公开方法 |
| 包 / 动态库 | 导出的符号。包对开发者友好,动态库跨语言但只能取语言间的共性部分 |
| 网络服务 | 网络协议(29 讲那张七条的路由表) |
| 命令行程序 | 命令行参数 + stdin + stdout |
| 桌面程序 | 用户的操作方式——而且"最重要的不是界面外观,是交互范式" |
| 子系统 | 一个逻辑概念,物理上不存在。通常 = 根模块的接口 |
它们的共性:都在定义"怎么完成业务需求",只是需求满足方式的层次不一样。
类和函数靠语言级调用,网络服务靠 RPC 请求,桌面程序靠用户交互。理解了这点,"接口应尽可能符合业务需求的自然预期"这句话就不只是在讲函数命名了——
它同样在讲你的 URL 怎么设计、你的命令行开关怎么起名、你的界面交互怎么组织。
顺带一句关于桌面程序的:
“桌面程序的界面外观当然是重要的,但不是最重要的。最重要的是交互范式,
即用户如何完成功能的业务流程的定义。为什么我们需要专门引入产品经理这样的角色来定义产品,
正是因为使用界面的重要性。”
四 · ★ v01 vs v26:不用 MVC 会怎样
作者掏出了一个之前没给过的东西:把画图程序所有代码(HTML + JavaScript)揉在一个文件里的版本。
4.1 真实行数(我数的)
v01(全揉一起) 470 行 1 个文件
v26(MVC 拆开):
dom.js Model 109 行
view.js View 112 行
accel/menu.js Controller 86 行
creator/path.js Controller 90 行
creator/freepath.js Controller 71 行
creator/rect.js Controller 108 行
index.htm 总控 18 行
──────────────────────────────────
合计 594 行 7 个文件
差额:+124 行
(原文估的是 +110,因为他把 dom.js 记成了 100 行。结论不变。)
4.2 所以 MVC 换来了什么
“这说明 MVC 架构的价值并不是给我们降低总代码行数。
实际上,它关注的重点是如何让我们团队协同作战,让工作并行。”“v26 版本我们把功能分拆为 6 个文件,可以交给 6 个团队成员来做,平均每个人写 100 行左右的代码。”
多写 124 行,换的是"6 个人可以同时动手,而且基本不冲突"。
作者自己也承认这个规模有点小题大做:
“对于总体代码量 500 行不到的一个程序来说,这多多少少显得有点小题大做。
但我们在此之后演进迭代了多个版本,功能越来越复杂,分工的必要性也就越来越大。”
而带读-08 那组数字正是这句话的验证:五轮迭代后前端涨到 1513 行,
而三个原有 Creator 从 269 变成 256——各自的作者从头到尾没被别人打扰过。
4.3 作者建议的三个对比维度
拿 v01 和 v26 对着读,看这三点:
| # | 看什么 |
|---|---|
| 1 | 功能的高内聚——某个功能的代码被分散在多少地方 |
| 2 | 功能间的低耦合——v01 全揉在一起,从"如何做系统分解"的视角推演 v26 用 MVC 的意义 |
| 3 | 怎么减少全局变量,为控件化做好准备(这条正好接 31 讲,见 带读-09 · 六) |
五 · 为什么会形成 MVC:稳定点 vs 变化点
这一节把 26 讲那个架子的来历讲清楚了。
“我们第一章探讨需求分析时反复强调一点:要分清需求的稳定点和变化点。
稳定点是系统的核心能力,而变化点则需要做好开放性设计。”
| 层 | 对应什么 | 为什么 |
|---|---|---|
| Model | 稳定点:业务的核心逻辑 | “除非出现新的技术革命导致产品的内在逻辑发生质变” |
| View | 变化点一:屏幕尺寸 | 更小的屏幕意味着信息要被更高效地组织 |
| Controller | 变化点二:交互方式 | 鼠标交互和多点触摸是完全不同的 |
这也解释了三层的"数量"为什么不一样(带读-03 · 3.1 那张依赖图):
“Model 层是一个整体……View 层也是一个整体,但在不同屏幕尺寸和平台可能有不同实现,
但数量不会太多……Controller 层并不是一个整体,它是以插件化的形式存在,不同 Controller 非常独立。”“比如创建矩形,在 PC 鼠标+键盘下有一个
RectCreator,
在触摸屏的交互方式可以是一个全新的RectCreator。在不同平台下,
我们可以初始化不同的 Controller 实例来适应该平台的交互方式。”
而 Model 自己也有变化点:存储和网络。
“不过无论是存储还是网络,从架构视角来说变化都是可预期的。存储介质会变,网络技术会变,
但是变的只是实现,它们的使用接口并没变化。这意味着 Model 层不只是核心逻辑稳定,
IO 和网络子系统也都很稳定。当然这也是把它们归于 Model 层的原因。
如果它们是易变的,可能就被从 Model 层独立出去了。”
这句回答了带读-05 那个疑问——为什么 28 讲把 localStorage 存取、29/30 讲把服务端通讯
全都塞进 Model?因为它们的使用接口稳定。
"什么该放进 Model"的判据,在这里给全了:稳定的放进去,易变的独立出去。
六 · ★ AI coding 之后,这些边界怎么变了
⚠️ 本节是我们自己的引申,不是原文内容。 32 讲写于 AI coding 普及之前。
32 讲的方法论有几条建立在一个前提上:写代码是有成本的,而且是由多个人分工写的。
这两条现在都动了。但不是全面模糊——有的松了,有的反而更硬了。
6.1 松掉的三条
① "让团队并行"这个理由被削弱了
32 讲最直白的那句:多写 124 行,换"6 个人各写 100 行"。
而现在写代码的可能是 1 个人 + AI——AI 不需要并行,它一次能读完 594 行。
② "该不该分"的经济账变了
原文说"500 行不到的程序分层显得小题大做",是因为多写 124 行有成本。
现在这个成本几乎塌了。于是常见的现象是:
让 AI 生成一堆分层,分了,但没想清楚为什么分——形式上有 MVC,实质是伪分层。
AI 特别擅长生成"符合模式"的代码。 你说"用 MVC",它就给你三个文件夹。
但它不知道你的业务哪里是稳定点、哪里是变化点——而第五节刚说了,
MVC 之所以长成那样,正是因为"业务核心稳定、用户交互多变"。
AI 能复制形式,复制不了那个判断。
③ 那"三分之一的精力"最容易被跳过
“没有需求分析,就没有业务架构。在业务架构过程中,需求分析至少应该花费三分之一以上的精力。”
AI coding 最容易跳过的就是这一步——你说个需求它直接开写。三分之一变成了零。
6.2 反而更硬的三条
① 「代码即文档」成了字面真理
“代码即文档。代码是理解一致性更强的文档。”
因为 AI 真的只读代码。 你写在文档系统里的设计说明它看不到;你写在接口里的意图它读得到。
"接口自然体现业务需求"从一条好习惯,变成了"AI 能不能帮上忙"的前提条件。
一个叫handleData2的函数,AI 跟人一样懵。
② 高内聚从"协作顺畅"变成了"AI 改得对不对"
原文给的理由是"团队协作顺畅、提交不冲突"。现在多了一条更直接的:
AI 的上下文是有限的。功能散落各处 = 改一处要先读一堆无关文件。
32 讲那个"一个功能尽可能单独一个文件"的习惯,
现在直接换算成"AI 能不能只读一个文件就改对"。
③ 低耦合里那条"不要让底层为我定制接口"更要紧了
因为 AI 会很乐意帮你在底层开专用口子——它不知道那个模块还有别的使用者。
这类"局部最优、全局变形"的改动,AI 生成起来毫无阻力。
6.3 新冒出来的一条边界:什么自己想,什么给 AI 写
这是 32 讲那个年代没有的问题。 而答案恰好就在这一讲最狠的那句话里:
“你都不需要看实现细节,只需要看他定义的模块、类和函数的使用接口。”
分解 = 定接口 ← 【必须自己想】
实现 = 填接口 ← 【可以给 AI】
| 谁做 | 为什么 | |
|---|---|---|
| 需求分析、划稳定点/变化点 | 人 | AI 不知道你的业务哪里会变 |
| 定模块边界、定接口 | 人 | 这就是"系统分解"本身,是架构工作的全部 |
| 填实现 | AI | 32 讲说的"不需要看实现细节" |
| 写测试、写 mock | AI | 见下 |
6.4 反过来:有一件事 AI 让它更可行了
32 讲说:
“为了降低风险,系统的概要设计阶段也应该有代码产出。
其一,系统的初始框架代码;其二,原型性的代码来验证。一些核心子系统在这个阶段提供了 mock 的系统。”
这正是 29 讲那个"先做 Mock 版服务端"的决策(带读-06 · 四)。
当年那个决策要权衡"值不值得多花几天",现在写 mock 的成本几乎为零——权衡基本没有了。
所以 AI coding 不是让 32 讲过时,是让它的天平换了个方向:
"想清楚"的相对成本上升了,"写出来"的相对成本下降了。
于是"先做个 mock 验证一下"这种以前嫌贵的做法,现在应该成为默认选项。
6.5 一句话收口
32 讲的方法论没有失效,失效的是它的"成本假设"。
- "分不分"的账变了——分层不再贵,所以更容易分错(分了但没想清楚)
- "想清楚"的账没变——稳定点在哪、接口该长什么样,AI 替不了
- 而"代码即文档"从建议变成了硬约束——因为读你代码的现在还有 AI
七 · 验收
| # | 问题 | 答案在 |
|---|---|---|
| 1 | 判断"分解得好不好",最朴素的两条标准是什么? | 二 |
| 2 | 函数、类、网络服务、命令行、桌面程序,它们的"使用界面"分别是什么?共性是什么? | 三 |
| 3 | v01(470 行,一个文件)vs v26(594 行,七个文件)——MVC 换来了什么? | 四 |
| 4 | 为什么 Model 是"一个整体",而 Controller 是"复数、插件式"的? | 五 |
| 5 | 为什么把 localStorage 存取、服务端通讯都塞进 Model? | 五 |
答案
1. ① 功能的使用界面(接口)应尽可能符合业务需求对它的自然预期;
② 功能实现要高内聚,功能之间耦合尽可能低。而且每一层的分解都遵循同一套路。
2. 函数→函数原型;类→公开属性和方法;包/动态库→导出的符号;
网络服务→网络协议;命令行→参数 + stdin/stdout;桌面程序→用户的交互范式。
共性:都在定义"怎么完成业务需求",只是需求满足方式的层次不同——
语言级调用 / RPC 请求 / 用户交互。
3. 不是降低代码量(反而多了 124 行),换的是**“6 个文件可以交给 6 个人并行做,而且基本不冲突”。
作者自己也承认 500 行的规模"多少显得小题大做",但后面五轮迭代验证了它**:
三个 Creator 从 269 变成 256,各自的作者从头到尾没被打扰。
4. 因为它们对应的东西不同:Model 是稳定点(业务核心逻辑),一个业务只有一套核心逻辑;
Controller 是变化点(交互方式),而交互方式随平台而异——
PC 的 RectCreator 和触摸屏的 RectCreator 可以是两个完全不同的实现,
所以它必须是插件式的复数,按平台装不同的。
5. 因为它们的使用接口是稳定的。原文:“存储介质会变,网络技术会变,
但变的只是实现,它们的使用接口并没变化……如果它们是易变的,可能就被从 Model 层独立出去了。”
判据:稳定的放进 Model,易变的独立出去。
下一步
33 讲:桌面开发篇的回顾与总结——整章收尾。
再往后 41~44 讲会把 29 讲那个 Mock 服务端做成正式版(数据库、多租户、高可靠)。
