GFramework/docs/zh-CN/tutorials/basic/05-command-system.md
GeWuYou e4e79e16dc docs(tutorials): 更新基础教程导航结构并添加完整教程内容
- 重构教程导航,将基础教程拆分为多个子章节
- 添加第1章:环境准备,包含.NET SDK和Godot引擎安装指南
- 添加第2章:项目创建与初始化,介绍GFramework项目结构搭建
- 添加第3章:基础计数器实现,演示传统MVC模式及问题分析
- 添加第4章:引入Model重构,展示GFramework的Model层设计
- 配置教程间的前后导航链接
- 更新导航菜单结构,支持折叠展开功能
2026-02-12 01:08:55 +08:00

536 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
prev:
text: '引入 Model 重构'
link: './04-model-refactor'
next:
text: 'Utility 与 System'
link: './06-utility-system'
---
# 第 5 章:命令系统优化
在上一章中,我们通过 Model 和事件系统实现了数据驱动的架构。但 Controller 仍然承担着交互逻辑,本章将引入 **Command命令模式
** 进一步优化。
## Controller 的职责问题
### 当前代码
```csharp
public override void _Ready()
{
_counterModel = this.GetModel<ICounterModel>()!;
AddButton.Pressed += () =>
{
_counterModel.Increment(); // ← 交互逻辑
};
SubButton.Pressed += () =>
{
_counterModel.Decrement(); // ← 交互逻辑
};
// ...
}
```
看起来很简洁,但这段代码同时承担着:
- **表现逻辑**View Binding`AddButton.Pressed +=`
- **交互逻辑**Interaction Logic`_counterModel.Increment()`
### 为什么这是问题?
现在只是简单的增减,但如果功能变复杂:
```csharp
AddButton.Pressed += async () =>
{
// 1. 验证状态
if (!CanIncrement()) return;
// 2. 执行业务逻辑
await DoSomethingAsync();
_counterModel.Increment();
// 3. 保存数据
await SaveToFileAsync();
// 4. 播放音效
PlaySound("increment.wav");
// 5. 统计埋点
LogAnalytics("counter_incremented");
// 6. 更新成就
UpdateAchievement();
};
```
**问题**
- Controller 迅速膨胀
- 逻辑难以复用(如果键盘快捷键也要增加计数?)
- 难以测试(需要 mock 按钮)
- 违反单一职责原则
## 理解 Command 模式
### Command 的作用
**Command命令** 是一种设计模式,它将"请求"封装成对象:
```
用户操作 → Command → Model
```
优势:
- **解耦**Controller 不关心如何增加计数,只负责"发送命令"
- **复用**:同一个命令可以被多个地方调用
- **扩展**:新增逻辑只需修改命令,不影响 Controller
- **可测试**:可以独立测试命令逻辑
### 职责划分
| 层级 | 职责 |
|----------------|------------|
| **Controller** | 将用户操作转换为命令 |
| **Command** | 封装具体的业务逻辑 |
| **Model** | 存储状态,发送事件 |
## 创建 Command
### 1. 创建增加命令
`scripts/command/` 创建 `IncreaseCountCommand.cs`
```csharp
using GFramework.Core.command;
using GFramework.Core.extensions;
using MyGFrameworkGame.scripts.model;
namespace MyGFrameworkGame.scripts.command;
/// <summary>
/// 增加计数器值的命令
/// </summary>
public class IncreaseCountCommand : AbstractCommand
{
/// <summary>
/// 执行命令的核心逻辑
/// </summary>
protected override void OnExecute()
{
// 获取 Model 并调用方法
var model = this.GetModel<ICounterModel>()!;
model.Increment();
}
}
```
::: tip AbstractCommand
`AbstractCommand` 是 GFramework 提供的基类,它:
- 自动注入 `Architecture` 上下文
- 提供 `GetModel``GetSystem``GetUtility` 等方法
- 管理命令的生命周期
:::
### 2. 创建减少命令
`scripts/command/` 创建 `DecreaseCountCommand.cs`
```csharp
using GFramework.Core.command;
using GFramework.Core.extensions;
using MyGFrameworkGame.scripts.model;
namespace MyGFrameworkGame.scripts.command;
/// <summary>
/// 减少计数器值的命令
/// </summary>
public class DecreaseCountCommand : AbstractCommand
{
/// <summary>
/// 执行命令的核心逻辑
/// </summary>
protected override void OnExecute()
{
var model = this.GetModel<ICounterModel>()!;
model.Decrement();
}
}
```
## 重构 Controller
### 使用命令替换直接调用
编辑 `App.cs`
```csharp
using GFramework.Core.Abstractions.controller;
using GFramework.Core.extensions;
using GFramework.SourceGenerators.Abstractions.rule;
using Godot;
using MyGFrameworkGame.scripts.command;
using MyGFrameworkGame.scripts.model;
namespace MyGFrameworkGame.scripts.app;
[ContextAware]
public partial class App : Control, IController
{
private Button AddButton => GetNode<Button>("%AddButton");
private Button SubButton => GetNode<Button>("%SubButton");
private Label Label => GetNode<Label>("%Label");
public override void _Ready()
{
// 监听事件
this.RegisterEvent<CounterModel.ChangedCountEvent>(e =>
{
UpdateView(e.Count);
});
// 使用命令替换直接调用
AddButton.Pressed += () =>
{
this.SendCommand(new IncreaseCountCommand());
};
SubButton.Pressed += () =>
{
this.SendCommand(new DecreaseCountCommand());
};
// 初始化界面
UpdateView();
}
private void UpdateView(int count = 0)
{
Label.Text = $"Count: {count}";
}
}
```
### 运行游戏
**F5** 运行游戏,功能依然正常!
## 对比重构前后
### 重构前(使用 Model
```csharp
AddButton.Pressed += () =>
{
_counterModel.Increment(); // ← 直接调用 Model
};
```
**问题**
- Controller 知道如何增加计数
- 如果逻辑复杂化Controller 会变臃肿
### 重构后(使用 Command
```csharp
AddButton.Pressed += () =>
{
this.SendCommand(new IncreaseCountCommand()); // ← 发送命令
};
```
**优势**
- Controller 不关心如何增加计数
- 逻辑封装在 Command 中
- Controller 只负责"转发用户意图"
## Command 的优势
### 1. 解耦 Controller
**之前**
```csharp
AddButton.Pressed += () =>
{
if (!CanIncrement()) return;
await SaveData();
_counterModel.Increment();
PlaySound();
LogAnalytics();
};
```
Controller 必须知道所有细节。
**现在**
```csharp
AddButton.Pressed += () =>
{
this.SendCommand(new IncreaseCountCommand());
};
```
所有逻辑在 Command 中:
```csharp
protected override void OnExecute()
{
if (!CanIncrement()) return;
await SaveData();
this.GetModel<ICounterModel>()!.Increment();
PlaySound();
LogAnalytics();
}
```
### 2. 逻辑复用
假设需要通过键盘快捷键增加计数:
**之前**
```csharp
AddButton.Pressed += () => { /* 逻辑 */ };
Input.IsActionPressed("increment") => { /* 复制相同逻辑 */ };
```
代码重复!
**现在**
```csharp
AddButton.Pressed += () => this.SendCommand(new IncreaseCountCommand());
Input.IsActionPressed("increment") => this.SendCommand(new IncreaseCountCommand());
```
逻辑只写一次!
### 3. 易于测试
**之前**
```csharp
// 无法测试,必须 mock 按钮
AddButton.Pressed += () => { /* 逻辑 */ };
```
**现在**
```csharp
// 可以直接测试命令
[Test]
public void IncreaseCommand_ShouldIncrementCount()
{
var model = new CounterModel();
var command = new IncreaseCountCommand();
command.Execute();
Assert.AreEqual(1, model.Count);
}
```
### 4. 支持撤销/重做(扩展)
Command 模式天然支持撤销功能:
```csharp
public class IncreaseCountCommand : AbstractCommand
{
protected override void OnExecute()
{
// 执行
this.GetModel<ICounterModel>()!.Increment();
}
public void Undo()
{
// 撤销
this.GetModel<ICounterModel>()!.Decrement();
}
}
```
## Command 的实际应用
让我们看一个更复杂的例子:
```csharp
/// <summary>
/// 更改语言命令
/// </summary>
public class ChangeLanguageCommand : AbstractAsyncCommand<ChangeLanguageInput>
{
protected override async Task OnExecuteAsync(ChangeLanguageInput input)
{
// 1. 获取设置 Model
var settingsModel = this.GetModel<ISettingsModel>()!;
// 2. 获取设置数据
var settings = settingsModel.GetData();
// 3. 修改语言配置
settings.Language = input.Language;
// 4. 应用设置(通过 System
await this.GetSystem<ISettingsSystem>()!.Apply();
}
}
```
如果这些逻辑都写在 Controller
```csharp
LanguageButton.Pressed += async () =>
{
var settingsModel = this.GetModel<ISettingsModel>()!;
var settings = settingsModel.GetData();
settings.Language = newLanguage;
await this.GetSystem<ISettingsSystem>()!.Apply();
};
```
**问题**
- Controller 臃肿
- 逻辑分散
- 难以复用
## 理解职责边界
### Controller vs Command
| 层级 | 职责 | 示例 |
|----------------|------------|-------------|
| **Controller** | 将用户操作转换为意图 | "用户点击了增加按钮" |
| **Command** | 封装业务逻辑 | "如何增加计数" |
| **Model** | 存储和管理状态 | "计数的值是多少" |
**类比**
- **Controller**:服务员(接收顾客点单)
- **Command**:厨师(制作菜品)
- **Model**:菜单(菜品信息)
### 何时使用 Command
**应该使用 Command**
- 逻辑超过 3 行
- 需要复用的操作
- 涉及多个 Model/System 的协作
- 需要异步操作
- 需要撤销/重做
**不需要 Command**
- 极简单的操作(如 `model.GetData()`
- 纯 UI 逻辑(如切换界面状态)
## 核心收获
通过这次重构,我们学到了:
| 概念 | 解释 |
|----------------|------------------------------|
| **Command 模式** | 将请求封装成对象 |
| **职责分离** | Controller 负责转发Command 负责执行 |
| **逻辑复用** | 同一命令可被多处调用 |
| **可测试性** | 命令可独立测试 |
| **单一职责** | 每个 Command 只做一件事 |
## 对比三个阶段
### 阶段 1基础实现
```csharp
private int _count;
AddButton.Pressed += () =>
{
_count++;
UpdateView();
};
```
**问题**状态、逻辑、UI 混在一起
### 阶段 2引入 Model
```csharp
private ICounterModel _counterModel;
AddButton.Pressed += () =>
{
_counterModel.Increment();
};
```
**改进**:状态抽离到 Model但交互逻辑仍在 Controller
### 阶段 3引入 Command
```csharp
AddButton.Pressed += () =>
{
this.SendCommand(new IncreaseCountCommand());
};
```
**完善**Controller 不再关心"如何",只负责"转发"
## 下一步
现在我们的架构已经很清晰了:
```
View → Controller → Command → Model → Event → View
```
但还有两个问题:
1. **业务规则**:如何实现"计数不能超过 20"
2. **状态响应**:如何实现"计数超过 10 时触发某个逻辑"
这些问题需要 **Utility****System** 来解决。
在下一章中,我们将:
- 引入 **Utility** 处理业务规则
- 引入 **System** 响应状态变化
- 完成完整的架构设计
👉 [第 6 章Utility 与 System](./06-utility-system.md)
---
::: details 本章检查清单
- [ ] IncreaseCountCommand 已创建
- [ ] DecreaseCountCommand 已创建
- [ ] App.cs 使用 SendCommand 替换了直接调用
- [ ] 运行游戏,功能正常
- [ ] 理解了 Command 的职责和优势
- [ ] 理解了 Controller、Command、Model 的职责边界
:::
::: tip 思考题
1. 如果需要实现"撤销"功能,应该如何修改 Command
2. 异步命令(如网络请求)应该如何实现?
3. 多个 Command 需要按顺序执行时,应该怎么做?
这些高级用法可以在后续深入学习!
:::