设计表示——多视图模型:八种表示方式与从设计到实现的顺序

一句话

「自顶向下设计」本质上不是一种图,而是一套多视图模型:文字定义「是什么」,图表示「怎么关联」,表格管理「先后和状态」,代码实现「具体怎么做」。

自顶向下设计到最终实现,最好不要只用一种东西。真正好用的是把不同层次的信息交给不同的表示方式。


一、八种表示方式

表示方式 最适合表达什么 解决的问题
文字说明 目标、规则、职责、约束 「这个东西到底要干什么?」
层级树 / 思维导图 从系统到模块到功能的分解 「大问题怎么拆成小问题?」
流程图 执行顺序、分支、循环 「运行时先做什么、后做什么?」
依赖图 A 依赖 B,模块之间的关系 「谁依赖谁?」
时序图 多对象之间按时间发生的交互 「Player、Input、Physics、Render 谁先和谁通信?」
状态图 状态变化 「玩家从 Idle 怎么进入 Run / Jump / Dead?」
表格 模块、接口、任务、优先级、状态 「具体要做哪些东西?」
代码 / 接口定义 最终可执行结构 「实际上怎么实现?」

其中最核心的四种关系:

1
2
3
4
结构关系:谁属于谁
依赖关系:谁依赖谁
时序关系:谁先谁后
数据关系:谁读谁写

这四种关系分别用不同的图/表表达,最后落到代码。对于大型项目,这比试图用一张 UML 图把所有事情塞进去靠谱得多。


二、顺序与依赖的三个核心表示

1. 层级树:表达「拆解关系」

1
2
3
4
游戏
├── 游戏循环(输入 / 更新 / 渲染)
├── ECS(Entity / Component / System / Registry)
└── Gameplay(Player / Enemy / Collision / Coin)

回答:整体由什么组成?

2. 依赖图:表达「谁依赖谁」

1
2
3
4
5
6
7
8
InputSystem ─────┐

PlayerComponent

PhysicsSystem ───┘

RenderSystem → TransformComponent
RenderSystem → SpriteComponent

回答: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
2
Level 0 目标 → Level 1 系统 → Level 2 模块 → Level 3 接口
→ Level 4 数据 → Level 5 函数 → Level 6 代码

做自顶向下软件设计的推荐组合

不要追求「一个万能图」,而是建立一个非常固定的组合:

1
① 需求文档 → ② 模块树 → ③ 依赖图 → ④ 流程图 → ⑤ 接口表 → ⑥ 实现任务表 → ⑦ 代码

其中:

  • 模块树管「包含关系」
  • 依赖图管「依赖关系」
  • 流程图管「执行顺序」
  • 接口表管「模块之间怎么连接」
  • 任务表管「开发顺序」

这样实际上就拥有了一套从「想法」一直走到 .cpp 文件的可追踪链条


四、从拆解到顺序步骤:拓扑排序与任务 DAG

两个不同的问题

前面解决的是「系统是什么,以及它们是什么关系」;现在问的是「从 0 到 1,究竟应该按照什么顺序把它做出来?

最适合的不是单纯的流程图,而是「依赖关系 → 拓扑排序 → 实现步骤」这一套。

先把「依赖」变成「可执行顺序」

1
2
Vector2 → Entity → Component → Registry → System
→ MovementSystem → CollisionSystem → RenderSystem → Game

这里隐含了后面的东西依赖前面的东西,因此可以得到实现顺序。但真正的工程步骤不能只有「先后」——一个模块「写完」不代表它真的可用:

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
2
3
4
5
6
7
8
             ┌→ Entity ───┐
Vector2 ─────┤ │
└→ Transform ├→ Registry → System

Input ────────────────────┘

Movement + Collision → Game
Render ────────────────┘

这是一个 DAG(有向无环图)。根据 DAG 做拓扑排序,就能得到合法的实现顺序。

  • 依赖图解决「谁必须先于谁」
  • 拓扑排序解决「那我到底按什么顺序做」

五、三种顺序与开发/运行之分

「合法顺序」不一定是「最佳顺序」。实际工程里通常还要加入一个原则:

每一步都尽量让系统变得「更完整,而且可以运行」(Incremental / Vertical Slice)。

最终有三种「顺序」:

A. 架构顺序

谁依赖谁。例如 Registry → System。

B. 实现顺序

先写谁?由依赖关系和拓扑排序决定:Entity → Component → Registry → System。

C. 验证顺序

先让什么跑起来?窗口 → 方块 → 方块移动 → 两个方块 → 碰撞 → 玩家 → 敌人 → 关卡。

明确答案:

用「任务表 + 依赖 DAG + 拓扑排序」表示工程实现顺序;用流程图表示程序运行顺序。两者不要混。

「我要先开发什么」和「程序运行时先做什么」是两种完全不同的顺序。


六、动作级步骤与三个层次

「实现顺序、依赖、拓扑排序」还是在讲怎么组织步骤,还没进入「具体步骤是什么」。这时候需要一套动作级步骤

三个层次:

1
2
3
4
5
6
设计层  「我要有一个 MovementSystem」

任务层 「实现 MovementSystem」

动作层 「创建 MovementSystem.h」「声明 update()」「遍历实体」「读取 Transform」
「修改 position」「编译」「运行」「验证」

一个好的具体步骤必须满足:

做完这一步,应该能明确知道「产生了什么结果」,并且知道「怎么判断它完成了」。

比如不要写「实现碰撞系统」,而应该写:

1
2
3
创建 CollisionSystem.h → 声明 update() → 读取两个实体的 Transform
→ 读取两个实体的 Collider → 判断 AABB 是否重叠 → 返回碰撞结果
→ 编译 → 创建两个测试方块 → 运行测试 → 确认重叠时返回 true → 完成

名称辨析:这不是「实现流程」,而是「设计与实现」

整个过程更准确的名字是 Top-Down Design and Implementation(自顶向下设计与实现),而不只是「自顶向下实现流程」——它包含两个过程:

1
2
自顶向下设计:需求 → 系统 → 模块 → 接口 → 依赖
自顶向下实现:依赖分析 → 确定实现顺序 → 拆可验证任务 → 逐步实现 → 测试 → 集成

而「怎么从拆解结果变成一个一个要做的步骤」更具体属于 Implementation Planning / Implementation Sequence(实现规划 / 实现顺序)

1
需求 → 功能分解 → 架构分解 → 依赖关系 → 实现顺序 → 实现任务 → 代码 → 验证

「自顶向下」描述的是思考和分解方向,拓扑排序 / 任务表 / 里程碑 / 增量开发描述的是怎样把它真正做出来。不要把「自顶向下 = 按顺序一个个写代码」这样理解:

从整体目标逐层分解到可实现单元,再根据依赖关系确定实现顺序,最后逐步编码和验证。

核心转换:

1
需求 → 功能 → 模块 → 模块依赖 → 实现任务 → 具体动作 → 验证

七、设计大、实现小步:三方向三角

修正「组件不组装就无法验证」

很多组件可以通过单元测试、模拟输入、桩对象、最小宿主环境独立验证;但它的最终行为必须在更高一级组装后再验证。

真正的过程不是「设计全部 → 写完全部组件 → 最后一次性运行」,而是:

1
设计整体 → 拆分 → 为每个层级建立局部闭环 → 一边实现一边验证 → 逐层集成 → 最终系统验证

三条轴

  • 架构上:谁依赖谁、谁调用谁、谁产生数据、谁消费数据、谁先启动、谁后执行、状态怎么变化 → 依赖图、数据流、执行流、启动流、状态图、流程图、时序图
  • 实现顺序:根据依赖关系决定先写什么、后写什么
  • 验证闭环(真正关键的第三条轴):不需要全部写完再运行,可以 BroadPhase → 测试 → 修正 → NarrowPhase → 测试 → 修正 → Solver → 测试 → 修正 → PhysicsWorld → 集成前三者 → 测试。这是分层闭环

不断扩大半径的闭环

1
2
3
4
5
6
7
8
9
Function → Test

Component → Component Test

Subsystem → Subsystem Test

System → Integration

Application → End-to-End

「闭环」不等于「必须能运行整个系统」——一个函数(输入→函数→输出→断言)已经是闭环,一个组件、一个子系统也是闭环。

设计可以大、实现必须小步

可以先设计一个相对完整的架构(Game: Input/ECS/Physics/Render/Audio/Gameplay),但实现时仍然是 ECS.Entity → 验证 → ECS.Component → 验证 → …… → 完整 Game → 验证。

设计可以是「大」的,实现必须是「小步」的。

组装本身也是一种验证

Component 单测通过、System 单测通过,并不意味着 Component + System 一定正确。因为会出现:接口不匹配、生命周期错误、数据时序错误、线程问题、状态同步错误、所有权错误。所以验证是:单元验证 → 组件验证 → 子系统验证 → 系统集成验证 → 整体验证,每向上一层就会发现一种新的问题。

四件事

1
2
3
4
① 设计      系统怎么组成?
② 实现 具体先写什么、后写什么?
③ 局部验证 当前这个东西本身对不对?
④ 集成验证 这些正确的东西组合起来以后还对不对?

「不崩溃」不是充分条件

一个系统可能不崩溃,但职责混乱、不易测试、耦合过高、修改一个地方影响十个地方、接口越来越胖——这时候也应该拆。

不断增加功能,直到出现职责过载、依赖复杂、难以测试、修改成本上升等信号,再重新分解。

传统工程的三种模式

  1. 经典工程:自顶向下设计,自底向上实现——上层依赖下层,下层不依赖上层,实现时天然容易从底向上构建
  2. 小项目/熟练工程:设计并不完整,靠经验逐步构建——隐式设计 + 自底向上构建 + 持续重构
  3. 更实际的成熟工程:两边同时走——架构从上往下约束,实现从下往上生长

三方向三角(核心)

设计可以自顶向下,实现通常自底向上,验证则逐层向上扩展。


与其他线的关系

  • 与依赖关系主题:依赖图是依赖关系的表达形式;依赖关系主题讲依赖怎么演化、怎么管理
  • 与算法线:算法线讲算法按类型选描述形式(步骤→流程图、递归→递归树);本线讲整个系统按关系类型选视图
  • 与拆解与组织主题:模块树/层级树是拆解的可视化;任务 DAG 与拓扑排序是组织在时间上的展开
  • 与七条流线:本线的流程图/时序图/状态图是七条流(执行流/时序/状态流)在设计时的表达形式
  • 与建模与逆向主题:八种视图就是同一个模型在不同维度上的投影——模型可以用语言、表格、图、代码多种形式表达