本篇要回答的问题:需求分析之后的架构第二步——系统的概要设计怎么做?评判系统分解好坏的标准是什么?为什么桌面程序会沉淀出 MVC 这个套路?
第一章讲过需求分析(架构第一步,占三分之一以上精力)。本讲把话题拉回架构,讲架构第二步:系统的概要设计(系统设计)。它的核心能力就是"对系统进行分解"——明确子系统的职责边界和接口协议,把大框架搭起来。
基础架构与业务架构
| 维度 | 是什么 | 考验什么 | 备注 |
|---|---|---|---|
| 基础架构 | 技术选型:选 OS、语言、框架、第三方库 | 选择能力、技术前瞻性与判断力 | 影响面更广、选错代价更高;真正牛的架构师无比重视它(如"大中台、小前台") |
| 业务架构 | 业务系统的分解能力 | 对领域问题的理解 | 分解领域问题,避不开需求分析 |
两者本质都是对业务系统的分解:基础架构分解出"与业务几乎无关、领域无关的基础设施",业务架构则分解领域问题本身。
系统分解优劣的两条朴素评判标准:
| 标准 | 含义 |
|---|---|
| 接口符合自然预期 | 功能的使用界面(接口)应尽可能符合业务需求对它的自然预期 |
| 高内聚低耦合 | 功能实现要高内聚,功能与功能之间耦合尽可能低 |
软件系统的组织单元有多层:子系统 → 模块 → 类 → 方法/函数。每一层的分解都遵循相同的方法论。
接口要自然体现业务需求
各类组织单元的"使用界面"是什么:
| 组织单元 | 使用界面(接口) |
|---|---|
| 函数 | 函数原型:函数名(含名字空间全称)+ 输入参数列表 + 输出结果列表 |
| 类 | 公开属性列表 + 公开方法列表(方法比函数多一个 receiver/this/self) |
| 包/静态库 | 由编程语言定义,对开发者友好 |
| 动态库(Go 里叫 plugin) | 由操作系统定义,可跨语言但只能取语言间共性,对开发者不友好 |
| 网络服务程序(service) | 网络协议 |
| 命令行程序(CLI) | 命令行(命令名/开关/参数)+ stdin + stdout |
| 桌面程序(GUI) | 用户的操作方式,最重要的是交互范式(故需产品经理定义) |
子系统是唯一不存在物理实体、只活在架构文档里的概念——它是一个逻辑上的"大模块",同样要定义使用接口。它与模块的对应有两种:
| 情况 | 结构 | 子系统的使用接口 | 例 |
|---|---|---|---|
| 最常见 | 一个根模块(总控)+ 若干子模块 | 即根模块的使用接口 | —— |
| 另一种 | 多个相似模块构成 | 这些模块有统一的使用界面 | Office 的 IO 子系统:Word/HTML/TXT/PDF 读写 |
关键判断:一个程序员系统分解能力强不强,一眼就能看出来——不用看实现,只看他定义的模块/类/函数的使用接口。若存在大量说不清业务意图的函数、职责不清的模块,基本还在搬砖阶段。职责是否单一清晰、接口是否自然到无需额外文档,体现架构功力。
功能实现准则:高内聚低耦合
| 准则 | 含义 | 许式伟的习惯/关注点 |
|---|---|---|
| 高内聚 | 一个功能的代码尽量写在一起,不散落各处 | 一功能尽量独立一个文件;小功能合并时用 // ---- 注释分割成逻辑"小文件"。好处:多大团队协作都顺畅,提交基本不冲突 |
| 低耦合 | 实现某功能所依赖的外部环境少、易构建 | 见下 |
外部依赖分两种:
| 依赖类型 | 关注点 |
|---|---|
| 对业务无关的基础组件依赖 | 核心看稳定:① 成熟度(诞生多久、接口是否稳定、issue 是否少);② 持久性(维护者社区信用、项目活跃度、贡献者多少) |
| 对底层业务模块依赖(架构关注重点) | ① 依赖要"通用",别让底层为我定制接口;② 依赖的接口个数少、调用频次低 |
怎么做系统分解?
系统分解是领域性问题,没有放之四海皆准的办法。流程:从需求归纳出发 → 把功能点涉及的数据(对象)、操作接口理清并归纳 → 每个功能归类 → 理清类与类关系、做到逻辑自洽 → 基本系统框架成形。
概要设计阶段以子系统为维度阐述各角色关系;对关键子系统进一步分解,甚至确定其所有模块的职责与接口。但:
焦点不是确定完整的模块列表,而是整个系统如何被有效串联起来。某个子系统不细化也无项目风险,就不必在此阶段细化。
而且概要设计也应该有代码产出,目的有二:① 系统初始框架代码(架子搭起来);② 原型性代码验证(核心子系统提供 mock)。好处是一上来就消除全局系统性风险,并给各负责人具象、确定的认知。
代码即文档。代码是理解一致性更强的文档。
再谈 MVC:套路从何而来
桌面虽业务千差万别,但本身是确定性领域,会形成固有套路——MVC。不同时期交互不同(键鼠/触屏),但框架一致:事件分派做输入,GDI 做界面呈现。
用"稳定点/变化点"解释 MVC 为何成形:
| 层 | 承接的稳定点/变化点 | 形态 |
|---|---|---|
| Model | 业务核心逻辑(稳定);其变化点在存储和网络,但只变实现、接口不变,故 IO/网络子系统也稳定,归入 Model | 类与函数,是一个整体 |
| View | 承接屏幕尺寸变化点(信息组织效率) | 不同尺寸/平台可有不同实现,但数量不多(响应式鼓励共享) |
| Controller | 承接交互方式变化点(鼠标 vs 多点触摸),把输入转为对 Model 的业务请求 | 插件化,各 Controller 非常独立,按平台初始化不同实例 |
变种如 MVP:Model 发出 DataChanged 后由 Controller 监听并 Update View(而非 View 自己响应)。这些差异只是细节权衡,不改实质。
怎么看待实战?
学架构强调"做中学"——计算机科学是实践科学,架构经验是一线实战的积累。为看清架构演变,作者还实现了一个所有代码揉在一起的非 MVC 版本(v01,约 470 行)与 MVC 版本(v26)对比:
| 对比项 | v01(非 MVC) | v26(MVC) |
|---|---|---|
| 代码规模 | 约 470 行 | 约 580 行(多 110 行:dom.js 100 + view.js 112 + 4 个 Controller + index.htm 18) |
| 价值 | —— | 不是降代码量,而是让团队并行:拆 6 文件可交 6 人,人均约 100 行 |
关键判断:MVC 的价值不在减少总行数,而在让团队协同、工作并行——靠的正是"功能高内聚、功能间低耦合"。代码量越大、迭代越频繁,分工的必要性越大。
总结
概要设计(系统设计)是架构第二步,核心是"对系统分解"。判好坏看两条:接口自然体现业务需求、实现高内聚低耦合。各级组织单元(子系统/模块/类/函数)的分解遵循同一方法论,使用界面是关键观察点。阶段焦点是"系统如何被有效串联"而非穷举模块,且应有框架代码与 mock 产出以提前消除全局风险。桌面领域的固有套路 MVC,正是用稳定点/变化点对业务核心、屏幕尺寸、交互方式分别安放的结果。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| 基础架构 | 做技术选型(OS/语言/框架/库),考验选择能力,影响面最广 |
| 业务架构 | 分解领域问题,离不开需求分析 |
| 使用界面(接口) | 函数原型/类公开成员/网络协议/命令行/交互范式等,是分解的关键观察点 |
| 子系统 | 只存在于架构文档的逻辑大模块,由根模块或多个相似模块构成 |
| 高内聚低耦合 | 功能代码聚在一起、对外部依赖少且通用 |
| 概要设计的代码产出 | 初始框架代码 + 核心子系统 mock,用于提前消除全局风险 |
| MVC | 桌面领域用稳定点/变化点分解出的固有套路 |
一句话速记
概要设计就是"分解系统":用接口自然 + 高内聚低耦合两把尺子衡量分解质量,以子系统为维度先把骨架和 mock 搭出来消除全局风险——而 MVC 不过是桌面领域里"稳定点归 Model、屏幕变化归 View、交互变化归 Controller"的标准答案。
几条值得记住的判断
- 看接口就能判断分解功力:说不清业务意图的函数、职责不清的模块,是搬砖信号。
- 概要设计要出代码:框架 + mock 提前暴露并消除全局系统性风险。
- MVC 不省代码量,省的是协作成本:让大团队高度并行。
思考题
对比 v01(所有代码揉一起)和 v26(MVC):从"功能高内聚、功能间低耦合、减少全局变量为控件化做准备"三个角度,你能推演出 v26 用 MVC 分解到底带来了哪些具体收益?回到你的项目,有没有某个职责不清、说不清业务意图的模块,正是分解没做好的痕迹?



