Skip to content

第 6 章:Utility 与 System

本章将引入架构的最后两个核心概念:Utility(工具类)System(系统),完成我们的架构设计。

新需求

需求 1:计数上限

计数器不能超过 20

需求 2:阈值检查

  • 当 Count > 10 时,输出提示信息
  • 当 Count < -10 时,输出提示信息

方案一:在 Command 中实现(❌ 不推荐)

错误示范

csharp
public class IncreaseCountCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        var model = this.GetModel<ICounterModel>()!;
        
        // ❌ 在 Command 里写业务规则
        if (model.Count >= 20)
        {
            return;  // 超过上限,不执行
        }
        
        model.Increment();
    }
}

问题

  1. 规则写死在 Command 里

    • 如果别的地方也要用"最大 20"的限制怎么办?
    • 规则变更(如改成 100)需要修改业务代码
  2. 无法单独测试规则

    • 只能通过 Command 测试,无法单独测试规则逻辑
  3. 违反单一职责原则

    • Command 应该只负责"执行操作"
    • 不应该负责"业务规则验证"

引入 Utility

Utility 是什么?

Utility(工具类) 提供可复用的无状态逻辑,负责:

✅ 纯函数式的计算和验证
✅ 数据转换和格式化
✅ 业务规则的封装

Utility 不应该

❌ 持有状态
❌ 依赖场景
❌ 直接修改 Model

特点

  • 无状态:只提供计算方法
  • 可复用:任何层都可以调用
  • 可测试:纯函数,易于测试

1. 定义 Utility 接口

scripts/utility/ 创建 ICounterUtility.cs

csharp
using GFramework.Core.Abstractions.utility;

namespace MyGFrameworkGame.scripts.utility;

/// <summary>
/// 计数器工具接口
/// </summary>
public interface ICounterUtility : IContextUtility
{
    /// <summary>
    /// 判断当前值是否可以增加
    /// </summary>
    bool CanIncrease(int current);

    /// <summary>
    /// 将值限制在有效范围内
    /// </summary>
    int Clamp(int value);
}

2. 实现 Utility 类

scripts/utility/ 创建 CounterUtility.cs

csharp
using System;
using GFramework.Core.utility;

namespace MyGFrameworkGame.scripts.utility;

/// <summary>
/// 计数器工具实现
/// </summary>
public class CounterUtility : AbstractContextUtility, ICounterUtility
{
    private readonly int _maxCount;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="maxCount">最大值(默认 20)</param>
    public CounterUtility(int maxCount = 20)
    {
        _maxCount = maxCount;
    }

    /// <summary>
    /// 初始化(可选)
    /// </summary>
    protected override void OnInit()
    {
        // 也可以通过上下文获取配置
    }

    /// <summary>
    /// 判断是否可以继续增加
    /// </summary>
    public bool CanIncrease(int current)
    {
        return current < _maxCount;
    }

    /// <summary>
    /// 将值限制在有效范围内
    /// </summary>
    public int Clamp(int value)
    {
        return Math.Clamp(value, 0, _maxCount);
    }
}

为什么用构造函数传参?

这里使用构造函数传递 maxCount 是为了灵活性。你也可以:

  • 从配置文件读取
  • 通过 architecture.Context.GetUtility() 传入
  • 硬编码在类内部

选择取决于项目需求。

3. 注册 Utility

编辑 scripts/module/UtilityModule.cs

csharp
using GFramework.Core.Abstractions.architecture;
using GFramework.Game.architecture;
using MyGFrameworkGame.scripts.utility;

namespace MyGFrameworkGame.scripts.module;

/// <summary>
/// 工具模块,负责注册所有的工具类
/// </summary>
public class UtilityModule : AbstractModule
{
    public override void Install(IArchitecture architecture)
    {
        // 注册 CounterUtility,最大值设为 20
        architecture.RegisterUtility<ICounterUtility>(new CounterUtility(maxCount: 20));
    }
}

4. 在 Command 中使用 Utility

编辑 IncreaseCountCommand.cs

csharp
using GFramework.Core.command;
using GFramework.Core.extensions;
using MyGFrameworkGame.scripts.model;
using MyGFrameworkGame.scripts.utility;

namespace MyGFrameworkGame.scripts.command;

public class IncreaseCountCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        var model = this.GetModel<ICounterModel>()!;
        var utility = this.GetUtility<ICounterUtility>()!;

        // ✅ 使用 Utility 检查规则
        if (!utility.CanIncrease(model.Count))
        {
            return;  // 超过上限,不执行
        }

        model.Increment();
    }
}

运行游戏

现在点击增加按钮,计数器最多只能到 20

Utility 的优势

1. 规则复用

假设多个地方需要检查上限:

csharp
// Command 中
if (!utility.CanIncrease(model.Count)) return;

// System 中
if (!utility.CanIncrease(value)) { /* ... */ }

// Controller 中
if (utility.CanIncrease(count)) { /* ... */ }

规则只写一次,到处可用!

2. 易于修改

需要改上限为 100?

csharp
// 只需修改注册时的参数
architecture.RegisterUtility<ICounterUtility>(new CounterUtility(maxCount: 100));

不需要改任何业务代码!

3. 易于测试

csharp
[Test]
public void CanIncrease_WhenAtMax_ReturnsFalse()
{
    var utility = new CounterUtility(maxCount: 20);
    
    Assert.IsFalse(utility.CanIncrease(20));
    Assert.IsTrue(utility.CanIncrease(19));
}

纯函数,测试简单!

引入 System

System 是什么?

现在有个新需求:

  • 当 Count > 10 时,输出提示信息 "Count 超过 10"
  • 当 Count < -10 时,输出提示信息 "Count 小于 -10"

这些逻辑应该写在哪里?

❌ 不应该在 Command 里

csharp
protected override void OnExecute()
{
    model.Increment();
    
    // ❌ Command 不应该关心这些"连锁反应"
    if (model.Count > 10) { /* ... */ }
    if (model.Count < -10) { /* ... */ }
}

为什么?

  • Command 只应该关心"行为"
  • 不应该关心"系统状态带来的连锁反应"

✅ 应该在 System 里

System(系统) 负责响应状态变化,执行系统级逻辑。

System 的职责

✅ 监听状态变化
✅ 执行规则检查
✅ 触发系统级反应
✅ 协调多个 Model 的交互

System 不应该

❌ 直接修改 Model(应通过 Command)
❌ 包含 UI 逻辑

1. 定义 System 接口

scripts/system/ 创建 ICounterThresholdSystem.cs

csharp
namespace MyGFrameworkGame.scripts.system;

/// <summary>
/// 计数器阈值检查系统接口
/// </summary>
public interface ICounterThresholdSystem
{
    /// <summary>
    /// 检查当前计数值是否超过阈值
    /// </summary>
    void CheckThreshold(int count);
}

2. 实现 System 类

scripts/system/ 创建 CounterThresholdSystem.cs

csharp
using GFramework.Core.extensions;
using GFramework.Core.system;
using Godot;
using MyGFrameworkGame.scripts.model;

namespace MyGFrameworkGame.scripts.system;

/// <summary>
/// 计数器阈值检查系统
/// </summary>
public class CounterThresholdSystem : AbstractSystem, ICounterThresholdSystem
{
    /// <summary>
    /// 初始化时注册事件监听
    /// </summary>
    protected override void OnInit()
    {
        // 监听计数变化事件
        this.RegisterEvent<CounterModel.ChangedCountEvent>(e =>
        {
            CheckThreshold(e.Count);
        });
    }

    /// <summary>
    /// 检查阈值
    /// </summary>
    public void CheckThreshold(int count)
    {
        if (count > 10)
        {
            GD.Print("Count 超过 10");
        }

        if (count < -10)
        {
            GD.Print("Count 小于 -10");
        }
    }
}

System 的生命周期

AbstractSystem 会在注册时自动调用 OnInit(),所以事件监听会在系统初始化时完成。

3. 注册 System

编辑 scripts/module/SystemModule.cs

csharp
using GFramework.Core.Abstractions.architecture;
using GFramework.Game.architecture;
using MyGFrameworkGame.scripts.system;

namespace MyGFrameworkGame.scripts.module;

/// <summary>
/// 系统模块,负责注册所有的系统逻辑
/// </summary>
public class SystemModule : AbstractModule
{
    public override void Install(IArchitecture architecture)
    {
        // 注册阈值检查系统
        architecture.RegisterSystem<ICounterThresholdSystem>(new CounterThresholdSystem());
    }
}

4. 运行游戏

启动游戏,当计数超过 10 或小于 -10 时,输出面板会显示提示信息:

阈值提示

System 的优势

1. 关注点分离

Command 负责"做什么"

csharp
model.Increment();  // 执行操作

System 负责"响应状态"

csharp
if (count > 10) { /* 触发反应 */ }

2. 逻辑集中

假设将来需求变更:

  • 达到 5 播放音效
  • 达到 15 触发警告
  • 达到 20 锁定按钮

❌ 如果写在 Command

csharp
// 在 N 个 Command 里重复相同逻辑
if (count == 5) PlaySound();
if (count == 15) ShowWarning();
if (count == 20) LockButton();

✅ 集中在 System

csharp
public void CheckThreshold(int count)
{
    if (count == 5) PlaySound();
    if (count == 15) ShowWarning();
    if (count == 20) LockButton();
}

只需修改一处!

3. 支持复杂规则

System 可以协调多个 Model:

csharp
protected override void OnInit()
{
    // 同时监听多个事件
    this.RegisterEvent<CounterChangedEvent>(e => { /* ... */ });
    this.RegisterEvent<ScoreChangedEvent>(e => { /* ... */ });
    
    // 当两个条件同时满足时触发
    if (counter > 10 && score > 100)
    {
        UnlockAchievement();
    }
}

完整架构总览

现在我们的架构已经完整:

┌─────────────┐
│    View     │  Godot UI 节点
└──────┬──────┘

┌──────▼──────┐
│ Controller  │  将用户操作转为命令
└──────┬──────┘

┌──────▼──────┐
│  Command    │  封装业务逻辑
└──────┬──────┘

┌──────▼──────┐
│   Model     │  存储状态,发送事件
└──────┬──────┘

       ├─────────┐
       │         │
┌──────▼──────┐  │
│   Event     │  │
└──────┬──────┘  │
       │         │
┌──────▼──────┐  │
│  System     │  │  响应状态变化
└──────┬──────┘  │
       │         │
┌──────▼──────┐  │
│  Utility    │◄─┘  提供业务规则
└─────────────┘

数据流

用户点击按钮

Controller: SendCommand(IncreaseCommand)

Command: GetUtility().CanIncrease() → 检查规则

Command: GetModel().Increment() → 修改状态

Model: SendEvent(ChangedCountEvent) → 发送事件

    ├→ Controller: RegisterEvent → 更新 UI
    └→ System: RegisterEvent → 检查阈值

各层职责总结

层级职责示例
View呈现 UI,接收输入Label、Button
Controller转发用户意图将点击转为命令
Command执行业务操作增加计数
Model存储状态,发送事件计数器的值
Event通知状态变化ChangedCountEvent
System响应状态,执行规则阈值检查
Utility提供业务规则上限验证

设计原则

单向数据流

Action → Command → Model → Event → View/System
  • 数据总是单向流动
  • 没有循环依赖
  • 易于理解和调试

关注点分离

Controller → "做什么"
Command   → "怎么做"
Model     → "状态是什么"
System    → "状态变化后做什么"
Utility   → "规则是什么"

依赖倒置

都依赖接口,不依赖具体实现

ICounterModel ← CounterModel
ICounterUtility ← CounterUtility
ICounterThresholdSystem ← CounterThresholdSystem

何时使用 Utility vs System

Utility

无状态的计算

csharp
utility.CanIncrease(count)
utility.Clamp(value)
utility.ValidateEmail(email)

业务规则验证

csharp
if (!utility.CanPurchase(player, item)) return;

System

状态驱动的逻辑

csharp
if (count > 10) { /* 触发某些事情 */ }

协调多个 Model

csharp
if (player.Level > 10 && achievement.Count > 5) { /* ... */ }

系统级反应

csharp
PlaySound();
ShowNotification();
UpdateAchievement();

核心收获

通过本章,我们学到了:

概念解释
Utility无状态的业务规则和计算
System响应状态变化,执行系统逻辑
关注点分离每一层专注自己的职责
单向数据流数据流向清晰可控
事件驱动通过事件解耦组件

完整代码回顾

项目结构

MyGFrameworkGame/
├── scripts/
│   ├── architecture/
│   │   └── GameArchitecture.cs
│   ├── module/
│   │   ├── ModelModule.cs
│   │   ├── SystemModule.cs
│   │   └── UtilityModule.cs
│   ├── model/
│   │   ├── ICounterModel.cs
│   │   └── CounterModel.cs
│   ├── command/
│   │   ├── IncreaseCountCommand.cs
│   │   └── DecreaseCountCommand.cs
│   ├── utility/
│   │   ├── ICounterUtility.cs
│   │   └── CounterUtility.cs
│   ├── system/
│   │   ├── ICounterThresholdSystem.cs
│   │   └── CounterThresholdSystem.cs
│   └── app/
│       └── App.cs
├── global/
│   └── GameEntryPoint.cs
└── scenes/
    └── App.tscn

架构层次

GameEntryPoint (入口)

GameArchitecture (架构)

Module (模块)
    ├── ModelModule → CounterModel
    ├── SystemModule → CounterThresholdSystem
    └── UtilityModule → CounterUtility

下一步

恭喜!你已经完成了基础教程的核心内容,掌握了 GFramework 的完整架构设计。

在最后一章中,我们将:

  • 回顾整个架构设计
  • 总结最佳实践
  • 解答常见问题
  • 指引下一步学习方向

👉 第 7 章:总结与最佳实践


本章检查清单
  • [ ] ICounterUtility 和 CounterUtility 已创建
  • [ ] Utility 已注册到 UtilityModule
  • [ ] IncreaseCountCommand 使用 Utility 检查规则
  • [ ] ICounterThresholdSystem 和 CounterThresholdSystem 已创建
  • [ ] System 已注册到 SystemModule
  • [ ] 运行游戏,计数最大为 20
  • [ ] 超过阈值时能看到提示信息
  • [ ] 理解了 Utility 和 System 的职责区别

思考题

  1. 如果需要在多个 System 之间通信,应该怎么做?
  2. Utility 和 System 哪个应该先注册?为什么?
  3. 如何实现"计数为偶数时播放音效"的功能?

这些问题会在进阶教程中探讨!

基于 Apache 2.0 许可证发布