03-设计表示
设计表示——多视图模型:八种表示方式与从设计到实现的顺序
一句话
「自顶向下设计」本质上不是一种图,而是一套多视图模型:文字定义「是什么」,图表示「怎么关联」,表格管理「先后和状态」,代码实现「具体怎么做」。
自顶向下设计到最终实现,最好不要只用一种东西。真正好用的是把不同层次的信息交给不同的表示方式。
一、八种表示方式
| 表示方式 | 最适合表达什么 | 解决的问题 |
|---|---|---|
| 文字说明 | 目标、规则、职责、约束 | 「这个东西到底要干什么?」 |
| 层级树 / 思维导图 | 从系统到模块到功能的分解 | 「大问题怎么拆成小问题?」 |
| 流程图 | 执行顺序、分支、循环 | 「运行时先做什么、后做什么?」 |
| 依赖图 | A 依赖 B,模块之间的关系 | 「谁依赖谁?」 |
| 时序图 | 多对象之间按时间发生的交互 | 「Player、Input、Physics、Render 谁先和谁通信?」 |
| 状态图 | 状态变化 | 「玩家从 Idle 怎么进入 Run / Jump / Dead?」 |
| 表格 | 模块、接口、任务、优先级、状态 | 「具体要做哪些东西?」 |
| 代码 / 接口定义 | 最终可执行结构 | 「实际上怎么实现?」 |
其中最核心的四种关系:
1 | 结构关系:谁属于谁 |
这四种关系分别用不同的图/表表达,最后落到代码。对于大型项目,这比试图用一张 UML 图把所有事情塞进去靠谱得多。
二、顺序与依赖的三个核心表示
1. 层级树:表达「拆解关系」
1 | 游戏 |
回答:整体由什么组成?
2. 依赖图:表达「谁依赖谁」
1 | InputSystem ─────┐ |
回答:A 能不能脱离 B? 这个东西对软件架构特别重要。
3. 流程图:表达「先后顺序」
1 | 开始 → 读取输入 → 更新ECS → 物理计算 → 碰撞检测 → 更新动画 → 渲染 → 下一帧 |
回答:运行的时候先做什么,后做什么?
再往下就是「实现顺序」——表格特别好用
| 顺序 | 模块 | 依赖 | 完成条件 |
|---|---|---|---|
| 1 | Vector2 | 无 | 能表示二维坐标 |
| 2 | Entity | 无 | 能生成实体 ID |
| 3 | Component | Entity | 能存储数据 |
| 4 | Registry | Entity + Component | 能增删查组件 |
| 5 | System | Registry | 能遍历实体 |
| 6 | MovementSystem | System + Transform | 能移动 |
| 7 | CollisionSystem | System + Transform | 能检测碰撞 |
| 8 | RenderSystem | System + Transform | 能绘制 |
| 9 | Game | 上述全部 | 主循环跑起来 |
这张表实际上是在回答:「我应该先写哪个文件?」
三、四层设计链与推荐固定组合
四层设计链
1 | 需求 → 结构 → 关系 → 执行 → 实现 |
对应:
1 | 文字 → 层级树 → 依赖图 → 流程图/时序图 → 表格 → 代码 |
甚至可以进一步形成层级:
1 | Level 0 目标 → Level 1 系统 → Level 2 模块 → Level 3 接口 |
做自顶向下软件设计的推荐组合
不要追求「一个万能图」,而是建立一个非常固定的组合:
1 | ① 需求文档 → ② 模块树 → ③ 依赖图 → ④ 流程图 → ⑤ 接口表 → ⑥ 实现任务表 → ⑦ 代码 |
其中:
- 模块树管「包含关系」
- 依赖图管「依赖关系」
- 流程图管「执行顺序」
- 接口表管「模块之间怎么连接」
- 任务表管「开发顺序」
这样实际上就拥有了一套从「想法」一直走到 .cpp 文件的可追踪链条。
四、从拆解到顺序步骤:拓扑排序与任务 DAG
两个不同的问题
前面解决的是「系统是什么,以及它们是什么关系」;现在问的是「从 0 到 1,究竟应该按照什么顺序把它做出来?」
最适合的不是单纯的流程图,而是「依赖关系 → 拓扑排序 → 实现步骤」这一套。
先把「依赖」变成「可执行顺序」
1 | Vector2 → Entity → Component → Registry → System |
这里隐含了后面的东西依赖前面的东西,因此可以得到实现顺序。但真正的工程步骤不能只有「先后」——一个模块「写完」不代表它真的可用:
| Step | 工作项 | 前置依赖 | 验证 |
|---|---|---|---|
| 1 | Vector2 | 无 | 加减乘正常 |
| 2 | Entity | 无 | 能创建/销毁 |
| 3 | Component | Entity | 能挂载 |
| 4 | Registry | Entity+Component | 增删查正常 |
| 5 | System | Registry | 能遍历实体 |
| 6 | Movement | System+Transform | 方块能移动 |
| 7 | Collision | Movement | 不穿透 |
| 8 | Render | Transform | 能显示 |
| 9 | Game | 全部 | 能运行 |
于是它不只是「先做 A,再做 B」,而变成:A 完成并通过验证 → 才允许进入 B。
最有用的单位不是「模块」,而是「可验证的步骤」
不要写「实现 ECS」,这个太大。应该拆成:创建 Entity → 能够生成 ID → 能够销毁 ID → 写测试 → 通过。
真正的「步骤」应该长这样:
1 | 动作 → 产生结果 → 验证结果 → 解锁下一步 |
任务 DAG
不要把开发过程理解成简单的 1→2→3→4→5。真实项目通常是:
1 | ┌→ Entity ───┐ |
这是一个 DAG(有向无环图)。根据 DAG 做拓扑排序,就能得到合法的实现顺序。
- 依赖图解决「谁必须先于谁」
- 拓扑排序解决「那我到底按什么顺序做」
五、三种顺序与开发/运行之分
「合法顺序」不一定是「最佳顺序」。实际工程里通常还要加入一个原则:
每一步都尽量让系统变得「更完整,而且可以运行」(Incremental / Vertical Slice)。
最终有三种「顺序」:
A. 架构顺序
谁依赖谁。例如 Registry → System。
B. 实现顺序
先写谁?由依赖关系和拓扑排序决定:Entity → Component → Registry → System。
C. 验证顺序
先让什么跑起来?窗口 → 方块 → 方块移动 → 两个方块 → 碰撞 → 玩家 → 敌人 → 关卡。
明确答案:
用「任务表 + 依赖 DAG + 拓扑排序」表示工程实现顺序;用流程图表示程序运行顺序。两者不要混。
「我要先开发什么」和「程序运行时先做什么」是两种完全不同的顺序。
六、动作级步骤与三个层次
「实现顺序、依赖、拓扑排序」还是在讲怎么组织步骤,还没进入「具体步骤是什么」。这时候需要一套动作级步骤。
三个层次:
1 | 设计层 「我要有一个 MovementSystem」 |
一个好的具体步骤必须满足:
做完这一步,应该能明确知道「产生了什么结果」,并且知道「怎么判断它完成了」。
比如不要写「实现碰撞系统」,而应该写:
1 | 创建 CollisionSystem.h → 声明 update() → 读取两个实体的 Transform |
名称辨析:这不是「实现流程」,而是「设计与实现」
整个过程更准确的名字是 Top-Down Design and Implementation(自顶向下设计与实现),而不只是「自顶向下实现流程」——它包含两个过程:
1 | 自顶向下设计:需求 → 系统 → 模块 → 接口 → 依赖 |
而「怎么从拆解结果变成一个一个要做的步骤」更具体属于 Implementation Planning / Implementation Sequence(实现规划 / 实现顺序):
1 | 需求 → 功能分解 → 架构分解 → 依赖关系 → 实现顺序 → 实现任务 → 代码 → 验证 |
「自顶向下」描述的是思考和分解方向,拓扑排序 / 任务表 / 里程碑 / 增量开发描述的是怎样把它真正做出来。不要把「自顶向下 = 按顺序一个个写代码」这样理解:
从整体目标逐层分解到可实现单元,再根据依赖关系确定实现顺序,最后逐步编码和验证。
核心转换:
1 | 需求 → 功能 → 模块 → 模块依赖 → 实现任务 → 具体动作 → 验证 |
七、设计大、实现小步:三方向三角
修正「组件不组装就无法验证」
很多组件可以通过单元测试、模拟输入、桩对象、最小宿主环境独立验证;但它的最终行为必须在更高一级组装后再验证。
真正的过程不是「设计全部 → 写完全部组件 → 最后一次性运行」,而是:
1 | 设计整体 → 拆分 → 为每个层级建立局部闭环 → 一边实现一边验证 → 逐层集成 → 最终系统验证 |
三条轴
- 架构上:谁依赖谁、谁调用谁、谁产生数据、谁消费数据、谁先启动、谁后执行、状态怎么变化 → 依赖图、数据流、执行流、启动流、状态图、流程图、时序图
- 实现顺序:根据依赖关系决定先写什么、后写什么
- 验证闭环(真正关键的第三条轴):不需要全部写完再运行,可以 BroadPhase → 测试 → 修正 → NarrowPhase → 测试 → 修正 → Solver → 测试 → 修正 → PhysicsWorld → 集成前三者 → 测试。这是分层闭环。
不断扩大半径的闭环
1 | Function → Test |
「闭环」不等于「必须能运行整个系统」——一个函数(输入→函数→输出→断言)已经是闭环,一个组件、一个子系统也是闭环。
设计可以大、实现必须小步
可以先设计一个相对完整的架构(Game: Input/ECS/Physics/Render/Audio/Gameplay),但实现时仍然是 ECS.Entity → 验证 → ECS.Component → 验证 → …… → 完整 Game → 验证。
设计可以是「大」的,实现必须是「小步」的。
组装本身也是一种验证
Component 单测通过、System 单测通过,并不意味着 Component + System 一定正确。因为会出现:接口不匹配、生命周期错误、数据时序错误、线程问题、状态同步错误、所有权错误。所以验证是:单元验证 → 组件验证 → 子系统验证 → 系统集成验证 → 整体验证,每向上一层就会发现一种新的问题。
四件事
1 | ① 设计 系统怎么组成? |
「不崩溃」不是充分条件
一个系统可能不崩溃,但职责混乱、不易测试、耦合过高、修改一个地方影响十个地方、接口越来越胖——这时候也应该拆。
不断增加功能,直到出现职责过载、依赖复杂、难以测试、修改成本上升等信号,再重新分解。
传统工程的三种模式
- 经典工程:自顶向下设计,自底向上实现——上层依赖下层,下层不依赖上层,实现时天然容易从底向上构建
- 小项目/熟练工程:设计并不完整,靠经验逐步构建——隐式设计 + 自底向上构建 + 持续重构
- 更实际的成熟工程:两边同时走——架构从上往下约束,实现从下往上生长
三方向三角(核心)
设计可以自顶向下,实现通常自底向上,验证则逐层向上扩展。
与其他线的关系
- 与依赖关系主题:依赖图是依赖关系的表达形式;依赖关系主题讲依赖怎么演化、怎么管理
- 与算法线:算法线讲算法按类型选描述形式(步骤→流程图、递归→递归树);本线讲整个系统按关系类型选视图
- 与拆解与组织主题:模块树/层级树是拆解的可视化;任务 DAG 与拓扑排序是组织在时间上的展开
- 与七条流线:本线的流程图/时序图/状态图是七条流(执行流/时序/状态流)在设计时的表达形式
- 与建模与逆向主题:八种视图就是同一个模型在不同维度上的投影——模型可以用语言、表格、图、代码多种形式表达
