---
title: 第 5 章：命令系统优化
description: 通过引入 Command 模式拆分 Controller 交互逻辑，建立更清晰的执行职责边界。
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（命令）** 是一种设计模式，它将"请求"封装成对象：

```text
用户操作 → 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.Core.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 不再关心"如何"，只负责"转发"

## 下一步

现在我们的架构已经很清晰了：

```text
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 需要按顺序执行时，应该怎么做？

这些高级用法可以在后续深入学习！
:::
