本篇要回答的问题:把"架构思维篇"反复讲的核心系统 / 周边系统正交分解与伤害值两个工具,真刀真枪地套到第二章实战的"画图程序"(v44)上,看业务到底是怎么被分解的,以及如何用数据评判方案优劣。
本讲是一篇加餐式实战复盘,不引入新概念,而是验证前几讲(57 心性、58 如何判断架构优劣、59 少谈框架多谈业务、60 架构分解之边界)的想法。基准是画图程序的最后一次迭代 v44(github.com/qiniu/qpaint/tree/v44)。核心信念:“架构的本质是业务的正交分解,分解后每个模块业务上仍然自洽”,而关注每个模块的业务属性是架构的最高准则。
文件级整体结构:把内核压到最小
除入口 index.htm 外,所有文件按业务属性分四类,用颜色标注:
| 类别 | 颜色 | 含义 |
|---|---|---|
| 核心系统 | 棕色 | 画图业务的核心,不可或缺 |
| 周边系统 | 黄色 | 业务的可选组件(各 Controller) |
| 通用控件 | 绿色 | 与画图业务无关的通用界面元素,被周边系统引用 |
| 基础框架 | 紫色 | 与业务无关的第三方代码或底层框架 |
关键洞见:整个画图程序的"内核"极小——只有
index.htm、view.js、dom.js三个文件。去掉所有周边系统及其依赖,程序仍能工作,只不过退化成一个只读查看器QPaintViewer。所有 Controller 都被做成彼此完全正交的可选组件。
伤害值:用数据评判周边对核心的"伤害"
最关心的不是核心,而是周边系统(Controller)对核心系统的伤害有多大。方法是逐处统计周边模块对 View 层 / Model 层接口的引用。
以 creator/rect.js 为例:对 View 层引用 10 处、对 Model 层引用 6 处,裸伤害值 = 16。但关键在于这些接口大多不是为它一家提供的:
关键判断:伤害值是工程测量值。若某接口被 N 个周边模块引用,则每个周边模块只分担
1/N的伤害。极端地,被无穷多模块引用的接口,对其中任一模块的伤害趋近 0——因为它已被实证为足够通用、足够稳定。
这解释了一个反直觉现象:新增一个互不相关的周边模块,反而会拉低既有模块的伤害值——因为它再次证实了某些接口确实通用。当前只被 rect.js 引用的接口(new QLine、new QRect、new QEllipse、shape.onpaint,表中标红)一眼看上去就很通用,只是因程序规模还小、暂无人分担。
按"被超过 1 个模块引用即记引用 5 次"的近似估算,5 个周边模块的伤害值:
| 周边模块 | 近似伤害值 |
|---|---|
creator/rect.js |
12/5 + 4 = 6.4 |
creator/path.js |
12/5 + 1 = 3.4 |
creator/freepath.js |
13/5 = 2.6 |
accel/select.js |
10/5 + 6 = 8 |
accel/menu.js |
5/5 + 6 = 7 |
| 合计(估算) | ≈ 27.4 |
把周边看作整体,实际引用是 31 处(实际伤害值 31)。估算值 27.4 偏小,差异来自"统一用 5 而非逐一统计引用次数"的近似。
Model 层:再分解一次
代码量最大的是 Model 层 dom.js(约 850 行),于是对它再做一次"核心 + 周边"分解——分形地复用同一套方法论。
Model 层内部同样分四类:
| 类别 | 标色 | 含义 |
|---|---|---|
| 核心系统(接口级) | 棕色 | 会被周边引用的"接口级"模块 |
| 核心系统(内部) | 白色 | 只被核心内部引用,可不画出 |
| OS 相关辅助函数 | 绿色 | 与业务无关但与平台相关 |
| 纯算法辅助函数 | 紫色 | 与业务、与 OS 都无关 |
因 JavaScript 是弱类型,Shape 接口在代码里不显式存在,作者用 Go 语法把它写清楚:style / onpaint / hitTest / bound / setProp / move / toJSON 等方法构成图形的统一契约。
关键洞见:Model 层需求的开放性主要体现在图形(Shape)种类(未来是否支持图片、艺术字……变数大)。不同图形对核心的需求完全一模一样,整个周边伤害值 = 4,平均每种图形伤害值 = 1。这正是好分解的标志——周边对核心的依赖整齐划一。
通用控件库:同一套方法论的第三次复用
控件种类无穷,出于开放性同样拆成核心(控件框架)+ 周边(各控件)。
控件实现本身与控件框架几乎无关,只是把自己注册到框架中。所有控件对核心需求一致,整个周边伤害值 = 1,平均每种控件伤害值 = 1/3。
总结
本讲把"核心系统 / 周边系统正交分解 + 伤害值度量"在画图程序上跑通三遍(整体文件层、Model 层、控件库),证明这套方法论是分形可复用的:任何一个复杂业务,向内看都能再分解出自己的核心与周边。好架构的特征是内核最小、周边正交、周边对核心的依赖整齐且伤害值低。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| 核心系统 | 业务不可或缺的部分,应被压到最小(画图内核仅 3 文件) |
| 周边系统 | 彼此正交的可选组件,去掉后程序仍能退化运行 |
| 伤害值 | 周边对核心接口的引用次数(工程测量值),用来量化耦合、评判方案优劣 |
| 伤害值分担(1/N) | 接口被越多周边引用,单个周边分担的伤害越低,因其被实证为通用 |
| 分形分解 | 把任一子系统(Model 层、控件库)当作完整业务,再做一次核心/周边拆分 |
| 正交 | 周边间互不引用,即使关联也只通过核心间接发生 |
一句话速记
把内核压到最小、让周边彼此正交,再用"伤害值"量化周边对核心的依赖——伤害越低、越整齐,分解越好;同一把尺子可以一层层递归地量下去。
几条值得记住的判断
- 内核最小化是复杂系统设计的第一要务:理清核心与周边的边界。
- 抽象共性接口 优于给单个周边开绿灯;接口的通用性需要"每多一个引用方就被实证加强一次"。
- 伤害值是可用于横向比较的数据,能客观评判不同架构方案的好坏。
思考题
如果你也写过一个"画图程序"或类似的可扩展系统,试着算一算你的周边模块对核心的伤害值。有没有某个接口被你主观判定为"通用",却至今只有一个调用方?它的通用性,是否还缺少实证?







