GUI 的 MVVM:入口的进一步拆分
CLI 的入口骨架是「接受 → 解析 → 分发 → 调用」。GUI 把这个骨架进一步拆分了:传入发生在控件交互里,分发发生在不同的用户流程里,调用发生在 ViewModel 层;而 View 与 ViewModel 之间不再靠函数调用传递,而是靠状态变化同步——状态一变,界面自己刷新。验证也因此分成两种:交互验证、状态变化的界面验证。
一、入口骨架回顾
从指令到系统的主线里,入口长这样:
CLI 里这四个动作都在 main 里串行完成:
1 2 3 4
| main ├── parse(argc, argv) ← 接受 + 解析 ├── dispatch(cmd, handlers) ← 分发 └── handler(&data) ← 调用
|
GUI 也是入口,但它不能沿用这个骨架——因为 GUI 不是「一次执行然后退出」,而是长期运行、事件驱动。用户随时可能点任何按钮。所以 GUI 的入口必须重新拆分。
二、GUI 的入口拆成了页面与调用
GUI 把入口骨架拆成两大部分:
1 2 3
| GUI 入口 ├── 页面(View) ← 传入 + 分发 落在控件交互里 └── 调用(ViewModel) ← 调用落在 VM 层
|
对照看,入口骨架的四步在 GUI 里各归其位:
1 2 3 4 5 6
| 入口骨架 GUI 里落在哪 ──────────────────────────────────────────── 接受(传入) View:控件交互(用户点按钮、输入文字) 解析 ViewModel:把用户操作转成命令 分发 用户流程:不同的用户操作走不同的流程 调用 ViewModel:调用业务 API
|
关键变化:
1 2 3 4
| CLI: 接受/解析/分发/调用 全在 main,线性执行 GUI: 接受在 View(控件),解析+调用在 ViewModel 分发在用户流程(不同操作 → 不同流程) View 与 ViewModel 通过状态变化同步
|
三、页面(View):传入与分发
View 负责产生用户操作和显示数据。
1 2 3 4 5 6 7 8 9 10
| View ├── 接受:控件交互 │ ├── 用户点击按钮 │ ├── 用户输入文字 │ └── 用户选择列表项 │ └── 分发:不同的用户操作 → 不同的流程 ├── 点登录 → 登录流程 ├── 点退出 → 退出流程 └── 点设置 → 设置流程
|
View 把用户操作转发出去,自己不处理业务:
1 2 3 4
| connect(loginButton_, &QPushButton::clicked, this, [this]() { viewModel_.login(username_->text(), password_->text()); });
|
View 不管业务怎么实现——它只负责「用户做了什么」和「界面显示什么」。
四、调用层(ViewModel):解析 + 调用
ViewModel 提供操作接口和可观察状态。
1 2 3 4 5
| ViewModel ├── 解析:把用户操作转成命令(login(username, password)) ├── 调用:调用业务 API(service.auth().login(...)) ├── 状态:修改状态(loading / loggedIn / error) └── 通知:状态变化 → 通知 View 刷新
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class LoginViewModel : public QObject { Q_OBJECT public: void login(const QString& username, const QString& password) { auto result = service_.auth().login(username, password); if (result.success) emit loginSucceeded(); else emit loginFailed(result.message); } signals: void loginSucceeded(); void loginFailed(const QString& message); private: Service& service_; };
|
ViewModel 对 View 暴露两类东西:
1 2 3
| ViewModel ├── Command / Action:login() ← View 调用它 └── State / Event:loginSucceeded ← View 订阅它
|
View 只需要:用户操作 → Command;状态变化 ← State/Event。 ViewModel 完全不需要 view.setText(...)——否则 ViewModel 就开始负责 UI 了。
五、View ↔ ViewModel:通过状态变化更新
这是 MVVM 和 CLI 最大的不同——View 与 ViewModel 之间不是函数调用,而是状态订阅:
1 2 3 4 5
| CLI:main 调用 handler,handler 返回结果(同步函数调用)
MVVM: View → 用户操作 → 调用 Command → ViewModel ViewModel → 修改状态 → 发出信号 → View 收到 → 刷新界面
|
1 2
| 用户 → View → 调用命令 → ViewModel → 修改状态 → ViewModel State → 通知 → View → 显示
|
状态变化是 View 和 ViewModel 之间的纽带:
1 2 3
| ViewModel 不知道 View 的存在(不持有 View 指针) View 订阅 ViewModel 的状态 状态一变,View 自动刷新
|
好处:
1 2 3
| ✅ ViewModel 不依赖 UI(换 GUI 库不用改 ViewModel) ✅ 状态集中管理(界面显示什么 = ViewModel 状态是什么) ✅ 可测试(状态变化可以直接测,不需要真实界面)
|
六、验证分两种:交互验证 + 状态变化界面验证
因为入口拆成了「页面(交互)」和「调用(状态)」,验证也拆成两种:
验证 1:交互验证
验证「用户操作 → View 正确转发」:
1 2 3 4 5
| 问:用户点登录按钮,View 有没有把用户名密码发给 ViewModel? 验证: → 模拟点击按钮 → 检查 ViewModel 收到了正确的 login(username, password) → 控件是否正确显示/禁用/隐藏
|
交互验证的对象是 View 的行为——控件交互、事件转发、显示状态。
验证 2:状态变化界面验证
验证「ViewModel 状态变化 → View 正确刷新」:
1 2 3 4 5
| 问:登录失败时,界面有没有正确显示错误? 验证: → 让 ViewModel 进入 loginFailed 状态 → 检查 View 是否收到了信号 → 检查界面是否显示了错误信息
|
状态变化界面验证的对象是 View 对状态的响应——信号收到、界面刷新、显示正确。
两种验证的分工
1 2 3 4 5 6 7
| 交互验证:用户 → View → Command 这一段 (用户操作是否正确转成命令)
状态变化界面验证:ViewModel State → View 这一段 (状态变化是否正确反映到界面)
业务逻辑本身 → ViewModel/Service 的单元测试(另一条线)
|
1 2 3 4 5 6 7 8 9 10
| ┌─ 交互验证 ─────────────┐ 用户 → View → Command → ViewModel │ 修改状态 │ ViewModel State │ 通知 View ┌─ 状态变化界面验证 ──────┘ View → 刷新显示
|
验证的拆分和结构的拆分一致——入口拆成「页面+调用」,验证就拆成「交互+状态变化界面」。
七、为什么 MVVM 会自然出现
MVVM 不是 GUI 的起点,而是 GUI 演化到一定复杂度的产物:
1 2 3 4 5
| 简单 GUI:UI → Handler → Service(一个 Handler 直接处理) → UI 事件和业务调用之间确实存在转换职责
复杂 GUI:UI → ViewModel → Service → 需要大量状态绑定、多页面、可替换 UI
|
触发条件:
1 2 3 4
| ✅ 多个页面共享状态 ✅ UI 复杂,需要大量状态绑定 ✅ 需要换 GUI 库(ViewModel 不依赖 View) ✅ 需要界面状态可测试
|
MVVM 是入口骨架(接受→解析→分发→调用)在 GUI 场景的进一步拆分——接受落在控件交互,分发落在用户流程,调用落在 ViewModel,页面和 ViewModel 通过状态变化同步。
八、和其他线的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| MVVM ←→ 从指令到系统主线(入口骨架) 主线:接受 → 解析 → 分发 → 调用(CLI) 本线:同一骨架在 GUI 的进一步拆分(页面 + ViewModel + 状态变化)
MVVM ←→ 从输入到交互(GUI 篇) 从输入到交互讲用户怎么操作 GUI(外部视角) 本线讲 GUI 内部入口怎么组织(内部视角)
MVVM ←→ 框架化组件 + 依赖注入 ViewModel 是 GUI 子系统的框架化组件(有状态、有生命周期) 业务组件被 ViewModel 调用 → 依赖注入出现
MVVM ←→ 边界 View/ViewModel 的边界 = 变化边界(UI 会变,业务不变) ViewModel 不持有 View = 生命周期边界
MVVM ←→ 七条流(状态流) View ↔ ViewModel 通过状态变化同步 = 状态流的 GUI 形态
MVVM ←→ 测试与验证(07-工程控制/05-测试、06-验证) 验证的拆分和结构的拆分一致:入口拆成页面+调用 → 验证拆成交互验证+状态变化界面验证 测试线里 GUI 的单元/集成/用户流程/E2E 测试 = 本线验证拆分的完整展开
|
收束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| GUI 的入口 = 入口骨架的进一步拆分: 接受 → 控件交互(View) 分发 → 用户流程(页面) 调用 → ViewModel View ↔ ViewModel → 状态变化同步
ViewModel 暴露两类东西: Command(View 调用它)+ State/Event(View 订阅它)
状态变化是纽带: ViewModel 不知道 View,状态一变 View 自动刷新
验证分两种: 交互验证:用户操作是否正确转成命令 状态变化界面验证:状态变化是否正确反映到界面
MVVM 是演化的结果,不是起点: 简单 GUI 用 Handler,复杂 GUI 才需要 ViewModel
|
MVVM = 入口骨架在 GUI 场景的进一步拆分 + 用状态变化连接页面与调用。 结构的拆分决定了验证的拆分——入口拆成页面与调用,验证就拆成交互验证与状态变化界面验证。