From 60faf8eaff0068f20e82418555fc11ea7c8ef240 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:14:06 +0800 Subject: [PATCH] =?UTF-8?q?docs(core):=20=E9=87=8D=E5=86=99=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E4=B8=93=E9=A2=98=E9=A1=B5=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 architecture、context、lifecycle、command、query 与 cqrs 页面,使其对齐当前公开 API 与初始化语义 - 移除 Init、属性式总线、旧输入赋值示例和已移除的 Mediator 兼容入口等过时说明 - 补充 documentation-governance-and-refresh 主题的恢复点、验证结果与下一步专题页计划 --- ...ntation-governance-and-refresh-tracking.md | 26 +- ...umentation-governance-and-refresh-trace.md | 20 + docs/zh-CN/core/architecture.md | 328 +++----- docs/zh-CN/core/command.md | 487 ++---------- docs/zh-CN/core/context.md | 549 +++----------- docs/zh-CN/core/cqrs.md | 703 +++--------------- docs/zh-CN/core/lifecycle.md | 513 +++---------- docs/zh-CN/core/query.md | 553 ++------------ 8 files changed, 612 insertions(+), 2567 deletions(-) diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index c12904e4..a3f984b8 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -7,20 +7,21 @@ ## 当前恢复点 -- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-002` -- 当前阶段:`Phase 2` +- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-003` +- 当前阶段:`Phase 3` - 当前焦点: - - 已完成 `docs/zh-CN/core/index.md`、`docs/zh-CN/game/index.md` 与 - `docs/zh-CN/source-generators/index.md` 的 landing page 重写 - - 栏目入口已改为以模块定位、包关系、最小接入路径和继续阅读为主,不再沿用旧版失真教程结构 - - 下一轮需要继续核对并重写 `docs/zh-CN/core/*`、`docs/zh-CN/game/*` 与 - `docs/zh-CN/source-generators/*` 的专题页内容 + - 已完成 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md` 与 + `cqrs.md` 的专题页重写 + - `core` 关键专题页已改回当前 `Architecture`、`ArchitectureContext`、旧 Command/Query 兼容层与新 CQRS + runtime 的真实入口语义 + - 下一轮需要继续推进 `docs/zh-CN/core/*` 余下专题页,以及 `docs/zh-CN/game/*`、 + `docs/zh-CN/source-generators/*` 的专题页核对 ## 当前状态摘要 - 文档治理规则已收口到仓库规范,README、站点入口与采用链路不再依赖旧文档自证 -- 高优先级模块入口已补齐,栏目 landing page 已回到可作为默认导航入口的状态 -- 当前主题仍是 active topic,因为核心栏目下的专题页仍可能包含与实现漂移的旧内容 +- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态 +- 当前主题仍是 active topic,因为 `core` 其余专题页及 `game`、`source-generators` 栏目下仍可能包含与实现漂移的旧内容 ## 当前活跃事实 @@ -29,6 +30,8 @@ - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 - `core`、`game` 与 `source-generators` 三个栏目入口页现在都以模块 README 与当前包拆分为准 - `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题 +- `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的 + `RegisterMediatorBehavior` 等过时说明 ## 当前风险 @@ -53,6 +56,7 @@ ## 下一步 -1. 先从 `docs/zh-CN/core/*` 开始,逐页核对架构、上下文、生命周期、命令、查询与 CQRS 的示例和术语 +1. 继续核对 `docs/zh-CN/core/*` 余下专题页,优先处理 `events`、`property`、`state-management`、`coroutine` + 与 `logging` 2. 再推进 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面 -3. 若专题页批量重写完成且验证通过,将本轮 landing page 收口和下一轮专题页修订过程迁入本 topic 的 `archive/` +3. 若专题页批量重写完成且验证通过,将本轮 `core` 专题页收口和后续修订过程迁入本 topic 的 `archive/` diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index f0164e42..bea860e5 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -62,3 +62,23 @@ - `documentation-governance-and-refresh` active tracking 已同步把风险缓解中的参考来源更新为 `ai-libs/` 下已验证参考实现 - 下一次专题页重写时,继续沿用同一表述,不再把特定参考项目名写入新的活跃文档入口 + +### 补充:2026-04-21 Core 专题页收口(RP-003) + +- 复核 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md` 与 `cqrs.md` + 后确认:这些页面仍大量保留旧 API 叙述,例如 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` + 赋值式命令/查询示例,以及已移除的 `RegisterMediatorBehavior` +- 对照 `Architecture`、`ArchitectureContext`、`IArchitectureContext`、`ContextAwareBase`、旧 + `AbstractCommand` / `AbstractQuery` 基类和 `GFramework.Cqrs/README.md` 后,重写上述六个页面 +- 新版专题页将结构统一为“当前角色、真实公开入口、最小示例、兼容边界、迁移方向”,避免继续复刻旧版大而全教程 +- `core/context.md` 已明确把 `GameContext` 收束为兼容回退路径,而不是新代码的推荐接法 +- `core/command.md` 与 `core/query.md` 已明确旧体系仍可用,但新功能应优先走 `GFramework.Cqrs` +- `core/cqrs.md` 已与当前 runtime / generator / handler 注册语义对齐,并明确 `RegisterCqrsPipelineBehavior()` + 是公开入口 +- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页重写没有破坏文档站构建 + +### 下一步 + +1. 继续处理 `docs/zh-CN/core/events.md`、`property.md`、`state-management.md`、`coroutine.md`、`logging.md` +2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现 +3. 完成下一批专题页重写后再次执行 `cd docs && bun run build` diff --git a/docs/zh-CN/core/architecture.md b/docs/zh-CN/core/architecture.md index 671e847a..e1900382 100644 --- a/docs/zh-CN/core/architecture.md +++ b/docs/zh-CN/core/architecture.md @@ -1,239 +1,147 @@ -# Architecture 架构详解 +# Architecture -> 深入了解 GFramework 的核心架构设计和实现 +`Architecture` 是 `GFramework.Core` 的运行时入口。它负责三件事: -## 目录 +- 组织初始化与销毁阶段 +- 接入模型、系统、工具和模块 +- 暴露 `ArchitectureContext` 作为统一上下文入口 -- [概述](#概述) -- [架构设计](#架构设计) -- [生命周期管理](#生命周期管理) -- [组件注册](#组件注册) -- [模块系统](#模块系统) -- [最佳实践](#最佳实践) -- [API 参考](#api-参考) +当前版本的 `Architecture` 已经是协调器外观。对外仍保留稳定的注册与生命周期 API,但内部职责已经拆给专门协作者处理。 -## 概述 +## 你真正会用到的公开入口 -Architecture 是 GFramework 的核心类,负责管理整个应用的生命周期、组件注册和模块管理。从 v1.1.0 开始,Architecture -采用模块化设计,将职责分离到专门的协作者中。 +最常见的成员只有这些: -> 命名约定: -> - `ArchitectureServices` 是公开的基础服务入口,负责容器、事件总线、命令执行器、查询执行器和服务模块管理 -> - `ArchitectureComponentRegistry` 是内部组件注册器,专门负责 System / Model / Utility 的注册与生命周期接入 -> - 两者不是同一层职责,不要混用 +- `OnInitialize()` + - 子类唯一必须实现的入口,用来注册模型、系统、工具、模块和额外的 CQRS 行为 +- `RegisterModel(...)` / `RegisterSystem(...)` / `RegisterUtility(...)` + - 注册运行时组件 +- `InstallModule(...)` + - 安装实现了 `IArchitectureModule` 的模块 +- `RegisterLifecycleHook(...)` + - 注册阶段钩子 +- `RegisterCqrsPipelineBehavior()` + - 注册 CQRS pipeline 行为 +- `RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` + - 显式接入其他程序集中的 CQRS handlers +- `InitializeAsync()` / `WaitUntilReadyAsync()` + - 启动架构并等待进入 `Ready` +- `DestroyAsync()` + - 逆序销毁所有已接入组件 -### 设计目标 - -- **单一职责**: 每个管理器只负责一个明确的功能 -- **类型安全**: 基于泛型的组件获取和注册 -- **生命周期管理**: 自动的初始化和销毁机制 -- **可扩展性**: 支持模块和钩子扩展 -- **向后兼容**: 保持公共 API 稳定 - -### 核心组件 - -``` -Architecture (核心协调器) - ├── ArchitectureBootstrapper (初始化基础设施编排) - ├── ArchitectureLifecycle (生命周期管理) - ├── ArchitectureComponentRegistry (组件注册) - └── ArchitectureModules (模块管理) -``` - -## 架构设计 - -### 设计模式 - -Architecture 采用以下设计模式: - -1. **组合模式 (Composition)**: Architecture 组合多个内部协作者 -2. **委托模式 (Delegation)**: 方法调用委托给专门的管理器 -3. **协调器模式 (Coordinator)**: Architecture 作为协调器统一对外接口 - -### 类图 - -``` -┌─────────────────────────────────────────────────────┐ -│ Architecture │ -│ - _bootstrapper: ArchitectureBootstrapper │ -│ - _lifecycle: ArchitectureLifecycle │ -│ - _componentRegistry: ArchitectureComponentRegistry│ -│ - _modules: ArchitectureModules │ -│ - _logger: ILogger │ -│ │ -│ + RegisterSystem() │ -│ + RegisterModel() │ -│ + RegisterUtility() │ -│ + InstallModule() │ -│ + InitializeAsync() │ -│ + DestroyAsync() │ -│ + event PhaseChanged │ -└─────────────────────────────────────────────────────┘ - │ │ │ │ - │ │ │ │ - ▼ ▼ ▼ ▼ -┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐ -│ Bootstrapper │ │ Lifecycle │ │ComponentReg. │ │ Modules │ -│ │ │ │ │ │ │ │ -│ - 环境初始化 │ │ - 阶段管理 │ │ - System 注册│ │ - 模块安装 │ -│ - 服务准备 │ │ - 钩子管理 │ │ - Model 注册 │ │ - 行为注册 │ -│ - 上下文绑定 │ │ - 组件初始化 │ │ - Utility 注册│ │ │ -│ - 容器冻结 │ │ - 就绪/销毁协调 │ │ - 生命周期接入│ │ │ -└──────────────┘ └──────────────────┘ └──────────────┘ └──────────────┘ -``` - -### 构造函数初始化 - -从 v1.1.0 开始,所有管理器在构造函数中初始化: +## 最小示例 ```csharp -protected Architecture( - IArchitectureConfiguration? configuration = null, - IEnvironment? environment = null, - IArchitectureServices? services = null, - IArchitectureContext? context = null) -{ - var resolvedConfiguration = configuration ?? new ArchitectureConfiguration(); - var resolvedEnvironment = environment ?? new DefaultEnvironment(); - var resolvedServices = services ?? new ArchitectureServices(); - _context = context; +using GFramework.Core.Architectures; - // 初始化 Logger - LoggerFactoryResolver.Provider = resolvedConfiguration.LoggerProperties.LoggerFactoryProvider; - _logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name); - - // 初始化协作者 - _bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger); - _lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger); - _componentRegistry = new ArchitectureComponentRegistry(this, resolvedConfiguration, resolvedServices, _lifecycle, _logger); - _modules = new ArchitectureModules(this, resolvedServices, _logger); -} -``` - -**优势**: - -- 消除 `null!` 断言,提高代码安全性 -- 对象在构造后立即可用 -- 符合"构造即完整"原则 -- 可以在 InitializeAsync 之前访问事件 - -## 生命周期管理 - -### 架构阶段 - -Architecture 定义了 11 个生命周期阶段: - -| 阶段 | 说明 | 触发时机 | -|------------------------|--------------|------------------| -| `None` | 初始状态 | 构造函数完成后 | -| `BeforeUtilityInit` | Utility 初始化前 | 开始初始化 Utility | -| `AfterUtilityInit` | Utility 初始化后 | 所有 Utility 初始化完成 | -| `BeforeModelInit` | Model 初始化前 | 开始初始化 Model | -| `AfterModelInit` | Model 初始化后 | 所有 Model 初始化完成 | -| `BeforeSystemInit` | System 初始化前 | 开始初始化 System | -| `AfterSystemInit` | System 初始化后 | 所有 System 初始化完成 | -| `Ready` | 就绪状态 | 所有组件初始化完成 | -| `Destroying` | 销毁中 | 开始销毁 | -| `Destroyed` | 已销毁 | 销毁完成 | -| `FailedInitialization` | 初始化失败 | 初始化过程中发生异常 | - -### 阶段转换 - -``` -正常流程: -None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit - → BeforeSystemInit → AfterSystemInit → Ready → Destroying → Destroyed - -异常流程: -Any → FailedInitialization -``` - -### 阶段事件 - -可以通过 `PhaseChanged` 事件监听阶段变化: - -```csharp -public class MyArchitecture : Architecture +public sealed class GameArchitecture : Architecture { protected override void OnInitialize() { - // 监听阶段变化 - PhaseChanged += phase => - { - Console.WriteLine($"Phase changed to: {phase}"); - }; + RegisterModel(new PlayerModel()); + RegisterSystem(new CombatSystem()); + RegisterUtility(new SaveUtility()); } } ``` -### 生命周期钩子 - -实现 `IArchitectureLifecycleHook` 接口可以在阶段变化时执行自定义逻辑: +启动方式: ```csharp -public class MyLifecycleHook : IArchitectureLifecycleHook +var architecture = new GameArchitecture(); +await architecture.InitializeAsync(); + +await architecture.WaitUntilReadyAsync(); +``` + +## 初始化时机 + +当前版本不再使用旧文档里的 `Init()` 入口。注册逻辑必须写在: + +```csharp +protected override void OnInitialize() +{ +} +``` + +框架会在 `InitializeAsync()` 内完成: + +1. 基础设施准备 +2. 创建并绑定 `ArchitectureContext` +3. 调用用户的 `OnInitialize()` +4. 按阶段初始化 `Utility -> Model -> System` +5. 进入 `Ready` + +如果你还看到旧示例里写 `protected override void Init()`,那就是过时内容。 + +## 组件注册顺序 + +`Architecture` 仍然维持清晰的组件边界: + +- `Model` + - 承载状态 +- `System` + - 承载业务流程 +- `Utility` + - 承载无状态或基础设施型能力 + +初始化顺序固定为: + +1. `Utility` +2. `Model` +3. `System` + +销毁时会按逆序处理,并优先调用异步销毁接口。 + +## 模块与 CQRS 接入 + +如果你的功能以模块形式组织,优先通过 `InstallModule(...)` 接入,而不是把所有注册逻辑都堆进一个超大的 `OnInitialize()`。 + +如果 handlers 不只在当前架构程序集里,需要显式追加程序集: + +```csharp +protected override void OnInitialize() +{ + RegisterCqrsPipelineBehavior>(); + + RegisterCqrsHandlersFromAssemblies( + [ + typeof(InventoryCqrsMarker).Assembly, + typeof(BattleCqrsMarker).Assembly + ]); +} +``` + +默认运行时会优先尝试消费端程序集上的生成注册表;缺失或不适用时回退到反射扫描。 + +## 阶段与钩子 + +`Architecture` 公开: + +- `CurrentPhase` +- `IsReady` +- `PhaseChanged` +- `RegisterLifecycleHook(...)` + +如果你需要在 `Ready`、`Destroying` 等阶段执行横切逻辑,比起把这类逻辑塞进某个具体 `System`,更适合单独实现 +`IArchitectureLifecycleHook`。 + +```csharp +public sealed class MetricsHook : IArchitectureLifecycleHook { public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) { - switch (phase) + if (phase == ArchitecturePhase.Ready) { - case ArchitecturePhase.Ready: - Console.WriteLine("Architecture is ready!"); - break; - case ArchitecturePhase.Destroying: - Console.WriteLine("Architecture is being destroyed!"); - break; + Console.WriteLine("Architecture ready."); } } } - -// 注册钩子 -architecture.RegisterLifecycleHook(new MyLifecycleHook()); ``` -### 初始化流程 +## 什么时候看别的页面 -``` -1. 创建 Architecture 实例 - └─> 构造函数初始化管理器 - -2. 调用 InitializeAsync() 或 Initialize() - ├─> ArchitectureBootstrapper 准备基础设施 - │ ├─> 初始化环境 (Environment.Initialize()) - │ ├─> 注册内置服务模块 - │ ├─> 初始化架构上下文并绑定 GameContext - │ ├─> 执行服务钩子 - │ └─> 初始化服务模块 - ├─> 调用 OnInitialize() (用户注册组件) - ├─> 初始化所有组件 - │ ├─> BeforeUtilityInit → 初始化 Utility → AfterUtilityInit - │ ├─> BeforeModelInit → 初始化 Model → AfterModelInit - │ └─> BeforeSystemInit → 初始化 System → AfterSystemInit - ├─> CompleteInitialization() 冻结 IoC 容器 - └─> 进入 Ready 阶段 - -3. 等待就绪 (可选) - └─> await architecture.WaitUntilReadyAsync() -``` - -### 销毁流程 - -``` -1. 调用 DestroyAsync() 或 Destroy() - ├─> 检查当前阶段 (如果是 None 或已销毁则直接返回) - ├─> 进入 Destroying 阶段 - ├─> 逆序销毁所有组件 - │ ├─> 优先调用 IAsyncDestroyable.DestroyAsync() - │ └─> 否则调用 IDestroyable.Destroy() - ├─> 销毁服务模块 - ├─> 进入 Destroyed 阶段 - └─> 清空 IoC 容器 -``` - ---- - -**版本**: 1.1.0 -**更新日期**: 2026-03-17 -**相关文档**: - -- [核心框架概述](./index.md) +- 想看上下文 API:转到 [context](./context.md) +- 想看阶段和销毁语义:转到 [lifecycle](./lifecycle.md) +- 想看旧命令 / 查询兼容层:转到 [command](./command.md) 和 [query](./query.md) +- 想看推荐的新请求模型:转到 [cqrs](./cqrs.md) diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md index d18b5513..9372b980 100644 --- a/docs/zh-CN/core/command.md +++ b/docs/zh-CN/core/command.md @@ -1,51 +1,29 @@ -# Command 包使用说明 +# Command -## 概述 +本页只说明 `GFramework.Core.Command` 里的旧命令体系。 -Command 包实现了命令模式(Command Pattern),用于封装用户操作和业务逻辑。通过命令模式,可以将请求封装为对象,实现操作的参数化、队列化、日志记录、撤销等功能。 +它仍然被保留,用来兼容存量代码;但如果你在写新功能,优先使用 [cqrs](./cqrs.md) 里的新请求模型。 -命令系统是 GFramework CQRS 架构的重要组成部分,与事件系统和查询系统协同工作,实现完整的业务逻辑处理流程。 +## 当前仍然可用的基类 -## 核心接口 +旧命令体系当前最常见的三个基类是: -### ICommand +- `AbstractCommand` + - 无输入、无返回值 +- `AbstractCommand` + - 有输入、无返回值 +- `AbstractCommand` + - 有输入、有返回值 -无返回值命令接口,定义了命令的基本契约。 +注意一个和旧文档不同的点:泛型命令现在通过构造函数接收输入,而不是依赖 `Input` 可写属性。 -**核心方法:** +## 无输入命令 ```csharp -void Execute(); // 执行命令 -``` +using GFramework.Core.Command; +using GFramework.Core.Extensions; -### ICommand`` - -带返回值的命令接口,用于需要返回执行结果的命令。 - -**核心方法:** - -```csharp -TResult Execute(); // 执行命令并返回结果 -``` - -## 核心类 - -### AbstractCommand - -无返回值命令的抽象基类,提供了命令的基础实现。它继承自 ContextAwareBase,具有上下文感知能力。 - -**核心方法:** - -```csharp -void ICommand.Execute(); // 实现 ICommand 接口 -protected abstract void OnExecute(); // 抽象执行方法,由子类实现 -``` - -**使用示例:** - -```csharp -// 定义一个无返回值的基础命令 -public class SimpleCommand : AbstractCommand +public sealed class RestoreHealthCommand : AbstractCommand { protected override void OnExecute() { @@ -54,422 +32,93 @@ public class SimpleCommand : AbstractCommand this.SendEvent(new PlayerHealthRestoredEvent()); } } - -// 使用命令 -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class GameController : IController -{ - public void OnRestoreHealthButtonClicked() - { - this.SendCommand(new SimpleCommand()); - } -} ``` -### AbstractCommand`` - -无输入参数但带返回值的命令基类。 - -**核心方法:** +发送方式: ```csharp -TResult ICommand.Execute(); // 实现 ICommand 接口 -protected abstract TResult OnExecute(); // 抽象执行方法,由子类实现 +this.SendCommand(new RestoreHealthCommand()); ``` -**使用示例:** +## 带输入命令 + +旧命令输入类型现在直接复用 CQRS 抽象层里的 `ICommandInput`: ```csharp -// 定义一个无输入但有返回值的命令 -public class GetPlayerHealthQuery : AbstractCommand +using GFramework.Core.Command; +using GFramework.Core.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Command; + +public sealed record DamagePlayerInput(int Amount) : ICommandInput; + +public sealed class DamagePlayerCommand(DamagePlayerInput input) + : AbstractCommand(input) { - protected override int OnExecute() + protected override void OnExecute(DamagePlayerInput input) { var playerModel = this.GetModel(); - return playerModel.Health.Value; - } -} - -// 使用命令 -public class UISystem : AbstractSystem -{ - protected override void OnInit() - { - this.RegisterEvent(OnUpdateUI); - } - - private void OnUpdateUI(UpdateUIEvent e) - { - var health = this.SendCommand(new GetPlayerHealthQuery()); - Console.WriteLine($"Player health: {health}"); + playerModel.Health.Value -= input.Amount; } } ``` -## 命令的生命周期 - -1. **创建命令**:实例化命令对象,传入必要的参数 -2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()` -3. **返回结果**:对于带返回值的命令,返回执行结果 -4. **命令销毁**:命令执行完毕后可以被垃圾回收 - -**注意事项:** - -- 命令应该是无状态的,执行完即可丢弃 -- 避免在命令中保存长期引用 -- 命令执行应该是原子操作 - -### 与 Store 配合使用 - -当某个 Model 内部使用 `Store` 管理复杂聚合状态时,Command 依然是推荐的写入口。 +发送方式: ```csharp -public sealed class DamagePlayerCommand(int amount) : AbstractCommand -{ - protected override void OnExecute() - { - var model = this.GetModel(); - model.Store.Dispatch(new DamagePlayerAction(amount)); - } -} +this.SendCommand(new DamagePlayerCommand(new DamagePlayerInput(10))); ``` -这样可以保持现有职责边界不变: - -- Controller 发送命令 -- Command 执行操作 -- Model 承载状态 -- Store 负责统一归约状态变化 - -完整示例见 [`state-management`](./state-management)。 - -## CommandBus - 命令总线 - -### 功能说明 - -`CommandBus` 是命令执行的核心组件,负责发送和执行命令。 - -**主要方法:** +## 带返回值命令 ```csharp -void Send(ICommand command); // 发送无返回值命令 -TResult Send(ICommand command); // 发送带返回值命令 +using GFramework.Core.Command; +using GFramework.Core.Extensions; +using GFramework.Cqrs.Abstractions.Cqrs.Command; + +public sealed record GetGoldRewardInput(int EnemyLevel) : ICommandInput; + +public sealed class GetGoldRewardCommand(GetGoldRewardInput input) + : AbstractCommand(input) +{ + protected override int OnExecute(GetGoldRewardInput input) + { + return input.EnemyLevel * 10; + } +} ``` -**特点:** - -- 统一的命令执行入口 -- 支持同步命令执行 -- 与架构上下文集成 - -### 使用示例 - ```csharp -// 通过架构获取命令总线 -var commandBus = architecture.Context.CommandBus; - -// 发送无返回值命令 -commandBus.Send(new StartGameCommand(1, "Player1")); - -// 发送带返回值命令 -var damage = commandBus.Send(new CalculateDamageCommand(100, 50)); +var reward = this.SendCommand(new GetGoldRewardCommand(new GetGoldRewardInput(3))); ``` -## 命令基类变体 +## 发送入口 -框架提供了多种命令基类以满足不同需求: +旧命令由 `IArchitectureContext` 的兼容入口执行: -### AbstractCommand`` +- `SendCommand(ICommand)` +- `SendCommand(ICommand)` +- `SendCommandAsync(IAsyncCommand)` +- `SendCommandAsync(IAsyncCommand)` -带输入参数的无返回值命令类。通过 `ICommandInput` 接口传递参数。 - -**核心方法:** +在 `IContextAware` 对象内,通常直接通过扩展使用: ```csharp -void ICommand.Execute(); // 实现 ICommand 接口 -protected abstract void OnExecute(TInput input); // 抽象执行方法,接收输入参数 +using GFramework.Core.Extensions; ``` -**使用示例:** +## 什么时候还应该用旧命令 -```csharp -// 定义输入对象 -public class StartGameInput : ICommandInput -{ - public int LevelId { get; set; } - public string PlayerName { get; set; } -} +- 你在维护既有 `Core.Command` 代码 +- 你的调用链已经依赖旧 `CommandExecutor` +- 当前改动目标是局部修复,不值得同时做 CQRS 迁移 -// 定义命令 -public class StartGameCommand : AbstractCommand -{ - protected override void OnExecute(StartGameInput input) - { - var playerModel = this.GetModel(); - var gameModel = this.GetModel(); +## 什么时候该切到 CQRS - playerModel.PlayerName.Value = input.PlayerName; - gameModel.CurrentLevel.Value = input.LevelId; - gameModel.GameState.Value = GameState.Playing; +下面这些场景更适合新 CQRS runtime: - this.SendEvent(new GameStartedEvent()); - } -} +- 需要 request / notification / stream 的统一模型 +- 需要 pipeline behaviors +- 需要 handler registry 生成器 +- 你正在写新的业务模块,而不是维护历史命令代码 -// 使用命令 -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class GameController : IController -{ - public void OnStartButtonClicked() - { - var input = new StartGameInput { LevelId = 1, PlayerName = "Player1" }; - this.SendCommand(new StartGameCommand { Input = input }); - } -} -``` - -### AbstractCommand`` - -既带输入参数又带返回值的命令类。 - -**核心方法:** - -```csharp -TResult ICommand.Execute(); // 实现 ICommand 接口 -protected abstract TResult OnExecute(TInput input); // 抽象执行方法,接收输入参数 -``` - -**使用示例:** - -```csharp -// 定义输入对象 -public class CalculateDamageInput : ICommandInput -{ - public int AttackerAttackPower { get; set; } - public int DefenderDefense { get; set; } -} - -// 定义命令 -public class CalculateDamageCommand : AbstractCommand -{ - protected override int OnExecute(CalculateDamageInput input) - { - var config = this.GetModel(); - var baseDamage = input.AttackerAttackPower - input.DefenderDefense; - var finalDamage = Math.Max(1, baseDamage * config.DamageMultiplier); - return (int)finalDamage; - } -} - -// 使用命令 -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() { } - - public void Attack(Character attacker, Character defender) - { - var input = new CalculateDamageInput - { - AttackerAttackPower = attacker.AttackPower, - DefenderDefense = defender.Defense - }; - - var damage = this.SendCommand(new CalculateDamageCommand { Input = input }); - defender.Health -= damage; - this.SendEvent(new DamageDealtEvent(attacker, defender, damage)); - } -} -``` - -### AbstractAsyncCommand`` - -支持异步执行的带输入参数的无返回值命令基类。 - -**核心方法:** - -```csharp -Task IAsyncCommand.ExecuteAsync(); // 实现异步命令接口 -protected abstract Task OnExecuteAsync(TInput input); // 抽象异步执行方法 -``` - -### AbstractAsyncCommand`` - -支持异步执行的既带输入参数又带返回值的命令基类。 - -**核心方法:** - -```csharp -Task IAsyncCommand.ExecuteAsync(); // 实现异步命令接口 -protected abstract Task OnExecuteAsync(TInput input); // 抽象异步执行方法 -``` - -**使用示例:** - -```csharp -// 定义输入对象 -public class LoadSaveDataInput : ICommandInput -{ - public string SaveSlot { get; set; } -} - -// 定义异步命令 -public class LoadSaveDataCommand : AbstractAsyncCommand -{ - protected override async Task OnExecuteAsync(LoadSaveDataInput input) - { - var storage = this.GetUtility(); - return await storage.LoadSaveDataAsync(input.SaveSlot); - } -} - -// 使用异步命令 -public class SaveSystem : AbstractSystem -{ - protected override void OnInit() - { - this.RegisterEvent(OnLoadGameRequest); - } - - private async void OnLoadGameRequest(LoadGameRequestEvent e) - { - var input = new LoadSaveDataInput { SaveSlot = e.SaveSlot }; - var saveData = await this.SendCommandAsync(new LoadSaveDataCommand { Input = input }); - - if (saveData != null) - { - this.SendEvent(new GameLoadedEvent { SaveData = saveData }); - } - } -} -``` - -## 命令处理器执行 - -所有发送给命令总线的命令最终都会通过 `CommandExecutor` 来执行: - -```csharp -public class CommandExecutor -{ - public static void Execute(ICommand command) - { - command.Execute(); - } - - public static TResult Execute(ICommand command) - { - return command.Execute(); - } -} -``` - -**特点:** - -- 提供统一的命令执行机制 -- 支持同步和异步命令执行 -- 可以扩展添加中间件逻辑 - -## 使用场景 - -### 1. 用户交互操作 - -```csharp -public class SaveGameCommand : AbstractCommand -{ - private readonly string _saveSlot; - - public SaveGameCommand(string saveSlot) - { - _saveSlot = saveSlot; - } - - protected override void OnExecute() - { - var saveSystem = this.GetSystem(); - var playerModel = this.GetModel(); - - saveSystem.SavePlayerData(playerModel, _saveSlot); - this.SendEvent(new GameSavedEvent(_saveSlot)); - } -} -``` - -### 2. 业务流程控制 - -```csharp -public class LoadLevelCommand : AbstractCommand -{ - private readonly int _levelId; - - public LoadLevelCommand(int levelId) - { - _levelId = levelId; - } - - protected override void OnExecute() - { - var levelSystem = this.GetSystem(); - var uiSystem = this.GetSystem(); - - // 显示加载界面 - uiSystem.ShowLoadingScreen(); - - // 加载关卡 - levelSystem.LoadLevel(_levelId); - - // 发送事件 - this.SendEvent(new LevelLoadedEvent(_levelId)); - } -} -``` - -## 最佳实践 - -1. **保持命令原子性**:一个命令应该完成一个完整的业务操作 -2. **命令无状态**:命令不应该保存长期状态,执行完即可丢弃 -3. **参数通过构造函数传递**:命令需要的参数应在创建时传入 -4. **避免命令嵌套**:命令内部尽量不要发送其他命令,使用事件通信 -5. **合理使用返回值**:只在确实需要返回结果时使用带返回值的命令 -6. **命令命名规范**:使用动词+名词形式,如 `StartGameCommand`、`SavePlayerCommand` -7. **单一职责原则**:每个命令只负责一个特定的业务操作 -8. **使用异步命令**:对于需要长时间执行的操作,使用异步命令避免阻塞 -9. **命令验证**:在命令执行前验证输入参数的有效性 -10. **错误处理**:在命令中适当处理异常情况 - -## 命令模式优势 - -### 1. 可扩展性 - -- 命令可以被序列化和存储 -- 支持命令队列和批处理 -- 便于实现撤销/重做功能 - -### 2. 可测试性 - -- 命令逻辑独立,易于单元测试 -- 可以模拟命令执行结果 -- 支持行为驱动开发 - -### 3. 可维护性 - -- 业务逻辑集中管理 -- 降低组件间耦合度 -- 便于重构和扩展 - -## 相关包 - -- [`architecture`](./architecture.md) - 架构核心,负责命令的分发和执行 -- [`extensions`](./extensions.md) - 提供 `SendCommand()` 扩展方法 -- [`query`](./query.md) - 查询模式,用于数据查询 -- [`events`](./events.md) - 事件系统,命令执行后的通知机制 -- [`system`](./system.md) - 业务系统,命令的主要执行者 -- [`model`](./model.md) - 数据模型,命令操作的数据 - ---- - -**许可证**:Apache 2.0 +迁移后常见写法见:[cqrs](./cqrs.md) diff --git a/docs/zh-CN/core/context.md b/docs/zh-CN/core/context.md index 8866f121..f905e2be 100644 --- a/docs/zh-CN/core/context.md +++ b/docs/zh-CN/core/context.md @@ -1,490 +1,163 @@ -# Context 上下文指南 +# Context -## 概述 +`IArchitectureContext` 是框架的统一上下文入口。 -Context(上下文)是 GFramework 中的核心概念,提供了对架构服务的统一访问入口。通过 Context,组件可以访问事件总线、命令总线、查询总线、IoC -容器等核心服务。 +当前版本的上下文不再以“公开属性总线”作为主要模型,而是以一组明确的方法同时承载: -## 核心接口 +- 组件获取 +- 事件系统 +- 旧 Command / Query 兼容入口 +- 新 CQRS runtime 入口 -### IArchitectureContext +默认实现类型是 `ArchitectureContext`。 -架构上下文接口,定义了对架构服务的访问契约。 +## 先记住一个事实 -**核心属性:** +如果你还在找旧文档里的这些属性: + +- `CommandBus` +- `QueryBus` +- `EventBus` +- `Container` + +那说明你看到的是旧写法。当前推荐入口是方法,不是这些属性式总线。 + +## 组件访问 + +`IArchitectureContext` 直接提供按类型获取组件的方法: ```csharp -IEventBus EventBus { get; } // 事件总线 -ICommandBus CommandBus { get; } // 命令总线 -IQueryBus QueryBus { get; } // 查询总线 -IIocContainer Container { get; } // IoC 容器 -IEnvironment Environment { get; } // 环境配置 -IArchitectureConfiguration Configuration { get; } // 架构配置 -ILogger Logger { get; } // 日志系统 -``` - -## 核心类 - -### ArchitectureContext - -架构上下文的完整实现。 - -**使用示例:** - -```csharp -// 通过架构获取上下文 var context = architecture.Context; -// 访问各个服务 -var eventBus = context.EventBus; -var commandBus = context.CommandBus; -var queryBus = context.QueryBus; -var container = context.Container; -var environment = context.Environment; -var logger = context.Logger; +var model = context.GetModel(); +var system = context.GetSystem(); +var utility = context.GetUtility(); +var service = context.GetService(); ``` -### GameContext +也支持批量获取和按优先级获取: -游戏上下文类,管理架构类型与上下文实例的映射关系。 +- `GetModels()` +- `GetSystems()` +- `GetUtilities()` +- `GetServices()` +- `GetModelsByPriority()` +- `GetSystemsByPriority()` +- `GetUtilitiesByPriority()` +- `GetServicesByPriority()` -**核心方法:** +## 在 `IContextAware` 对象里怎么用 + +大多数业务代码不会手动把 `architecture.Context` 传来传去,而是通过 `IContextAware` 扩展方法访问上下文: ```csharp -// 绑定架构类型到上下文 -static void Bind(IArchitectureContext context) - where TArchitecture : IArchitecture; +using GFramework.Core.Extensions; -// 获取架构类型对应的上下文 -static IArchitectureContext GetContext() - where TArchitecture : IArchitecture; - -// 解绑架构类型 -static void Unbind() - where TArchitecture : IArchitecture; -``` - -## 在组件中使用 Context - -### 在 Model 中使用 - -```csharp -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - - protected override void OnInit() - { - // 通过 Context 访问事件总线 - var context = this.GetContext(); - var eventBus = context.EventBus; - - // 监听生命值变化 - Health.Register(hp => - { - if (hp <= 0) - { - // 发送事件 - eventBus.Send(new PlayerDiedEvent()); - } - }); - } -} -``` - -### 在 System 中使用 - -```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - // 通过 Context 访问各个服务 - var context = this.GetContext(); - var eventBus = context.EventBus; - var commandBus = context.CommandBus; - var container = context.Container; - - // 注册事件监听 - eventBus.Register(OnEnemyAttack); - } - - private void OnEnemyAttack(EnemyAttackEvent e) - { - var context = this.GetContext(); - var playerModel = context.Container.Get(); - - // 处理伤害 - playerModel.Health.Value -= e.Damage; - } -} -``` - -### 在 Command 中使用 - -```csharp -public class StartGameCommand : AbstractCommand +public sealed class DamagePlayerCommand : AbstractCommand { protected override void OnExecute() { - // 通过 Context 访问服务 - var context = this.GetContext(); - var container = context.Container; - var eventBus = context.EventBus; - - var playerModel = container.Get(); - playerModel.Health.Value = playerModel.MaxHealth.Value; - - eventBus.Send(new GameStartedEvent()); + var playerModel = this.GetModel(); + playerModel.Health.Value -= 10; } } ``` -### 在 Query 中使用 +常用扩展包括: + +- `GetModel()` +- `GetSystem()` +- `GetUtility()` +- `GetService()` +- `SendEvent(...)` +- `RegisterEvent(...)` +- `SendCommand(...)` +- `SendQuery(...)` + +## 事件入口 + +框架事件系统仍然由上下文统一暴露: ```csharp -public class GetPlayerHealthQuery : AbstractQuery +context.SendEvent(new PlayerDiedEvent()); + +var unRegister = context.RegisterEvent(static e => { - protected override int OnDo() - { - // 通过 Context 访问容器 - var context = this.GetContext(); - var playerModel = context.Container.Get(); - - return playerModel.Health.Value; - } -} -``` - -## GameContext 的使用 - -### 绑定架构到 GameContext - -```csharp -public class GameArchitecture : Architecture -{ - protected override void Init() - { - // 注册组件 - RegisterModel(new PlayerModel()); - RegisterSystem(new CombatSystem()); - } -} - -// 在应用启动时绑定 -var architecture = new GameArchitecture(); -await architecture.InitializeAsync(); - -// 绑定架构到 GameContext -GameContext.Bind(architecture.Context); -``` - -### 从 GameContext 获取上下文 - -```csharp -// 在任何地方获取架构上下文 -var context = GameContext.GetContext(); - -// 访问服务 -var playerModel = context.Container.Get(); -var eventBus = context.EventBus; -``` - -### 使用 GameContext 的扩展方法 - -```csharp -// 通过扩展方法简化访问 -public static class GameContextExtensions -{ - public static T GetModel(this IArchitectureContext context) - where T : class, IModel - { - return context.Container.Get(); - } - - public static T GetSystem(this IArchitectureContext context) - where T : class, ISystem - { - return context.Container.Get(); - } -} - -// 使用 -var context = GameContext.GetContext(); -var playerModel = context.GetModel(); -var combatSystem = context.GetSystem(); -``` - -## Context 中的服务 - -### EventBus - 事件总线 - -```csharp -var context = architecture.Context; -var eventBus = context.EventBus; - -// 注册事件 -eventBus.Register(e => -{ - Console.WriteLine("Player died!"); + Console.WriteLine("Player died."); }); - -// 发送事件 -eventBus.Send(new PlayerDiedEvent()); ``` -### CommandBus - 命令总线 +在 `IContextAware` 对象里也可以直接用扩展: ```csharp -var context = architecture.Context; -var commandBus = context.CommandBus; - -// 发送命令 -commandBus.Send(new StartGameCommand()); - -// 发送带返回值的命令 -var damage = commandBus.Send(new CalculateDamageCommand { Input = input }); +this.SendEvent(new PlayerDiedEvent()); ``` -### QueryBus - 查询总线 +## 旧 Command / Query 兼容入口 + +当前上下文仍保留旧命令 / 查询体系: + +- `SendCommand(ICommand)` +- `SendCommand(ICommand)` +- `SendCommandAsync(IAsyncCommand)` +- `SendCommandAsync(IAsyncCommand)` +- `SendQuery(IQuery)` +- `SendQueryAsync(IAsyncQuery)` + +这部分入口主要用于兼容存量代码。新功能优先看 [cqrs](./cqrs.md)。 + +## 新 CQRS 入口 + +`IArchitectureContext` 也是当前 CQRS runtime 的主入口。最重要的方法是: + +- `SendRequestAsync(...)` +- `SendRequest(...)` +- `SendAsync(...)` +- `PublishAsync(...)` +- `CreateStream(...)` +- `SendCommandAsync(...)` / `SendQueryAsync(...)` 的 CQRS 重载 + +示例: ```csharp -var context = architecture.Context; -var queryBus = context.QueryBus; - -// 发送查询 -var health = queryBus.Send(new GetPlayerHealthQuery { Input = new EmptyQueryInput() }); +var playerId = await architecture.Context.SendRequestAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); ``` -### Container - IoC 容器 +如果你在 `IContextAware` 对象内部,通常直接用 `GFramework.Cqrs.Extensions` 里的扩展: ```csharp -var context = architecture.Context; -var container = context.Container; +using GFramework.Cqrs.Extensions; -// 获取已注册的组件 -var playerModel = container.Get(); -var combatSystem = container.Get(); - -// 获取所有实现某接口的组件 -var allSystems = container.GetAll(); +var playerId = await this.SendAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); ``` -### Environment - 环境配置 +## `GameContext` 现在是什么角色 -```csharp -var context = architecture.Context; -var environment = context.Environment; +`GameContext` 仍然存在,但已经退到兼容和回退路径。 -// 获取环境值 -var gameMode = environment.Get("GameMode"); -var maxPlayers = environment.Get("MaxPlayers"); +`ContextAwareBase` 在实例未显式注入上下文时,会回退到 `GameContext.GetFirstArchitectureContext()`。这能保证部分旧代码继续工作,但它不是新代码的首选接法。 -// 安全获取值 -if (environment.TryGet("ServerAddress", out var address)) -{ - Console.WriteLine($"Server: {address}"); -} -``` +新代码更推荐: -### Logger - 日志系统 +- 让对象通过框架流程注入 `IArchitectureContext` +- 或使用 `[ContextAware]` 生成路径 +- 或显式从 `architecture.Context` 启动调用链 -```csharp -var context = architecture.Context; -var logger = context.Logger; +## 什么时候需要手动拿 `architecture.Context` -// 记录日志 -logger.Log("Game started"); -logger.LogWarning("Low memory"); -logger.LogError("Failed to load resource"); -``` +以下场景适合直接使用 `architecture.Context`: -## Context 的生命周期 +- 组合根或启动代码 +- 非 `IContextAware` 对象 +- 测试中显式驱动请求和事件 +- 你要清楚地区分“旧 Command / Query 兼容入口”和“新 CQRS 入口” -### 创建 +## 继续阅读 -Context 在架构初始化时自动创建: - -```csharp -var architecture = new GameArchitecture(); -// Context 在这里被创建 -var context = architecture.Context; -``` - -### 使用 - -Context 在架构的整个生命周期中可用: - -```csharp -// 初始化期间 -await architecture.InitializeAsync(); - -// Ready 阶段 -var context = architecture.Context; -var playerModel = context.Container.Get(); - -// 销毁前 -architecture.Destroy(); -``` - -### 销毁 - -Context 随着架构的销毁而销毁: - -```csharp -architecture.Destroy(); -// Context 不再可用 -``` - -## 最佳实践 - -### 1. 通过扩展方法简化访问 - -```csharp -public static class ContextExtensions -{ - public static T GetModel(this IArchitectureContext context) - where T : class, IModel - { - return context.Container.Get(); - } - - public static T GetSystem(this IArchitectureContext context) - where T : class, ISystem - { - return context.Container.Get(); - } - - public static void SendCommand(this IArchitectureContext context, ICommand command) - { - context.CommandBus.Send(command); - } - - public static TResult SendQuery(this IArchitectureContext context, IQuery query) - { - return context.QueryBus.Send(query); - } -} - -// 使用 -var context = architecture.Context; -var playerModel = context.GetModel(); -context.SendCommand(new StartGameCommand()); -``` - -### 2. 缓存 Context 引用 - -```csharp -public class GameSystem : AbstractSystem -{ - private IArchitectureContext _context; - - protected override void OnInit() - { - // 缓存 Context 引用 - _context = this.GetContext(); - - // 后续使用缓存的引用 - _context.EventBus.Register(OnGameStarted); - } - - private void OnGameStarted(GameStartedEvent e) - { - var playerModel = _context.Container.Get(); - } -} -``` - -### 3. 使用 GameContext 实现全局访问 - -```csharp -// 在应用启动时绑定 -public class GameBootstrapper -{ - public async Task StartAsync() - { - var architecture = new GameArchitecture(); - await architecture.InitializeAsync(); - - // 绑定到 GameContext - GameContext.Bind(architecture.Context); - } -} - -// 在任何地方访问 -public class UIController -{ - public void UpdateHealthDisplay() - { - var context = GameContext.GetContext(); - var playerModel = context.Container.Get(); - - // 更新 UI - healthText.text = playerModel.Health.Value.ToString(); - } -} -``` - -### 4. 处理 Context 不可用的情况 - -```csharp -public class SafeGameSystem : AbstractSystem -{ - protected override void OnInit() - { - try - { - var context = this.GetContext(); - if (context == null) - { - Console.WriteLine("Context not available"); - return; - } - - var playerModel = context.Container.Get(); - } - catch (Exception ex) - { - Console.WriteLine($"Error accessing context: {ex.Message}"); - } - } -} -``` - -## Context vs Architecture - -### Architecture - -- **职责**:管理组件的生命周期 -- **作用**:注册、初始化、销毁组件 -- **访问**:通过 `GetArchitecture()` 获取 - -### Context - -- **职责**:提供对架构服务的访问 -- **作用**:访问事件总线、命令总线、查询总线等 -- **访问**:通过 `GetContext()` 获取 - -```csharp -// Architecture 用于管理 -var architecture = GameArchitecture.Interface; -architecture.RegisterModel(new PlayerModel()); - -// Context 用于访问服务 -var context = architecture.Context; -var playerModel = context.Container.Get(); -``` - -## 相关包 - -- [`architecture`](./architecture.md) - 架构核心,创建和管理 Context -- [`ioc`](./ioc.md) - IoC 容器,通过 Context 访问 -- [`events`](./events.md) - 事件总线,通过 Context 访问 -- [`command`](./command.md) - 命令总线,通过 Context 访问 -- [`query`](./query.md) - 查询总线,通过 Context 访问 -- [`environment`](./environment.md) - 环境配置,通过 Context 访问 -- [`logging`](./logging.md) - 日志系统,通过 Context 访问 - ---- - -**许可证**:Apache 2.0 +- 架构入口:[architecture](./architecture.md) +- 生命周期:[lifecycle](./lifecycle.md) +- 旧命令系统:[command](./command.md) +- 旧查询系统:[query](./query.md) +- 新 CQRS runtime:[cqrs](./cqrs.md) diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md index 0c248a96..8d9158ec 100644 --- a/docs/zh-CN/core/cqrs.md +++ b/docs/zh-CN/core/cqrs.md @@ -1,656 +1,171 @@ --- title: CQRS -description: GFramework 内建 CQRS runtime,用统一请求分发、通知发布和流式处理组织业务逻辑。 +description: 当前推荐的新请求模型,统一覆盖 command、query、notification、stream request 和 pipeline behaviors。 --- # CQRS -## 概述 +`GFramework.Cqrs` 是当前推荐的新请求模型 runtime。 -CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,将数据的读取(Query)和修改(Command)操作分离。GFramework -当前内建自有 CQRS runtime,通过统一的请求分发器、通知发布和流式请求管道提供类型安全、解耦的业务逻辑处理方式。 +如果你在写新功能,优先使用这套模型,而不是继续扩展 `GFramework.Core.Command` / `Query` 的兼容层。 -通过 CQRS,你可以将复杂的业务逻辑拆分为独立的命令和查询处理器,每个处理器只负责单一职责,使代码更易于测试和维护。 - -**主要特性**: - -- 命令查询职责分离 -- 内建请求分发与解耦设计 -- 支持管道行为(Behaviors) -- 异步处理支持 -- 与架构系统深度集成 -- 支持流式处理 - -## 接入包 - -按模块安装 CQRS runtime;如果希望在编译期生成 handler 注册表,再额外安装对应的 source generator: +## 安装方式 ```bash dotnet add package GeWuYou.GFramework.Cqrs dotnet add package GeWuYou.GFramework.Cqrs.Abstractions +``` -# 可选:编译期生成 handler registry,减少冷启动反射扫描 +如果你希望消费端程序集在编译期生成 handler registry,再额外安装: + +```bash dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators ``` -## 核心概念 +## 先理解分层 -### Command(命令) +- `GFramework.Cqrs.Abstractions` + - 纯契约层,定义请求、处理器、行为等接口 +- `GFramework.Cqrs` + - 默认 runtime、dispatcher、处理器基类和上下文扩展 +- `GFramework.Cqrs.SourceGenerators` + - 可选生成器,为消费端程序集生成 `ICqrsHandlerRegistry` -命令表示修改系统状态的操作,如创建、更新、删除: +## 最小示例 -```csharp -using GFramework.Cqrs.Command; -using GFramework.Cqrs.Abstractions.Cqrs.Command; +消息基类和处理器基类在不同命名空间: -// 定义命令输入 -public class CreatePlayerInput : ICommandInput -{ - public string Name { get; set; } - public int Level { get; set; } -} +- 消息基类:`GFramework.Cqrs.Command` / `Query` / `Notification` +- 处理器基类:`GFramework.Cqrs.Cqrs.Command` / `Query` / `Notification` -// 定义命令 -public class CreatePlayerCommand : CommandBase -{ - public CreatePlayerCommand(CreatePlayerInput input) : base(input) { } -} -``` - -### Query(查询) - -查询表示读取系统状态的操作,不修改数据: - -```csharp -using GFramework.Cqrs.Query; -using GFramework.Cqrs.Abstractions.Cqrs.Query; - -// 定义查询输入 -public class GetPlayerInput : IQueryInput -{ - public int PlayerId { get; set; } -} - -// 定义查询 -public class GetPlayerQuery : QueryBase -{ - public GetPlayerQuery(GetPlayerInput input) : base(input) { } -} -``` - -### Handler(处理器) - -处理器负责执行命令或查询的具体逻辑: +示例: ```csharp using GFramework.Cqrs.Command; using GFramework.Cqrs.Cqrs.Command; +using GFramework.Cqrs.Abstractions.Cqrs.Command; -// 命令处理器 -public class CreatePlayerCommandHandler : AbstractCommandHandler +public sealed record CreatePlayerInput(string Name) : ICommandInput; + +public sealed class CreatePlayerCommand(CreatePlayerInput input) + : CommandBase(input) { - public override async ValueTask Handle( +} + +public sealed class CreatePlayerCommandHandler + : AbstractCommandHandler +{ + public override ValueTask Handle( CreatePlayerCommand command, CancellationToken cancellationToken) { - var input = command.Input; - var playerModel = this.GetModel(); - - // 创建玩家 - var playerId = playerModel.CreatePlayer(input.Name, input.Level); - - return playerId; + var playerModel = Context.GetModel(); + var playerId = playerModel.Create(command.Input.Name); + return ValueTask.FromResult(playerId); } } ``` -> 说明:消息基类位于 `GFramework.Cqrs.Command` / `Query` / `Notification` 命名空间,而处理器基类位于 -> `GFramework.Cqrs.Cqrs.*` 命名空间。编写最小示例时需要同时引用对应的消息与 handler 命名空间。 +## 发送请求 -### Dispatcher(请求分发器) - -架构上下文会负责将命令、查询和通知路由到对应的处理器: +如果你在 `IContextAware` 对象内部: ```csharp -// 通过架构上下文发送命令 -var command = new CreatePlayerCommand(new CreatePlayerInput -{ - Name = "Player1", - Level = 1 -}); +using GFramework.Cqrs.Extensions; -var playerId = await this.SendAsync(command); +var playerId = await this.SendAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); ``` -## 基本用法 - -### 定义和发送命令 +如果你在组合根或测试里: ```csharp -// 1. 定义命令输入 -public class SaveGameInput : ICommandInput +var playerId = await architecture.Context.SendRequestAsync( + new CreatePlayerCommand(new CreatePlayerInput("Alice"))); +``` + +最常用的上下文入口有: + +- `SendRequestAsync(...)` +- `SendAsync(...)` +- `SendQueryAsync(...)` +- `PublishAsync(...)` +- `CreateStream(...)` + +## 查询、通知和流 + +这套 runtime 不只处理 command,也统一处理: + +- Query + - 读路径请求 +- Notification + - 一对多广播 +- Stream Request + - 返回 `IAsyncEnumerable` + +也就是说,新代码通常不需要再分别设计“命令总线”“查询总线”和另一套通知分发语义。 + +## 注册处理器 + +在标准 `Architecture` 启动路径中,CQRS runtime 会自动接入基础设施。你通常只需要在 `OnInitialize()` 里追加行为或额外程序集: + +```csharp +protected override void OnInitialize() { - public int SlotId { get; set; } - public GameData Data { get; set; } -} + RegisterCqrsPipelineBehavior>(); + RegisterCqrsPipelineBehavior>(); -// 2. 定义命令 -public class SaveGameCommand : CommandBase -{ - public SaveGameCommand(SaveGameInput input) : base(input) { } -} - -// 3. 实现命令处理器 -public class SaveGameCommandHandler : AbstractCommandHandler -{ - public override async ValueTask Handle( - SaveGameCommand command, - CancellationToken cancellationToken) - { - var input = command.Input; - var saveSystem = this.GetSystem(); - - // 保存游戏 - await saveSystem.SaveAsync(input.SlotId, input.Data); - - // 发送事件 - this.SendEvent(new GameSavedEvent { SlotId = input.SlotId }); - - return Unit.Value; - } -} - -// 4. 发送命令 -public async Task SaveGame() -{ - var command = new SaveGameCommand(new SaveGameInput - { - SlotId = 1, - Data = currentGameData - }); - - await this.SendAsync(command); + RegisterCqrsHandlersFromAssemblies( + [ + typeof(InventoryCqrsMarker).Assembly, + typeof(BattleCqrsMarker).Assembly + ]); } ``` -### 定义和发送查询 +默认逻辑会: + +1. 优先使用消费端程序集上的生成注册器 +2. 生成注册器不可用时回退到反射扫描 +3. 对同一程序集去重,避免重复注册 + +## Pipeline Behavior + +如果你需要围绕请求处理流程插入横切逻辑,使用: ```csharp -// 1. 定义查询输入 -public class GetHighScoresInput : IQueryInput -{ - public int Count { get; set; } = 10; -} - -// 2. 定义查询 -public class GetHighScoresQuery : QueryBase> -{ - public GetHighScoresQuery(GetHighScoresInput input) : base(input) { } -} - -// 3. 实现查询处理器 -public class GetHighScoresQueryHandler : AbstractQueryHandler> -{ - public override async ValueTask> Handle( - GetHighScoresQuery query, - CancellationToken cancellationToken) - { - var input = query.Input; - var scoreModel = this.GetModel(); - - // 查询高分榜 - var scores = await scoreModel.GetTopScoresAsync(input.Count); - - return scores; - } -} - -// 4. 发送查询 -public async Task> GetHighScores() -{ - var query = new GetHighScoresQuery(new GetHighScoresInput - { - Count = 10 - }); - - var scores = await this.SendQueryAsync(query); - return scores; -} -``` - -### 注册处理器 - -在架构中注册 CQRS 行为;默认会自动接入当前架构所在程序集和 `GFramework.Core` 程序集中的处理器: - -```csharp -public class GameArchitecture : Architecture -{ - protected override void OnInitialize() - { - // 注册通用开放泛型行为 - RegisterCqrsPipelineBehavior>(); - RegisterCqrsPipelineBehavior>(); - - // 默认只自动扫描当前架构程序集和 GFramework.Core 程序集中的处理器 - } -} -``` - -当前版本会优先使用源码生成的程序集级 handler registry 来注册“当前业务程序集”里的处理器; -如果该程序集没有生成注册器,或者包含生成代码无法合法引用的处理器类型,则会自动回退到运行时反射扫描。 -`GFramework.Core` 等未挂接该生成器的程序集仍会继续走反射扫描。 - -如果处理器位于其他模块或扩展程序集中,需要额外接入对应程序集的处理器注册,而不是只依赖默认接入范围: - -```csharp -public class GameArchitecture : Architecture -{ - protected override void OnInitialize() - { - RegisterCqrsPipelineBehavior>(); - - RegisterCqrsHandlersFromAssemblies( - [ - typeof(InventoryCqrsMarker).Assembly, - typeof(BattleCqrsMarker).Assembly - ]); - } -} -``` - -`RegisterCqrsHandlersFromAssembly(...)` / `RegisterCqrsHandlersFromAssemblies(...)` 会复用与默认启动路径相同的注册逻辑: -优先使用程序集级生成注册器,失败时自动回退到反射扫描;如果同一程序集已经由默认路径或其他模块接入,框架会自动去重,避免重复注册 -handler。 - -`RegisterCqrsPipelineBehavior()` 是唯一保留的公开入口;旧的 `Mediator` 兼容别名与扩展已移除,不再继续维护。 -如果你正在从旧版本迁移,只需要直接改用 `RegisterCqrsPipelineBehavior()`; -旧 `RegisterMediatorBehavior()` 已移除,不再保留兼容入口。 -当前接口支持两种形式: - -- 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求 -- 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior` - -## 高级用法 - -### Request(请求) - -Request 是更通用的消息类型,可以用于任何场景: - -```csharp -using GFramework.Cqrs.Request; -using GFramework.Cqrs.Abstractions.Cqrs.Request; - -// 定义请求输入 -public class ValidatePlayerInput : IRequestInput -{ - public string PlayerName { get; set; } -} - -// 定义请求 -public class ValidatePlayerRequest : RequestBase -{ - public ValidatePlayerRequest(ValidatePlayerInput input) : base(input) { } -} - -// 实现请求处理器 -public class ValidatePlayerRequestHandler : AbstractRequestHandler -{ - public override async ValueTask Handle( - ValidatePlayerRequest request, - CancellationToken cancellationToken) - { - var input = request.Input; - var playerModel = this.GetModel(); - - // 验证玩家名称 - var isValid = await playerModel.IsNameValidAsync(input.PlayerName); - - return isValid; - } -} -``` - -### Notification(通知) - -Notification 用于一对多的消息广播: - -```csharp -using GFramework.Cqrs.Notification; -using GFramework.Cqrs.Abstractions.Cqrs.Notification; - -// 定义通知输入 -public class PlayerLevelUpInput : INotificationInput -{ - public int PlayerId { get; set; } - public int NewLevel { get; set; } -} - -// 定义通知 -public class PlayerLevelUpNotification : NotificationBase -{ - public PlayerLevelUpNotification(PlayerLevelUpInput input) : base(input) { } -} - -// 实现通知处理器 1 -public class AchievementNotificationHandler : AbstractNotificationHandler -{ - public override async ValueTask Handle( - PlayerLevelUpNotification notification, - CancellationToken cancellationToken) - { - var input = notification.Input; - // 检查成就 - CheckLevelAchievements(input.PlayerId, input.NewLevel); - await Task.CompletedTask; - } -} - -// 实现通知处理器 2 -public class RewardNotificationHandler : AbstractNotificationHandler -{ - public override async ValueTask Handle( - PlayerLevelUpNotification notification, - CancellationToken cancellationToken) - { - var input = notification.Input; - // 发放奖励 - GiveRewards(input.PlayerId, input.NewLevel); - await Task.CompletedTask; - } -} - -// 发布通知(所有处理器都会收到) -var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput -{ - PlayerId = 1, - NewLevel = 10 -}); - -await this.PublishAsync(notification); -``` - -### Pipeline Behaviors(管道行为) - -Behaviors 可以在处理器执行前后添加横切关注点: - -```csharp -using GFramework.Core.Abstractions.Cqrs; - -// 日志行为 -public class LoggingBehavior : IPipelineBehavior - where TMessage : IRequest -{ - public async ValueTask Handle( - TMessage message, - MessageHandlerDelegate next, - CancellationToken cancellationToken) - { - var messageName = message.GetType().Name; - Console.WriteLine($"[开始] {messageName}"); - - var response = await next(message, cancellationToken); - - Console.WriteLine($"[完成] {messageName}"); - - return response; - } -} - -// 性能监控行为 -public class PerformanceBehavior : IPipelineBehavior - where TMessage : IRequest -{ - public async ValueTask Handle( - TMessage message, - MessageHandlerDelegate next, - CancellationToken cancellationToken) - { - var stopwatch = Stopwatch.StartNew(); - - var response = await next(message, cancellationToken); - - stopwatch.Stop(); - var elapsed = stopwatch.ElapsedMilliseconds; - - if (elapsed > 100) - { - Console.WriteLine($"警告: {message.GetType().Name} 耗时 {elapsed}ms"); - } - - return response; - } -} - -// 注册行为 RegisterCqrsPipelineBehavior>(); -RegisterCqrsPipelineBehavior>(); ``` -### 验证行为 +适合的场景包括: -```csharp -public class ValidationBehavior : IPipelineBehavior - where TMessage : IRequest -{ - public async ValueTask Handle( - TMessage message, - MessageHandlerDelegate next, - CancellationToken cancellationToken) - { - // 验证输入 - if (message is IValidatable validatable) - { - var errors = validatable.Validate(); - if (errors.Any()) - { - throw new ValidationException(errors); - } - } +- 日志 +- 性能统计 +- 校验 +- 审计 +- 重试或统一异常封装 - return await next(message, cancellationToken); - } -} -``` +旧的 `Mediator` 兼容别名入口已经移除;当前公开入口只有 `RegisterCqrsPipelineBehavior()`。 -### 流式处理 +## 和旧 Command / Query 的关系 -处理大量数据时使用流式处理: +当前仓库同时存在两套路径: -```csharp -// 流式查询 -public class GetAllPlayersStreamQuery : QueryBase> -{ - public GetAllPlayersStreamQuery() : base(new EmptyInput()) { } -} +- 旧路径 + - `GFramework.Core.Command` + - `GFramework.Core.Query` +- 新路径 + - `GFramework.Cqrs` -// 流式查询处理器 -public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler -{ - public override async IAsyncEnumerable Handle( - GetAllPlayersStreamQuery query, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var playerModel = this.GetModel(); +`IArchitectureContext` 仍然会兼容旧入口,但新代码应优先使用 CQRS runtime。 - await foreach (var player in playerModel.GetAllPlayersAsync(cancellationToken)) - { - yield return player; - } - } -} +一个简单判断规则: -// 使用流式查询 -var query = new GetAllPlayersStreamQuery(); -var stream = this.CreateStream(query); +- 在维护历史代码:允许继续使用旧 Command / Query +- 在写新功能或新模块:优先使用 CQRS -await foreach (var player in stream) -{ - Console.WriteLine($"玩家: {player.Name}"); -} -``` +## 继续阅读 -## 最佳实践 - -1. **命令和查询分离**:严格区分修改和读取操作 - ```csharp - ✓ CreatePlayerCommand, GetPlayerQuery // 职责清晰 - ✗ PlayerCommand // 职责不明确 - ``` - -2. **使用有意义的命名**:命令用动词,查询用 Get - ```csharp - ✓ CreatePlayerCommand, UpdateScoreCommand, GetHighScoresQuery - ✗ PlayerCommand, ScoreCommand, ScoresQuery - ``` - -3. **输入验证**:在处理器中验证输入 - ```csharp - public override async ValueTask Handle(...) - { - if (string.IsNullOrEmpty(command.Input.Name)) - throw new ArgumentException("Name is required"); - - // 处理逻辑 - } - ``` - -4. **使用 Behaviors 处理横切关注点**:日志、性能、验证等 - ```csharp - RegisterCqrsPipelineBehavior>(); - RegisterCqrsPipelineBehavior>(); - ``` - -5. **保持处理器简单**:一个处理器只做一件事 - ```csharp - ✓ 处理器只负责业务逻辑,通过架构组件访问数据 - ✗ 处理器中包含复杂的数据访问和业务逻辑 - ``` - -6. **使用 CancellationToken**:支持操作取消 - ```csharp - public override async ValueTask Handle(..., CancellationToken cancellationToken) - { - await someAsyncOperation(cancellationToken); - } - ``` - -## 常见问题 - -### 问题:Command 和 Query 有什么区别? - -**解答**: - -- **Command**:修改系统状态,可能有副作用,通常返回 void 或简单结果 -- **Query**:只读取数据,无副作用,返回查询结果 - -```csharp -// Command: 修改状态 -CreatePlayerCommand -> 创建玩家 -UpdateScoreCommand -> 更新分数 - -// Query: 读取数据 -GetPlayerQuery -> 获取玩家信息 -GetHighScoresQuery -> 获取高分榜 -``` - -### 问题:什么时候使用 Request? - -**解答**: -Request 是更通用的消息类型,当操作既不是纯命令也不是纯查询时使用: - -```csharp -// 验证操作:读取数据并返回结果,但不修改状态 -ValidatePlayerRequest - -// 计算操作:基于输入计算结果 -CalculateDamageRequest -``` - -### 问题:Notification 和 Event 有什么区别? - -**解答**: - -- **Notification**:通过内建 CQRS runtime 发送,处理器在同一请求上下文中执行 -- **Event**:通过 EventBus 发送,监听器异步执行 - -```csharp -// Notification: 同步处理 -await this.PublishAsync(notification); // 等待所有处理器完成 - -// Event: 异步处理 -this.SendEvent(event); // 立即返回,监听器异步执行 -``` - -### 问题:如何处理命令失败? - -**解答**: -使用异常或返回 Result 类型: - -```csharp -// 方式 1: 抛出异常 -public override async ValueTask Handle(...) -{ - if (!IsValid()) - throw new InvalidOperationException("Invalid operation"); - - return Unit.Value; -} - -// 方式 2: 返回 Result -public override async ValueTask Handle(...) -{ - if (!IsValid()) - return Result.Failure("Invalid operation"); - - return Result.Success(); -} -``` - -### 问题:处理器可以调用其他处理器吗? - -**解答**: -可以,通过架构上下文继续发送新的命令或查询: - -```csharp -public override async ValueTask Handle(...) -{ - // 调用其他命令 - await this.SendAsync(new AnotherCommand(...)); - - return Unit.Value; -} -``` - -### 问题:如何测试处理器? - -**解答**: -处理器是独立的类,易于单元测试: - -```csharp -[Test] -public async Task CreatePlayer_ShouldReturnPlayerId() -{ - // Arrange - var handler = new CreatePlayerCommandHandler(); - handler.SetContext(mockContext); - - var command = new CreatePlayerCommand(new CreatePlayerInput - { - Name = "Test", - Level = 1 - }); - - // Act - var playerId = await handler.Handle(command, CancellationToken.None); - - // Assert - Assert.That(playerId, Is.GreaterThan(0)); -} -``` - -## 相关文档 - -- [命令系统](/zh-CN/core/command) - 传统命令模式 -- [查询系统](/zh-CN/core/query) - 传统查询模式 -- [事件系统](/zh-CN/core/events) - 事件驱动架构 -- [协程系统](/zh-CN/core/coroutine) - 在协程中使用 CQRS +- 架构入口:[architecture](./architecture.md) +- 上下文入口:[context](./context.md) +- 模块 README:`GFramework.Cqrs/README.md` diff --git a/docs/zh-CN/core/lifecycle.md b/docs/zh-CN/core/lifecycle.md index 7e13a204..a546070d 100644 --- a/docs/zh-CN/core/lifecycle.md +++ b/docs/zh-CN/core/lifecycle.md @@ -1,461 +1,166 @@ --- title: 生命周期管理 -description: 生命周期管理提供了标准化的组件初始化和销毁机制,确保资源的正确管理和释放。 +description: 当前版本的架构生命周期由阶段模型、初始化顺序、逆序销毁和生命周期钩子共同组成。 --- # 生命周期管理 -## 概述 +`GFramework.Core` 的生命周期由 `Architecture` 统一编排,而不是让每个组件各自决定初始化时机。 -生命周期管理是 GFramework 中用于管理组件初始化和销毁的核心机制。通过实现标准的生命周期接口,组件可以在适当的时机执行初始化逻辑和资源清理,确保系统的稳定性和资源的有效管理。 +你真正需要关注的是: -GFramework 提供了同步和异步两套生命周期接口,适用于不同的使用场景。架构会自动管理所有注册组件的生命周期,开发者只需实现相应的接口即可。 +- 阶段枚举 `ArchitecturePhase` +- 组件初始化顺序 +- 逆序销毁语义 +- `IArchitectureLifecycleHook` -**主要特性**: +## 阶段模型 -- 标准化的初始化和销毁流程 -- 支持同步和异步操作 -- 自动生命周期管理 -- 按注册顺序初始化,按逆序销毁 -- 与架构系统深度集成 +当前公开阶段如下: -## 核心概念 +| 阶段 | 含义 | +| --- | --- | +| `None` | 尚未开始初始化 | +| `BeforeUtilityInit` | 即将初始化工具 | +| `AfterUtilityInit` | 工具初始化完成 | +| `BeforeModelInit` | 即将初始化模型 | +| `AfterModelInit` | 模型初始化完成 | +| `BeforeSystemInit` | 即将初始化系统 | +| `AfterSystemInit` | 系统初始化完成 | +| `Ready` | 架构已完成初始化并可供稳定使用 | +| `Destroying` | 正在销毁 | +| `Destroyed` | 已销毁 | +| `FailedInitialization` | 初始化流程失败 | -### 生命周期接口层次 +正常路径: -GFramework 提供了一套完整的生命周期接口: +```text +None +-> BeforeUtilityInit +-> AfterUtilityInit +-> BeforeModelInit +-> AfterModelInit +-> BeforeSystemInit +-> AfterSystemInit +-> Ready +-> Destroying +-> Destroyed +``` + +## 初始化顺序 + +注册顺序和初始化顺序不是一回事。当前框架会按组件类别统一推进: + +1. `Utility` +2. `Model` +3. `System` + +这保证了大多数系统在初始化时,可以安全依赖已经就绪的工具与模型。 + +启动方式: ```csharp -// 同步接口 -public interface IInitializable -{ - void Initialize(); -} +var architecture = new GameArchitecture(); +await architecture.InitializeAsync(); +await architecture.WaitUntilReadyAsync(); +``` -public interface IDestroyable -{ - void Destroy(); -} +注册逻辑仍然写在 `OnInitialize()`: -public interface ILifecycle : IInitializable, IDestroyable -{ -} - -// 异步接口 -public interface IAsyncInitializable -{ - Task InitializeAsync(); -} - -public interface IAsyncDestroyable -{ - ValueTask DestroyAsync(); -} - -public interface IAsyncLifecycle : IAsyncInitializable, IAsyncDestroyable +```csharp +protected override void OnInitialize() { + RegisterUtility(new SaveUtility()); + RegisterModel(new PlayerModel()); + RegisterSystem(new CombatSystem()); } ``` -### 初始化阶段 +## 销毁语义 -组件在注册到架构后会自动进行初始化: +销毁由 `DestroyAsync()` 统一触发,框架会按逆序回收组件。 + +如果组件实现了异步销毁接口,框架会优先走异步路径。也就是说,新代码应优先实现: + +- `IAsyncDestroyable` +- 或其他已有的异步销毁基类路径 + +同步 `Destroy()` 主要是兼容入口。 + +## 组件自己的生命周期 + +大多数组件不需要手写 `Initialize()`;继承框架基类即可: ```csharp -public class PlayerModel : AbstractModel +public sealed class PlayerModel : AbstractModel { protected override void OnInit() { - // 初始化逻辑 - Console.WriteLine("PlayerModel 初始化"); } } ``` -### 销毁阶段 - -当架构销毁时,所有实现了 `IDestroyable` 的组件会按注册的逆序被销毁: - ```csharp -public class GameSystem : AbstractSystem -{ - public void Destroy() - { - // 清理资源 - Console.WriteLine("GameSystem 销毁"); - } -} -``` - -## 基本用法 - -### 实现同步生命周期 - -最常见的方式是继承框架提供的抽象基类: - -```csharp -using GFramework.Core.Model; - -public class InventoryModel : AbstractModel -{ - private List _items = new(); - - protected override void OnInit() - { - // 初始化库存 - _items = new List(); - Console.WriteLine("库存系统已初始化"); - } -} -``` - -### 实现销毁逻辑 - -对于需要清理资源的组件,实现 `IDestroyable` 接口: - -```csharp -using GFramework.Core.Abstractions.System; -using GFramework.Core.Abstractions.Lifecycle; - -public class AudioSystem : ISystem, IDestroyable -{ - private AudioEngine _engine; - - public void Initialize() - { - _engine = new AudioEngine(); - _engine.Start(); - } - - public void Destroy() - { - // 清理音频资源 - _engine?.Stop(); - _engine?.Dispose(); - _engine = null; - } -} -``` - -### 在架构中注册 - -组件注册后,架构会自动管理其生命周期: - -```csharp -public class GameArchitecture : Architecture -{ - protected override void Init() - { - // 注册顺序:Model -> System -> Utility - RegisterModel(new PlayerModel()); // 1. 初始化 - RegisterModel(new InventoryModel()); // 2. 初始化 - RegisterSystem(new AudioSystem()); // 3. 初始化 - - // 销毁顺序会自动反转: - // AudioSystem -> InventoryModel -> PlayerModel - } -} -``` - -## 高级用法 - -### 异步初始化 - -对于需要异步操作的组件(如加载配置、连接数据库),使用异步生命周期: - -```csharp -using GFramework.Core.Abstractions.Lifecycle; -using GFramework.Core.Abstractions.System; - -public class ConfigurationSystem : ISystem, IAsyncInitializable -{ - private Configuration _config; - - public async Task InitializeAsync() - { - // 异步加载配置文件 - _config = await LoadConfigurationAsync(); - Console.WriteLine("配置已加载"); - } - - private async Task LoadConfigurationAsync() - { - await Task.Delay(100); // 模拟异步操作 - return new Configuration(); - } -} -``` - -### 异步销毁 - -对于需要异步清理的资源(如关闭网络连接、保存数据): - -```csharp -using GFramework.Core.Abstractions.Lifecycle; - -public class NetworkSystem : ISystem, IAsyncDestroyable -{ - private NetworkClient _client; - - public void Initialize() - { - _client = new NetworkClient(); - } - - public async ValueTask DestroyAsync() - { - // 异步关闭连接 - if (_client != null) - { - await _client.DisconnectAsync(); - await _client.DisposeAsync(); - } - Console.WriteLine("网络连接已关闭"); - } -} -``` - -### 完整异步生命周期 - -同时实现异步初始化和销毁: - -```csharp -public class DatabaseSystem : ISystem, IAsyncLifecycle -{ - private DatabaseConnection _connection; - - public async Task InitializeAsync() - { - // 异步连接数据库 - _connection = new DatabaseConnection(); - await _connection.ConnectAsync("connection-string"); - Console.WriteLine("数据库已连接"); - } - - public async ValueTask DestroyAsync() - { - // 异步关闭数据库连接 - if (_connection != null) - { - await _connection.CloseAsync(); - await _connection.DisposeAsync(); - } - Console.WriteLine("数据库连接已关闭"); - } -} -``` - -### 生命周期钩子 - -监听架构的生命周期阶段: - -```csharp -using GFramework.Core.Abstractions.Enums; - -public class AnalyticsSystem : AbstractSystem +public sealed class CombatSystem : AbstractSystem { protected override void OnInit() { - Console.WriteLine("分析系统初始化"); - } - - public override void OnArchitecturePhase(ArchitecturePhase phase) - { - switch (phase) - { - case ArchitecturePhase.Initializing: - Console.WriteLine("架构正在初始化"); - break; - case ArchitecturePhase.Ready: - Console.WriteLine("架构已就绪"); - StartTracking(); - break; - case ArchitecturePhase.Destroying: - Console.WriteLine("架构正在销毁"); - StopTracking(); - break; - } - } - - private void StartTracking() { } - private void StopTracking() { } -} -``` - -## 最佳实践 - -1. **优先使用抽象基类**:继承 `AbstractModel`、`AbstractSystem` 等基类,它们已经实现了生命周期接口 - ```csharp - ✓ public class MyModel : AbstractModel { } - ✗ public class MyModel : IModel, IInitializable { } - ``` - -2. **初始化顺序很重要**:按依赖关系注册组件,被依赖的组件先注册 - ```csharp - protected override void Init() - { - RegisterModel(new ConfigModel()); // 先注册配置 - RegisterModel(new PlayerModel()); // 再注册依赖配置的模型 - RegisterSystem(new GameplaySystem()); // 最后注册系统 - } - ``` - -3. **销毁时释放资源**:实现 `Destroy()` 方法清理非托管资源 - ```csharp - public void Destroy() - { - // 释放事件订阅 - _eventBus.Unsubscribe(OnGameEvent); - - // 释放非托管资源 - _nativeHandle?.Dispose(); - - // 清空引用 - _cache?.Clear(); - } - ``` - -4. **异步操作使用异步接口**:避免在同步方法中阻塞异步操作 - ```csharp - ✓ public async Task InitializeAsync() { await LoadDataAsync(); } - ✗ public void Initialize() { LoadDataAsync().Wait(); } // 可能死锁 - ``` - -5. **避免在初始化中访问其他组件**:初始化顺序可能导致组件尚未就绪 - ```csharp - ✗ protected override void OnInit() - { - var system = this.GetSystem(); // 可能尚未初始化 - } - - ✓ public override void OnArchitecturePhase(ArchitecturePhase phase) - { - if (phase == ArchitecturePhase.Ready) - { - var system = this.GetSystem(); // 安全 - } - } - ``` - -6. **使用 OnArchitecturePhase 处理跨组件依赖**:在 Ready 阶段访问其他组件 - ```csharp - public override void OnArchitecturePhase(ArchitecturePhase phase) - { - if (phase == ArchitecturePhase.Ready) - { - // 此时所有组件都已初始化完成 - var config = this.GetModel(); - ApplyConfiguration(config); - } - } - ``` - -## 常见问题 - -### 问题:什么时候使用同步 vs 异步生命周期? - -**解答**: - -- **同步**:简单的初始化逻辑,如创建对象、设置默认值 -- **异步**:需要 I/O 操作的场景,如加载文件、网络请求、数据库连接 - -```csharp -// 同步:简单初始化 -public class ScoreModel : AbstractModel -{ - protected override void OnInit() - { - Score = 0; // 简单赋值 - } -} - -// 异步:需要 I/O -public class SaveSystem : ISystem, IAsyncInitializable -{ - public async Task InitializeAsync() - { - await LoadSaveDataAsync(); // 文件 I/O } } ``` -### 问题:组件的初始化和销毁顺序是什么? +如果你的组件需要真正的异步初始化或销毁,再补对应接口。 -**解答**: +## 生命周期钩子 -- **初始化顺序**:按注册顺序(先注册先初始化) -- **销毁顺序**:按注册的逆序(后注册先销毁) +当你需要做横切阶段逻辑时,优先实现 `IArchitectureLifecycleHook`,而不是把这些逻辑分散到某个具体 `System` 里。 ```csharp -protected override void Init() +public sealed class MetricsHook : IArchitectureLifecycleHook { - RegisterModel(new A()); // 1. 初始化,3. 销毁 - RegisterModel(new B()); // 2. 初始化,2. 销毁 - RegisterSystem(new C()); // 3. 初始化,1. 销毁 -} -``` - -### 问题:如何在初始化时访问其他组件? - -**解答**: -不要在 `OnInit()` 中访问其他组件,使用 `OnArchitecturePhase()` 在 Ready 阶段访问: - -```csharp -public class DependentSystem : AbstractSystem -{ - protected override void OnInit() - { - // ✗ 不要在这里访问其他组件 - } - - public override void OnArchitecturePhase(ArchitecturePhase phase) + public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) { if (phase == ArchitecturePhase.Ready) { - // ✓ 在这里安全访问其他组件 - var config = this.GetModel(); + Console.WriteLine("Architecture ready."); } } } ``` -### 问题:Destroy() 方法一定会被调用吗? - -**解答**: -只有在正常销毁架构时才会调用。如果应用程序崩溃或被强制终止,`Destroy()` 可能不会被调用。因此: - -- 不要依赖 `Destroy()` 保存关键数据 -- 使用自动保存机制保护重要数据 -- 非托管资源应该实现 `IDisposable` 模式 - -### 问题:可以在 Destroy() 中访问其他组件吗? - -**解答**: -不推荐。销毁时其他组件可能已经被销毁。如果必须访问,确保检查组件是否仍然可用: +注册方式: ```csharp -public void Destroy() -{ - // ✗ 不安全 - var system = this.GetSystem(); - system.DoSomething(); - - // ✓ 安全 - try - { - var system = this.GetSystem(); - system?.DoSomething(); - } - catch - { - // 组件可能已销毁 - } -} +architecture.RegisterLifecycleHook(new MetricsHook()); ``` -## 相关文档 +## 阶段监听 -- [架构组件](/zh-CN/core/architecture) - 架构基础和组件注册 -- [Model 层](/zh-CN/core/model) - 数据模型的生命周期 -- [System 层](/zh-CN/core/system) - 业务系统的生命周期 -- [异步初始化](/zh-CN/core/async-initialization) - 异步架构初始化详解 +如果你只需要观察阶段变化,也可以直接订阅: + +```csharp +architecture.PhaseChanged += phase => +{ + Console.WriteLine($"Phase changed: {phase}"); +}; +``` + +## 什么时候会进入 `FailedInitialization` + +如果初始化流程中抛出异常,架构会切到 `FailedInitialization`。这意味着: + +- `Ready` 不会被触发 +- 后续诊断应先回到启动路径 +- 文档示例不应假设“只要 new 了 Architecture 就一定能跑到 Ready” + +## 推荐做法 + +- 新代码优先使用 `InitializeAsync()` / `DestroyAsync()` +- 把注册逻辑放在 `OnInitialize()`,不要沿用旧文档里的 `Init()` +- 让 `Utility` 承载底层能力,让 `Model` 承载状态,再让 `System` 消费两者 +- 跨组件阶段逻辑优先写成 `IArchitectureLifecycleHook` + +## 继续阅读 + +- 架构入口:[architecture](./architecture.md) +- 上下文入口:[context](./context.md) diff --git a/docs/zh-CN/core/query.md b/docs/zh-CN/core/query.md index f618bcf0..43e31ab7 100644 --- a/docs/zh-CN/core/query.md +++ b/docs/zh-CN/core/query.md @@ -1,532 +1,103 @@ -# Query 包使用说明 +# Query -## 概述 +本页说明 `GFramework.Core.Query` 里的旧查询体系。 -Query 包实现了 CQRS(命令查询职责分离)模式中的查询部分。Query 用于封装数据查询逻辑,与 Command 不同的是,Query -有返回值且不应该修改系统状态。 +和旧命令系统一样,它仍然保留用于兼容存量代码;新功能优先使用 [cqrs](./cqrs.md) 中的新查询模型。 -查询系统是 GFramework CQRS 架构的重要组成部分,专门负责数据读取操作,与命令系统和事件系统协同工作。 +## 当前仍然可用的基类 -## 核心接口 +旧查询体系最常见的两个基类是: -### IQuery`` +- `AbstractQuery` + - 无输入查询 +- `AbstractQuery` + - 带输入查询 -查询接口,定义了查询的基本契约。 +与旧文档不同,带输入查询现在通过构造函数接收输入,不再依赖 `Input` 属性赋值。 -**核心成员:** +## 无输入查询 ```csharp -TResult Do(); // 执行查询并返回结果 -``` +using GFramework.Core.Extensions; +using GFramework.Core.Query; -## 核心类 - -### AbstractQuery`` - -抽象查询基类,提供了查询的基础实现。通过 `IQueryInput` 接口传递参数。 - -**核心方法:** - -```csharp -TResult IQuery.Do(); // 实现 IQuery 接口 -protected abstract TResult OnDo(TInput input); // 抽象查询方法,接收输入参数 -``` - -**使用方式:** - -```csharp -public abstract class AbstractQuery : ContextAwareBase, IQuery - where TInput : IQueryInput +public sealed class GetPlayerHealthQuery : AbstractQuery { - public TResult Do() => OnDo(Input); // 执行查询 - public TInput Input { get; set; } // 输入参数 - protected abstract TResult OnDo(TInput input); // 子类实现查询逻辑 -} -``` - -### AbstractAsyncQuery`` - -支持异步执行的查询基类。 - -**核心方法:** - -```csharp -Task IAsyncQuery.DoAsync(); // 实现异步查询接口 -protected abstract Task OnDoAsync(TInput input); // 抽象异步查询方法 -``` - -### EmptyQueryInput - -空查询输入类,用于表示不需要任何输入参数的查询操作。 - -**使用方式:** - -```csharp -public sealed class EmptyQueryInput : IQueryInput -{ - // 作为占位符使用,适用于那些不需要额外输入参数的查询场景 -} -``` - -### QueryBus - -查询总线实现,负责执行查询并返回结果。 - -**核心方法:** - -```csharp -TResult Send(IQuery query); // 发送并执行查询 -``` - -**使用方式:** - -```csharp -public sealed class QueryBus : IQueryBus -{ - public TResult Send(IQuery query) + protected override int OnDo() { - ArgumentNullException.ThrowIfNull(query); - return query.Do(); + return this.GetModel().Health.Value; } } ``` -## 基本使用 - -### 1. 定义查询 +发送方式: ```csharp -// 定义查询输入 -public class GetPlayerGoldInput : IQueryInput { } +var health = this.SendQuery(new GetPlayerHealthQuery()); +``` -// 查询玩家金币数量 -public class GetPlayerGoldQuery : AbstractQuery -{ - protected override int OnDo(GetPlayerGoldInput input) - { - return this.GetModel().Gold.Value; - } -} +## 带输入查询 -// 定义查询输入 -public class GetItemCountInput : IQueryInput -{ - public string ItemId { get; set; } -} +旧查询输入类型现在直接复用 CQRS 抽象层里的 `IQueryInput`: -// 查询背包中指定物品的数量 -public class GetItemCountQuery : AbstractQuery +```csharp +using GFramework.Core.Extensions; +using GFramework.Core.Query; +using GFramework.Cqrs.Abstractions.Cqrs.Query; + +public sealed record GetItemCountInput(string ItemId) : IQueryInput; + +public sealed class GetItemCountQuery(GetItemCountInput input) + : AbstractQuery(input) { protected override int OnDo(GetItemCountInput input) { - var inventory = this.GetModel(); - return inventory.GetItemCount(input.ItemId); - } -} - -// 定义异步查询输入 -public class LoadPlayerDataInput : IQueryInput -{ - public string PlayerId { get; set; } -} - -// 异步查询玩家数据 -public class LoadPlayerDataQuery : AbstractAsyncQuery -{ - protected override async Task OnDoAsync(LoadPlayerDataInput input) - { - var storage = this.GetUtility(); - return await storage.LoadPlayerDataAsync(input.PlayerId); + var inventoryModel = this.GetModel(); + return inventoryModel.GetItemCount(input.ItemId); } } ``` -### 2. 发送查询 - ```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class ShopUI : IController -{ - [Export] private Button _buyButton; - [Export] private int _itemPrice = 100; - - public void OnReady() - { - _buyButton.Pressed += OnBuyButtonPressed; - } - - private void OnBuyButtonPressed() - { - // 查询玩家金币 - var query = new GetPlayerGoldQuery { Input = new GetPlayerGoldInput() }; - int playerGold = this.SendQuery(query); - - if (playerGold >= _itemPrice) - { - // 发送购买命令 - this.SendCommand(new BuyItemCommand { Input = new BuyItemInput { ItemId = "sword_01" } }); - } - else - { - Console.WriteLine("金币不足!"); - } - } -} +var count = this.SendQuery( + new GetItemCountQuery(new GetItemCountInput("potion"))); ``` -### 3. 在 System 中使用 +## 异步查询 + +上下文仍然保留旧异步查询执行入口: + +- `SendQueryAsync(IAsyncQuery)` + +这主要面向兼容旧 `AsyncQueryExecutor` 路径。文档不再推荐围绕旧 `QueryBus` 设计新功能。 + +## 发送入口 + +旧查询的执行入口是: + +- `SendQuery(IQuery)` +- `SendQueryAsync(IAsyncQuery)` + +在 `IContextAware` 对象内部,通常直接使用 `GFramework.Core.Extensions` 里的扩展: ```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - // 注册事件监听 - this.RegisterEvent(OnEnemyAttack); - } - - private void OnEnemyAttack(EnemyAttackEvent e) - { - // 查询玩家是否已经死亡 - var query = new IsPlayerDeadQuery { Input = new EmptyQueryInput() }; - bool isDead = this.SendQuery(query); - - if (!isDead) - { - // 执行伤害逻辑 - this.SendCommand(new TakeDamageCommand { Input = new TakeDamageInput { Damage = e.Damage } }); - } - } -} - -public class IsPlayerDeadQuery : AbstractQuery -{ - protected override bool OnDo(EmptyQueryInput input) - { - return this.GetModel().Health.Value <= 0; - } -} -``` +using GFramework.Core.Extensions; ``` -## 高级用法 +## 什么时候继续保留旧查询 -### 1. 带参数的复杂查询 +- 你在维护现有 `Core.Query` 代码 +- 当前代码已经建立在旧查询执行器之上 +- 你只想修正局部行为,不想顺手迁移整条调用链 -```csharp -// 定义查询输入 -public class GetEnemiesInRangeInput : IQueryInput -{ - public Vector3 Center { get; set; } - public float Radius { get; set; } -} +## 什么时候改用 CQRS 查询 -// 查询指定范围内的敌人列表 -public class GetEnemiesInRangeQuery : AbstractQuery> -{ - protected override List OnDo(GetEnemiesInRangeInput input) - { - var enemySystem = this.GetSystem(); - return enemySystem.GetEnemiesInRange(input.Center, input.Radius); - } -} +如果你正在写新的读取路径,优先考虑: -// 使用 -var input = new GetEnemiesInRangeInput { Center = playerPosition, Radius = 10.0f }; -var query = new GetEnemiesInRangeQuery { Input = input }; -var enemies = this.SendQuery(query); -``` +- `GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery` +- `AbstractQueryHandler` +- `architecture.Context.SendQueryAsync(...)` -### 2. 组合查询 +原因很简单:新查询路径和命令、通知、流式请求共享同一 dispatcher 与行为管道。 -```csharp -// 定义查询输入 -public class CanUseSkillInput : IQueryInput -{ - public string SkillId { get; set; } -} - -// 查询玩家是否可以使用技能 -public class CanUseSkillQuery : AbstractQuery -{ - protected override bool OnDo(CanUseSkillInput input) - { - var playerModel = this.GetModel(); - - // 查询技能消耗 - var skillCostQuery = new GetSkillCostQuery { Input = new GetSkillCostInput { SkillId = input.SkillId } }; - var skillCost = this.SendQuery(skillCostQuery); - - // 检查是否满足条件 - return playerModel.Mana.Value >= skillCost.ManaCost - && !this.SendQuery(new IsSkillOnCooldownQuery { Input = new IsSkillOnCooldownInput { SkillId = input.SkillId } }); - } -} - -public class GetSkillCostInput : IQueryInput -{ - public string SkillId { get; set; } -} - -public class GetSkillCostQuery : AbstractQuery -{ - protected override SkillCost OnDo(GetSkillCostInput input) - { - return this.GetModel().GetSkillCost(input.SkillId); - } -} - -public class IsSkillOnCooldownInput : IQueryInput -{ - public string SkillId { get; set; } -} - -public class IsSkillOnCooldownQuery : AbstractQuery -{ - protected override bool OnDo(IsSkillOnCooldownInput input) - { - return this.GetModel().IsOnCooldown(input.SkillId); - } -} -``` - -### 3. 聚合数据查询 - -```csharp -// 查询玩家战斗力 -public class GetPlayerPowerQuery : AbstractQuery -{ - protected override int OnDo(EmptyQueryInput input) - { - var playerModel = this.GetModel(); - var equipmentModel = this.GetModel(); - - int basePower = playerModel.Level.Value * 10; - int equipmentPower = equipmentModel.GetTotalPower(); - int buffPower = this.SendQuery(new GetBuffPowerQuery { Input = new EmptyQueryInput() }); - - return basePower + equipmentPower + buffPower; - } -} - -// 查询玩家详细信息(用于UI显示) -public class GetPlayerInfoQuery : AbstractQuery -{ - protected override PlayerInfo OnDo(EmptyQueryInput input) - { - var playerModel = this.GetModel(); - - return new PlayerInfo - { - Name = playerModel.Name.Value, - Level = playerModel.Level.Value, - Health = playerModel.Health.Value, - MaxHealth = playerModel.MaxHealth.Value, - Gold = this.SendQuery(new GetPlayerGoldQuery { Input = new GetPlayerGoldInput() }), - Power = this.SendQuery(new GetPlayerPowerQuery { Input = new EmptyQueryInput() }) - }; - } -} -``` - -### 4. 跨 System 查询 - -```csharp -// 在 AI System 中查询玩家状态 -public class EnemyAISystem : AbstractSystem -{ - protected override void OnInit() { } - - public void UpdateEnemyBehavior(Enemy enemy) - { - // 查询玩家位置 - var playerPosQuery = new GetPlayerPositionQuery { Input = new EmptyQueryInput() }; - var playerPos = this.SendQuery(playerPosQuery); - - // 查询玩家是否在攻击范围内 - var inRangeInput = new IsPlayerInRangeInput { Position = enemy.Position, Range = enemy.AttackRange }; - bool inRange = this.SendQuery(new IsPlayerInRangeQuery { Input = inRangeInput }); - - if (inRange) - { - // 查询是否可以攻击 - var canAttackInput = new CanEnemyAttackInput { EnemyId = enemy.Id }; - bool canAttack = this.SendQuery(new CanEnemyAttackQuery { Input = canAttackInput }); - - if (canAttack) - { - this.SendCommand(new EnemyAttackCommand { Input = new EnemyAttackInput { EnemyId = enemy.Id } }); - } - } - } -} -``` - -## Query 的执行机制 - -所有发送给查询总线的查询最终都会通过 `QueryExecutor` 来执行: - -```csharp -public class QueryExecutor -{ - public static TResult Execute(IQuery query) - { - return query.Do(); - } -} -``` - -**特点:** - -- 提供统一的查询执行机制 -- 支持同步查询执行 -- 与架构上下文集成 - -## Command vs Query - -### Command(命令) - -- **用途**:修改系统状态 -- **返回值**:无返回值(void)或有返回值 -- **示例**:购买物品、造成伤害、升级角色 - -### Query(查询) - -- **用途**:读取数据,不修改状态 -- **返回值**:必须有返回值 -- **示例**:获取金币数量、检查技能冷却、查询玩家位置 - -```csharp -// ❌ 错误:在 Query 中修改状态 -public class BadQuery : AbstractQuery -{ - protected override int OnDo() - { - var model = this.GetModel(); - model.Gold.Value += 100; // 不应该在 Query 中修改数据! - return model.Gold.Value; - } -} - -// ✅ 正确:Query 只读取数据 -public class GoodQuery : AbstractQuery -{ - protected override int OnDo() - { - return this.GetModel().Gold.Value; - } -} - -// ✅ 修改数据应该使用 Command -public class AddGoldCommand : AbstractCommand -{ - private readonly int _amount; - - public AddGoldCommand(int amount) - { - _amount = amount; - } - - protected override void OnExecute() - { - var model = this.GetModel(); - model.Gold.Value += _amount; - } -} -``` - -## 最佳实践 - -1. **查询只读取,不修改** - 保持 Query 的纯粹性 -2. **小而专注** - 每个 Query 只负责一个具体的查询任务 -3. **可组合** - 复杂查询可以通过组合简单查询实现 -4. **避免过度查询** - 如果需要频繁查询,考虑使用 BindableProperty -5. **命名清晰** - Query 名称应该清楚表达查询意图(Get、Is、Can、Has等前缀) -6. **参数通过构造函数传递** - 查询需要的参数应在创建时传入 -7. **查询无状态** - 查询不应该保存长期状态,执行完即可丢弃 -8. **合理使用缓存** - 对于复杂计算,可以在 Model 中缓存结果 - -## 性能优化 - -### 1. 缓存查询结果 - -```csharp -// 在 Model 中缓存复杂计算 -public class PlayerModel : AbstractModel -{ - private int? _cachedPower; - - public int GetPower() - { - if (_cachedPower == null) - { - _cachedPower = CalculatePower(); - } - return _cachedPower.Value; - } - - private int CalculatePower() - { - // 复杂计算... - return 100; - } - - public void InvalidatePowerCache() - { - _cachedPower = null; - } -} -``` - -### 2. 批量查询 - -```csharp -// 一次查询多个数据,而不是多次单独查询 -public class GetMultipleItemCountsQuery : AbstractQuery> -{ - private readonly List _itemIds; - - public GetMultipleItemCountsQuery(List itemIds) - { - _itemIds = itemIds; - } - - protected override Dictionary OnDo() - { - var inventory = this.GetModel(); - return _itemIds.ToDictionary(id => id, id => inventory.GetItemCount(id)); - } -} -``` - -## 查询模式优势 - -### 1. 职责分离 - -- 读写操作明确分离 -- 便于优化读写性能 -- 降低系统复杂度 - -### 2. 可扩展性 - -- 读写可以独立扩展 -- 支持不同的数据存储策略 -- 便于实现读写分离 - -### 3. 可维护性 - -- 查询逻辑集中管理 -- 便于重构和优化 -- 降低组件间耦合 - -## 相关包 - -- [`command`](./command.md) - CQRS 的命令部分 -- [`model`](./model.md) - Query 主要从 Model 获取数据 -- [`system`](./system.md) - System 中可以发送 Query -- **Controller** - Controller 中可以发送 Query(接口定义在 Core.Abstractions 中) -- [`extensions`](./extensions.md) - 提供 SendQuery 扩展方法 -- [`architecture`](./architecture.md) - 架构核心,负责查询的分发和执行 \ No newline at end of file +继续阅读:[cqrs](./cqrs.md)