在前面的架构思维篇中(第57讲(详见架构思维/架构设计篇对应章节) ~ 第60讲(详见架构思维/架构设计篇对应章节)),我们反复强调了三个核心论点:
但论点是否成立,终究要看实战验证。本节以第二章"桌面开发篇"的画图程序(v44 版本)为例,解剖架构设计的实际运作机制。
架构分解的第一步,是对代码进行分类。画图程序的全部文件被划分为四类:
| 类别 | 颜色标记 | 定义 | 特征 |
|---|---|---|---|
| 核心系统 | 棕色 | 业务核心,不可或缺 | 去掉后系统无法完整运行 |
| 周边系统 | 黄色 | 业务的可选组件 | 去掉后系统仍可运行(但功能缩减) |
| 通用控件 | 绿色 | 与业务无关的通用界面元素 | 被周边系统引用,不直接属于业务 |
| 基础框架 | 紫色 | 第三方代码或底层框架 | 最基础的依赖 |
伤害值是许式伟提出的衡量周边模块对核心系统"侵入程度"的工程度量。其定义逻辑如下:
本质洞察:抽象出共性的业务方法,比给某个周边模块单独开绿灯要好。每增加一个引用方,就实证了接口的通用性——这正是增加新周边模块反而降低既有模块伤害值的原因。
关键发现:画图程序的内核极小——仅 index.htm、view.js、dom.js 三个文件。去掉所有周边系统及其依赖后,程序仍可工作,只不过退化为一个只读的画图查看器(QPaintViewer)。这证明了所有 Controller 都做到了彼此完全正交的可选组件。
Model 层的伤害值极低:所有图形对核心系统的需求完全一样,整体伤害值仅为 4,平均每种图形的伤害值为 1。这正是正交分解带来的结果——Shape 接口定义了统一的契约,各图形只需要实现这个契约,对核心系统的侵入极小。
伤害值更低:控件只需把自己注册到控件框架中,整体伤害值为 1,平均每种控件的伤害值为 1/3。
| 策略 | 优势 | 风险 |
|---|---|---|
| 核心系统最小化 | 内核稳定、周边正交、伤害值低 | 初期功能不完整(如只读查看器) |
| 核心系统膨胀 | 功能完整、无需可选组件 | 耦合度高、演进困难、伤害值高 |
Trade-off:接受"最小内核 + 可选周边"的架构,换取长期的演进自由度。核心系统只保留不可或缺的业务逻辑,其余全部以正交的周边模块形式存在。
伤害值共担机制看似违反直觉——新增一个周边模块反而降低既有模块的伤害值?但这恰恰是工程测量的正确逻辑:
type Shape interface {
onpaint(ctx CanvasRenderingContext2D)
hitTest(pt Point) HitResult
bound() Rect
setProp(parent any, key string, val any)
move(parent any, dx, dy float64)
toJSON() any
}这个接口的设计选择了**"接口继承 + 组合"而非"类继承"**,体现了 Go 语言的设计哲学:组合实现代码复用,接口实现多态,彼此完全独立。
交叉参考:这与 加餐"想当架构师,我需要成为全才吗?" 中关于"组合优于继承"的讨论一致。
去掉所有 Controller 及其依赖后,画图程序退化为 QPaintViewer——一个只读查看器。这看似是"功能缺失",实则是一个极具工程价值的特征:
如果核心系统为某个周边模块单独提供接口(只有该模块引用),这相当于为特定需求开了绿灯。表面上解决了问题,实际上:
两个周边模块不应该直接交互。如果它们需要建立联系,必须通过核心系统作为中介。这确保了周边模块之间的正交性,使得任何一个周边模块可以被独立替换或移除。
交叉参考:
- 架构思维基础:第57讲"心性"(详见架构思维/架构设计篇对应章节)、第58讲"架构设计优劣"(详见架构思维/架构设计篇对应章节)
- 业务正交分解:第59讲"少谈点框架,多谈点业务"(详见架构思维/架构设计篇对应章节)、第60讲"架构分解:边界"(详见架构思维/架构设计篇对应章节)
- MVC架构:第22讲"桌面程序的架构建议"