GUI 的 MVVM:入口的进一步拆分

CLI 的入口骨架是「接受 → 解析 → 分发 → 调用」。GUI 把这个骨架进一步拆分了:传入发生在控件交互里,分发发生在不同的用户流程里,调用发生在 ViewModel 层;而 View 与 ViewModel 之间不再靠函数调用传递,而是靠状态变化同步——状态一变,界面自己刷新。验证也因此分成两种:交互验证、状态变化的界面验证。


一、入口骨架回顾

从指令到系统的主线里,入口长这样:

1
接受 → 解析 → 分发 → 调用 → 业务

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
// View:控件交互 → 把操作转成命令发给 ViewModel
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 场景的进一步拆分 + 用状态变化连接页面与调用。 结构的拆分决定了验证的拆分——入口拆成页面与调用,验证就拆成交互验证与状态变化界面验证。