From 2b4b87babab367a7d127933d14487dfc0a8965e7 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:34:28 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(state):=20=E6=B7=BB=E5=8A=A0=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=E6=A1=86=E6=9E=B6=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 Store 类作为集中式状态容器,默认支持状态归约和订阅通知 - 添加 IReadonlyStore、IStore、IReducer 等状态管理相关抽象接口 - 实现 StoreExtensions 扩展方法,提供 Select 和 ToBindableProperty 选择器功能 - 添加 StoreSelection 类,支持从完整状态树中投影局部状态视图 - 实现 StoreDispatchContext 和 StoreDispatchRecord 用于分发过程诊断 - 添加 IStoreMiddleware 中间件接口,支持在分发过程中插入日志和审计逻辑 - 实现完整的状态选择器和绑定属性桥接功能,便于现有 UI 代码复用 - 添加 Store 相关单元测试,覆盖状态归约、订阅通知和选择器桥接场景 --- .../StateManagement/IReadonlyStore.cs | 40 ++ .../StateManagement/IReducer.cs | 19 + .../StateManagement/IStateSelector.cs | 17 + .../StateManagement/IStore.cs | 17 + .../StateManagement/IStoreDiagnostics.cs | 31 ++ .../StateManagement/IStoreMiddleware.cs | 19 + .../StateManagement/StoreDispatchContext.cs | 55 +++ .../StateManagement/StoreDispatchRecord.cs | 62 +++ .../StateManagement/StoreTests.cs | 362 +++++++++++++++ GFramework.Core/Extensions/StoreExtensions.cs | 90 ++++ GFramework.Core/StateManagement/Store.cs | 434 ++++++++++++++++++ .../StateManagement/StoreSelection.cs | 230 ++++++++++ 12 files changed, 1376 insertions(+) create mode 100644 GFramework.Core.Abstractions/StateManagement/IReadonlyStore.cs create mode 100644 GFramework.Core.Abstractions/StateManagement/IReducer.cs create mode 100644 GFramework.Core.Abstractions/StateManagement/IStateSelector.cs create mode 100644 GFramework.Core.Abstractions/StateManagement/IStore.cs create mode 100644 GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs create mode 100644 GFramework.Core.Abstractions/StateManagement/IStoreMiddleware.cs create mode 100644 GFramework.Core.Abstractions/StateManagement/StoreDispatchContext.cs create mode 100644 GFramework.Core.Abstractions/StateManagement/StoreDispatchRecord.cs create mode 100644 GFramework.Core.Tests/StateManagement/StoreTests.cs create mode 100644 GFramework.Core/Extensions/StoreExtensions.cs create mode 100644 GFramework.Core/StateManagement/Store.cs create mode 100644 GFramework.Core/StateManagement/StoreSelection.cs diff --git a/GFramework.Core.Abstractions/StateManagement/IReadonlyStore.cs b/GFramework.Core.Abstractions/StateManagement/IReadonlyStore.cs new file mode 100644 index 0000000..688d585 --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/IReadonlyStore.cs @@ -0,0 +1,40 @@ +using GFramework.Core.Abstractions.Events; + +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 只读状态容器接口,用于暴露应用状态快照和订阅能力。 +/// 该抽象适用于 Controller、Query、ViewModel 等只需要观察状态的调用方, +/// 使其无需依赖写入能力即可响应复杂状态树的变化。 +/// +/// 状态树的根状态类型。 +public interface IReadonlyStore +{ + /// + /// 获取当前状态快照。 + /// Store 负责保证返回值与最近一次成功分发后的状态一致。 + /// + TState State { get; } + + /// + /// 订阅状态变化通知。 + /// 仅当 Store 判断状态发生有效变化时,才会调用该监听器。 + /// + /// 状态变化时的监听器,参数为新的状态快照。 + /// 用于取消订阅的句柄。 + IUnRegister Subscribe(Action listener); + + /// + /// 订阅状态变化通知,并立即以当前状态调用一次监听器。 + /// 该方法适合在 UI 初始化或 ViewModel 首次绑定时建立同步视图。 + /// + /// 状态变化时的监听器,参数为新的状态快照。 + /// 用于取消订阅的句柄。 + IUnRegister SubscribeWithInitValue(Action listener); + + /// + /// 取消订阅指定的状态监听器。 + /// + /// 需要移除的监听器。 + void UnSubscribe(Action listener); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/StateManagement/IReducer.cs b/GFramework.Core.Abstractions/StateManagement/IReducer.cs new file mode 100644 index 0000000..160281a --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/IReducer.cs @@ -0,0 +1,19 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 定义状态归约器接口。 +/// Reducer 应保持纯函数风格:根据当前状态和 action 计算下一状态, +/// 不直接产生副作用,也不依赖外部可变环境。 +/// +/// 状态树的根状态类型。 +/// 当前 reducer 处理的 action 类型。 +public interface IReducer +{ + /// + /// 根据当前状态和 action 计算下一状态。 + /// + /// 当前状态快照。 + /// 触发本次归约的 action。 + /// 归约后的下一状态。 + TState Reduce(TState currentState, TAction action); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/StateManagement/IStateSelector.cs b/GFramework.Core.Abstractions/StateManagement/IStateSelector.cs new file mode 100644 index 0000000..c0bba6d --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/IStateSelector.cs @@ -0,0 +1,17 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 定义状态选择器接口,用于从整棵状态树中投影出局部状态视图。 +/// 该抽象适用于复用复杂选择逻辑,避免在 UI 或 Controller 中重复编写投影代码。 +/// +/// 源状态类型。 +/// 投影后的局部状态类型。 +public interface IStateSelector +{ + /// + /// 从给定状态中选择目标片段。 + /// + /// 当前完整状态。 + /// 投影后的局部状态。 + TSelected Select(TState state); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/StateManagement/IStore.cs b/GFramework.Core.Abstractions/StateManagement/IStore.cs new file mode 100644 index 0000000..7389f46 --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/IStore.cs @@ -0,0 +1,17 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 可写状态容器接口,提供统一的状态分发入口。 +/// 所有状态变更都应通过分发 action 触发,以保持单向数据流和可测试性。 +/// +/// 状态树的根状态类型。 +public interface IStore : IReadonlyStore +{ + /// + /// 分发一个 action 以触发状态演进。 + /// Store 会按注册顺序执行与该 action 类型匹配的 reducer,并在状态变化后通知订阅者。 + /// + /// action 的具体类型。 + /// 要分发的 action 实例。 + void Dispatch(TAction action); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs b/GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs new file mode 100644 index 0000000..163304a --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/IStoreDiagnostics.cs @@ -0,0 +1,31 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 暴露 Store 的诊断信息。 +/// 该接口用于调试、监控和后续时间旅行能力的扩展,不参与状态写入流程。 +/// +/// 状态树的根状态类型。 +public interface IStoreDiagnostics +{ + /// + /// 获取当前已注册的订阅者数量。 + /// + int SubscriberCount { get; } + + /// + /// 获取最近一次分发的 action 类型。 + /// 即使该次分发未引起状态变化,该值也会更新。 + /// + Type? LastActionType { get; } + + /// + /// 获取最近一次真正改变状态的时间戳。 + /// 若尚未发生状态变化,则返回 。 + /// + DateTimeOffset? LastStateChangedAt { get; } + + /// + /// 获取最近一次分发记录。 + /// + StoreDispatchRecord? LastDispatchRecord { get; } +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/StateManagement/IStoreMiddleware.cs b/GFramework.Core.Abstractions/StateManagement/IStoreMiddleware.cs new file mode 100644 index 0000000..722f64d --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/IStoreMiddleware.cs @@ -0,0 +1,19 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 定义 Store 分发中间件接口。 +/// 中间件用于在 action 分发前后插入日志、诊断、审计或拦截逻辑, +/// 同时保持核心 Store 实现专注于状态归约与订阅通知。 +/// +/// 状态树的根状态类型。 +public interface IStoreMiddleware +{ + /// + /// 执行一次分发管线节点。 + /// 实现通常应调用 继续后续处理;若选择短路, + /// 需要自行保证上下文状态对调用方仍然是可解释的。 + /// + /// 当前分发上下文。 + /// 继续执行后续中间件或 reducer 的委托。 + void Invoke(StoreDispatchContext context, Action next); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/StateManagement/StoreDispatchContext.cs b/GFramework.Core.Abstractions/StateManagement/StoreDispatchContext.cs new file mode 100644 index 0000000..129650a --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/StoreDispatchContext.cs @@ -0,0 +1,55 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 表示一次 Store 分发流程中的上下文数据。 +/// 中间件和 Store 实现通过该对象共享当前 action、分发时间以及归约结果。 +/// +/// 状态树的根状态类型。 +public sealed class StoreDispatchContext +{ + /// + /// 初始化一个新的分发上下文。 + /// + /// 当前分发的 action。 + /// 分发前的状态快照。 + /// 时抛出。 + public StoreDispatchContext(object action, TState previousState) + { + Action = action ?? throw new ArgumentNullException(nameof(action)); + PreviousState = previousState; + NextState = previousState; + DispatchedAt = DateTimeOffset.UtcNow; + } + + /// + /// 获取当前分发的 action 实例。 + /// + public object Action { get; } + + /// + /// 获取当前分发的 action 运行时类型。 + /// + public Type ActionType => Action.GetType(); + + /// + /// 获取分发前的状态快照。 + /// + public TState PreviousState { get; } + + /// + /// 获取或设置归约后的下一状态。 + /// Store 会在 reducer 执行完成后使用该值更新内部状态。 + /// + public TState NextState { get; set; } + + /// + /// 获取或设置本次分发是否导致状态发生变化。 + /// 中间件可读取该值进行日志和诊断,但通常应由 Store 负责最终判定。 + /// + public bool HasStateChanged { get; set; } + + /// + /// 获取本次分发创建时的时间戳。 + /// + public DateTimeOffset DispatchedAt { get; } +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/StateManagement/StoreDispatchRecord.cs b/GFramework.Core.Abstractions/StateManagement/StoreDispatchRecord.cs new file mode 100644 index 0000000..691482c --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/StoreDispatchRecord.cs @@ -0,0 +1,62 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 记录最近一次 Store 分发的结果。 +/// 该结构为调试和诊断提供稳定的只读视图,避免调用方直接依赖 Store 的内部状态。 +/// +/// 状态树的根状态类型。 +public sealed class StoreDispatchRecord +{ + /// + /// 初始化一条分发记录。 + /// + /// 本次分发的 action。 + /// 分发前状态。 + /// 分发后状态。 + /// 是否发生了有效状态变化。 + /// 分发时间。 + /// 时抛出。 + public StoreDispatchRecord( + object action, + TState previousState, + TState nextState, + bool hasStateChanged, + DateTimeOffset dispatchedAt) + { + Action = action ?? throw new ArgumentNullException(nameof(action)); + PreviousState = previousState; + NextState = nextState; + HasStateChanged = hasStateChanged; + DispatchedAt = dispatchedAt; + } + + /// + /// 获取本次分发的 action 实例。 + /// + public object Action { get; } + + /// + /// 获取本次分发的 action 运行时类型。 + /// + public Type ActionType => Action.GetType(); + + /// + /// 获取分发前状态。 + /// + public TState PreviousState { get; } + + /// + /// 获取分发后状态。 + /// + public TState NextState { get; } + + /// + /// 获取本次分发是否产生了有效状态变化。 + /// + public bool HasStateChanged { get; } + + /// + /// 获取分发时间。 + /// + public DateTimeOffset DispatchedAt { get; } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/StateManagement/StoreTests.cs b/GFramework.Core.Tests/StateManagement/StoreTests.cs new file mode 100644 index 0000000..a8693d4 --- /dev/null +++ b/GFramework.Core.Tests/StateManagement/StoreTests.cs @@ -0,0 +1,362 @@ +using GFramework.Core.Abstractions.Property; +using GFramework.Core.Abstractions.StateManagement; +using GFramework.Core.Extensions; +using GFramework.Core.Property; +using GFramework.Core.StateManagement; + +namespace GFramework.Core.Tests.StateManagement; + +/// +/// Store 状态管理能力的单元测试。 +/// 这些测试覆盖集中式状态容器的核心职责:状态归约、订阅通知、选择器桥接和诊断行为。 +/// +[TestFixture] +public class StoreTests +{ + /// + /// 测试 Store 在创建后能够暴露初始状态。 + /// + [Test] + public void State_Should_Return_Initial_State() + { + var store = CreateStore(new CounterState(1, "Player")); + + Assert.That(store.State.Count, Is.EqualTo(1)); + Assert.That(store.State.Name, Is.EqualTo("Player")); + } + + /// + /// 测试 Dispatch 能够执行 reducer 并向订阅者广播新状态。 + /// + [Test] + public void Dispatch_Should_Update_State_And_Notify_Subscribers() + { + var store = CreateStore(); + var receivedStates = new List(); + + store.Subscribe(receivedStates.Add); + + store.Dispatch(new IncrementAction(2)); + + Assert.That(store.State.Count, Is.EqualTo(2)); + Assert.That(receivedStates.Count, Is.EqualTo(1)); + Assert.That(receivedStates[0].Count, Is.EqualTo(2)); + } + + /// + /// 测试当 reducer 返回逻辑相等状态时不会触发通知。 + /// + [Test] + public void Dispatch_Should_Not_Notify_When_State_Does_Not_Change() + { + var store = CreateStore(); + var notifyCount = 0; + + store.Subscribe(_ => notifyCount++); + + store.Dispatch(new RenameAction("Player")); + + Assert.That(store.State.Name, Is.EqualTo("Player")); + Assert.That(notifyCount, Is.EqualTo(0)); + } + + /// + /// 测试同一 action 类型的多个 reducer 会按注册顺序执行。 + /// + [Test] + public void Dispatch_Should_Run_Multiple_Reducers_In_Registration_Order() + { + var store = CreateStore(); + store.RegisterReducer((state, action) => + state with { Count = state.Count + action.Amount * 10 }); + + store.Dispatch(new IncrementAction(1)); + + Assert.That(store.State.Count, Is.EqualTo(11)); + } + + /// + /// 测试 SubscribeWithInitValue 会立即回放当前状态并继续接收后续变化。 + /// + [Test] + public void SubscribeWithInitValue_Should_Replay_Current_State_And_Future_Changes() + { + var store = CreateStore(new CounterState(5, "Player")); + var receivedCounts = new List(); + + store.SubscribeWithInitValue(state => receivedCounts.Add(state.Count)); + store.Dispatch(new IncrementAction(3)); + + Assert.That(receivedCounts, Is.EqualTo(new[] { 5, 8 })); + } + + /// + /// 测试注销订阅后不会再收到后续通知。 + /// + [Test] + public void UnRegister_Handle_Should_Stop_Future_Notifications() + { + var store = CreateStore(); + var notifyCount = 0; + + var unRegister = store.Subscribe(_ => notifyCount++); + store.Dispatch(new IncrementAction(1)); + unRegister.UnRegister(); + store.Dispatch(new IncrementAction(1)); + + Assert.That(notifyCount, Is.EqualTo(1)); + } + + /// + /// 测试选择器仅在所选状态片段变化时触发通知。 + /// + [Test] + public void Select_Should_Only_Notify_When_Selected_Slice_Changes() + { + var store = CreateStore(); + var selectedCounts = new List(); + var selection = store.Select(state => state.Count); + + selection.Register(selectedCounts.Add); + + store.Dispatch(new RenameAction("Renamed")); + store.Dispatch(new IncrementAction(2)); + + Assert.That(selectedCounts, Is.EqualTo(new[] { 2 })); + } + + /// + /// 测试选择器支持自定义比较器,从而抑制无意义的局部状态通知。 + /// + [Test] + public void Select_Should_Respect_Custom_Selected_Value_Comparer() + { + var store = CreateStore(); + var selectedCounts = new List(); + var selection = store.Select( + state => state.Count, + new TensBucketEqualityComparer()); + + selection.Register(selectedCounts.Add); + + store.Dispatch(new IncrementAction(5)); + store.Dispatch(new IncrementAction(6)); + + Assert.That(selectedCounts, Is.EqualTo(new[] { 11 })); + } + + /// + /// 测试 ToBindableProperty 可桥接到现有 BindableProperty 风格用法,并与旧属性系统共存。 + /// + [Test] + public void ToBindableProperty_Should_Work_With_Existing_BindableProperty_Pattern() + { + var store = CreateStore(); + var mirror = new BindableProperty(0); + IReadonlyBindableProperty bindableProperty = store.ToBindableProperty(state => state.Count); + + bindableProperty.Register(value => mirror.Value = value); + store.Dispatch(new IncrementAction(3)); + + Assert.That(mirror.Value, Is.EqualTo(3)); + } + + /// + /// 测试 IStateSelector 接口重载能够复用显式选择逻辑。 + /// + [Test] + public void Select_With_IStateSelector_Should_Project_Selected_Value() + { + var store = CreateStore(); + var selection = store.Select(new CounterNameSelector()); + + Assert.That(selection.Value, Is.EqualTo("Player")); + } + + /// + /// 测试 Store 在中间件内部发生同一实例的嵌套分发时会抛出异常。 + /// + [Test] + public void Dispatch_Should_Throw_When_Nested_Dispatch_Happens_On_Same_Store() + { + var store = CreateStore(); + store.UseMiddleware(new NestedDispatchMiddleware(store)); + + Assert.That( + () => store.Dispatch(new IncrementAction(1)), + Throws.InvalidOperationException.With.Message.Contain("Nested dispatch")); + } + + /// + /// 测试中间件链执行顺序和 Store 诊断信息更新。 + /// + [Test] + public void Dispatch_Should_Run_Middlewares_In_Order_And_Update_Diagnostics() + { + var store = CreateStore(); + var logs = new List(); + + store.UseMiddleware(new RecordingMiddleware(logs, "first")); + store.UseMiddleware(new RecordingMiddleware(logs, "second")); + + store.Dispatch(new IncrementAction(2)); + + Assert.That(logs, Is.EqualTo(new[] + { + "first:before", + "second:before", + "second:after", + "first:after" + })); + + Assert.That(store.LastActionType, Is.EqualTo(typeof(IncrementAction))); + Assert.That(store.LastStateChangedAt, Is.Not.Null); + Assert.That(store.LastDispatchRecord, Is.Not.Null); + Assert.That(store.LastDispatchRecord!.HasStateChanged, Is.True); + Assert.That(store.LastDispatchRecord.NextState.Count, Is.EqualTo(2)); + } + + /// + /// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。 + /// + [Test] + public void Dispatch_Without_Matching_Reducer_Should_Update_Record_Without_Changing_State() + { + var store = CreateStore(); + + store.Dispatch(new NoopAction()); + + Assert.That(store.State.Count, Is.EqualTo(0)); + Assert.That(store.LastActionType, Is.EqualTo(typeof(NoopAction))); + Assert.That(store.LastDispatchRecord, Is.Not.Null); + Assert.That(store.LastDispatchRecord!.HasStateChanged, Is.False); + Assert.That(store.LastStateChangedAt, Is.Null); + } + + /// + /// 创建一个带有基础 reducer 的测试 Store。 + /// + /// 可选初始状态。 + /// 已配置基础 reducer 的 Store 实例。 + private static Store CreateStore(CounterState? initialState = null) + { + var store = new Store(initialState ?? new CounterState(0, "Player")); + store.RegisterReducer((state, action) => state with { Count = state.Count + action.Amount }); + store.RegisterReducer((state, action) => state with { Name = action.Name }); + return store; + } + + /// + /// 用于测试的计数器状态。 + /// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。 + /// + /// 当前计数值。 + /// 当前名称。 + private sealed record CounterState(int Count, string Name); + + /// + /// 表示增加计数的 action。 + /// + /// 要增加的数量。 + private sealed record IncrementAction(int Amount); + + /// + /// 表示修改名称的 action。 + /// + /// 新的名称。 + private sealed record RenameAction(string Name); + + /// + /// 表示没有匹配 reducer 的 action,用于验证无变更分发路径。 + /// + private sealed record NoopAction; + + /// + /// 显式选择器实现,用于验证 IStateSelector 重载。 + /// + private sealed class CounterNameSelector : IStateSelector + { + /// + /// 从状态中选择名称字段。 + /// + /// 完整状态。 + /// 名称字段。 + public string Select(CounterState state) + { + return state.Name; + } + } + + /// + /// 将计数值按十位分桶比较的测试比较器。 + /// 该比较器用于验证选择器只在局部状态“语义变化”时才触发通知。 + /// + private sealed class TensBucketEqualityComparer : IEqualityComparer + { + /// + /// 判断两个值是否落在同一个十位分桶中。 + /// + /// 左侧值。 + /// 右侧值。 + /// 若位于同一分桶则返回 ,否则返回 + public bool Equals(int x, int y) + { + return x / 10 == y / 10; + } + + /// + /// 返回基于十位分桶的哈希码。 + /// + /// 目标值。 + /// 分桶哈希码。 + public int GetHashCode(int obj) + { + return obj / 10; + } + } + + /// + /// 记录中间件调用顺序的测试中间件。 + /// + private sealed class RecordingMiddleware(List logs, string name) : IStoreMiddleware + { + /// + /// 记录当前中间件在分发前后的调用顺序。 + /// + /// 当前分发上下文。 + /// 后续处理节点。 + public void Invoke(StoreDispatchContext context, Action next) + { + logs.Add($"{name}:before"); + next(); + logs.Add($"{name}:after"); + } + } + + /// + /// 在中间件阶段尝试二次分发的测试中间件,用于验证重入保护。 + /// + private sealed class NestedDispatchMiddleware(Store store) : IStoreMiddleware + { + /// + /// 标记是否已经触发过一次嵌套分发,避免因测试实现本身导致无限递归。 + /// + private bool _hasTriggered; + + /// + /// 在第一次进入中间件时执行嵌套分发。 + /// + /// 当前分发上下文。 + /// 后续处理节点。 + public void Invoke(StoreDispatchContext context, Action next) + { + if (!_hasTriggered) + { + _hasTriggered = true; + store.Dispatch(new IncrementAction(1)); + } + + next(); + } + } +} \ No newline at end of file diff --git a/GFramework.Core/Extensions/StoreExtensions.cs b/GFramework.Core/Extensions/StoreExtensions.cs new file mode 100644 index 0000000..dc74fa4 --- /dev/null +++ b/GFramework.Core/Extensions/StoreExtensions.cs @@ -0,0 +1,90 @@ +using GFramework.Core.Abstractions.Property; +using GFramework.Core.Abstractions.StateManagement; +using GFramework.Core.StateManagement; + +namespace GFramework.Core.Extensions; + +/// +/// 为 Store 提供选择器和 BindableProperty 风格桥接扩展。 +/// 这些扩展用于在集中式状态容器和现有 Property/UI 生态之间建立最小侵入的互操作层。 +/// +public static class StoreExtensions +{ + /// + /// 从 Store 中选择一个局部状态视图。 + /// + /// 源状态类型。 + /// 局部状态类型。 + /// 源 Store。 + /// 状态选择委托。 + /// 可用于订阅局部状态变化的只读绑定视图。 + public static StoreSelection Select( + this IReadonlyStore store, + Func selector) + { + ArgumentNullException.ThrowIfNull(store); + ArgumentNullException.ThrowIfNull(selector); + + return new StoreSelection(store, selector); + } + + /// + /// 从 Store 中选择一个局部状态视图,并指定局部状态比较器。 + /// + /// 源状态类型。 + /// 局部状态类型。 + /// 源 Store。 + /// 状态选择委托。 + /// 用于比较局部状态是否变化的比较器。 + /// 可用于订阅局部状态变化的只读绑定视图。 + public static StoreSelection Select( + this IReadonlyStore store, + Func selector, + IEqualityComparer? comparer) + { + ArgumentNullException.ThrowIfNull(store); + ArgumentNullException.ThrowIfNull(selector); + + return new StoreSelection(store, selector, comparer); + } + + /// + /// 使用显式选择器对象从 Store 中选择一个局部状态视图。 + /// + /// 源状态类型。 + /// 局部状态类型。 + /// 源 Store。 + /// 状态选择器实例。 + /// 用于比较局部状态是否变化的比较器。 + /// 可用于订阅局部状态变化的只读绑定视图。 + public static StoreSelection Select( + this IReadonlyStore store, + IStateSelector selector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(store); + ArgumentNullException.ThrowIfNull(selector); + + return new StoreSelection(store, selector.Select, comparer); + } + + /// + /// 将 Store 中选中的局部状态桥接为 IReadonlyBindableProperty 风格接口。 + /// + /// 源状态类型。 + /// 局部状态类型。 + /// 源 Store。 + /// 状态选择委托。 + /// 用于比较局部状态是否变化的比较器。 + /// 只读绑定属性视图。 + public static IReadonlyBindableProperty ToBindableProperty( + this IReadonlyStore store, + Func selector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(store); + ArgumentNullException.ThrowIfNull(selector); + + return new StoreSelection(store, selector, comparer); + } +} \ No newline at end of file diff --git a/GFramework.Core/StateManagement/Store.cs b/GFramework.Core/StateManagement/Store.cs new file mode 100644 index 0000000..12c9a51 --- /dev/null +++ b/GFramework.Core/StateManagement/Store.cs @@ -0,0 +1,434 @@ +using GFramework.Core.Abstractions.Events; +using GFramework.Core.Abstractions.StateManagement; +using GFramework.Core.Events; + +namespace GFramework.Core.StateManagement; + +/// +/// 集中式状态容器的默认实现,用于统一管理复杂状态树的读取、归约和订阅通知。 +/// 该类型定位于现有 BindableProperty 之上的可选能力,适合跨模块共享、需要统一变更入口 +/// 或需要中间件/诊断能力的状态场景,而不是替代所有简单字段级响应式属性。 +/// +/// 状态树的根状态类型。 +public class Store : IStore, IStoreDiagnostics +{ + /// + /// 当前状态变化订阅者列表。 + /// 使用列表而不是委托链,便于精确维护订阅数量并生成稳定的快照调用序列。 + /// + private readonly List> _listeners = []; + + /// + /// Store 内部所有可变状态的同步锁。 + /// 该锁同时保护订阅集合、reducer 注册表和分发过程,确保状态演进是串行且可预测的。 + /// + private readonly object _lock = new(); + + /// + /// 已注册的中间件链,按添加顺序执行。 + /// + private readonly List> _middlewares = []; + + /// + /// 按 action 具体运行时类型组织的 reducer 注册表。 + /// Store 采用精确类型匹配策略,保证 reducer 执行顺序和行为保持确定性。 + /// + private readonly Dictionary> _reducers = []; + + /// + /// 用于判断状态是否发生有效变化的比较器。 + /// + private readonly IEqualityComparer _stateComparer; + + /// + /// 标记当前 Store 是否正在执行分发。 + /// 该标记用于阻止同一 Store 的重入分发,避免产生难以推导的执行顺序和状态回滚问题。 + /// + private bool _isDispatching; + + /// + /// 最近一次分发的 action 类型。 + /// + private Type? _lastActionType; + + /// + /// 最近一次分发记录。 + /// + private StoreDispatchRecord? _lastDispatchRecord; + + /// + /// 最近一次真正改变状态的时间戳。 + /// + private DateTimeOffset? _lastStateChangedAt; + + /// + /// 当前 Store 持有的状态快照。 + /// + private TState _state; + + /// + /// 初始化一个新的 Store。 + /// + /// Store 的初始状态。 + /// 状态比较器;未提供时使用 。 + public Store(TState initialState, IEqualityComparer? comparer = null) + { + _state = initialState; + _stateComparer = comparer ?? EqualityComparer.Default; + } + + /// + /// 获取当前状态快照。 + /// + public TState State + { + get + { + lock (_lock) + { + return _state; + } + } + } + + /// + /// 订阅状态变化通知。 + /// + /// 状态变化时的监听器。 + /// 用于取消订阅的句柄。 + /// 时抛出。 + public IUnRegister Subscribe(Action listener) + { + ArgumentNullException.ThrowIfNull(listener); + + lock (_lock) + { + _listeners.Add(listener); + } + + return new DefaultUnRegister(() => UnSubscribe(listener)); + } + + /// + /// 订阅状态变化通知,并立即回放当前状态。 + /// + /// 状态变化时的监听器。 + /// 用于取消订阅的句柄。 + /// 时抛出。 + public IUnRegister SubscribeWithInitValue(Action listener) + { + ArgumentNullException.ThrowIfNull(listener); + + var currentState = State; + listener(currentState); + return Subscribe(listener); + } + + /// + /// 取消订阅指定监听器。 + /// + /// 需要移除的监听器。 + /// 时抛出。 + public void UnSubscribe(Action listener) + { + ArgumentNullException.ThrowIfNull(listener); + + lock (_lock) + { + _listeners.Remove(listener); + } + } + + /// + /// 分发一个 action 并按顺序执行匹配的 reducer。 + /// + /// action 的具体类型。 + /// 要分发的 action。 + /// 时抛出。 + /// 当同一 Store 发生重入分发时抛出。 + public void Dispatch(TAction action) + { + ArgumentNullException.ThrowIfNull(action); + + Action[] listenersSnapshot = Array.Empty>(); + StoreDispatchContext? context = null; + + lock (_lock) + { + EnsureNotDispatching(); + _isDispatching = true; + + try + { + context = new StoreDispatchContext(action!, _state); + + // 在锁内串行执行完整分发流程,确保 reducer 与中间件看到的是一致的状态序列, + // 并且不会因为并发写入导致 reducer 顺序失效。 + ExecuteDispatchPipeline(context); + + _lastActionType = context.ActionType; + _lastDispatchRecord = new StoreDispatchRecord( + context.Action, + context.PreviousState, + context.NextState, + context.HasStateChanged, + context.DispatchedAt); + + if (!context.HasStateChanged) + { + return; + } + + _state = context.NextState; + _lastStateChangedAt = context.DispatchedAt; + listenersSnapshot = _listeners.Count > 0 ? _listeners.ToArray() : Array.Empty>(); + } + finally + { + _isDispatching = false; + } + } + + // 始终在锁外通知订阅者,避免监听器内部读取 Store 或执行额外逻辑时产生死锁。 + foreach (var listener in listenersSnapshot) + { + listener(context!.NextState); + } + } + + /// + /// 获取当前订阅者数量。 + /// + public int SubscriberCount + { + get + { + lock (_lock) + { + return _listeners.Count; + } + } + } + + /// + /// 获取最近一次分发的 action 类型。 + /// + public Type? LastActionType + { + get + { + lock (_lock) + { + return _lastActionType; + } + } + } + + /// + /// 获取最近一次真正改变状态的时间戳。 + /// + public DateTimeOffset? LastStateChangedAt + { + get + { + lock (_lock) + { + return _lastStateChangedAt; + } + } + } + + /// + /// 获取最近一次分发记录。 + /// + public StoreDispatchRecord? LastDispatchRecord + { + get + { + lock (_lock) + { + return _lastDispatchRecord; + } + } + } + + /// + /// 注册一个强类型 reducer。 + /// 同一 action 类型可注册多个 reducer,它们会按照注册顺序依次归约状态。 + /// + /// reducer 处理的 action 类型。 + /// 要注册的 reducer 实例。 + /// 当前 Store 实例,便于链式配置。 + /// 时抛出。 + public Store RegisterReducer(IReducer reducer) + { + ArgumentNullException.ThrowIfNull(reducer); + + lock (_lock) + { + var actionType = typeof(TAction); + if (!_reducers.TryGetValue(actionType, out var reducers)) + { + reducers = []; + _reducers[actionType] = reducers; + } + + reducers.Add(new ReducerAdapter(reducer)); + } + + return this; + } + + /// + /// 使用委托快速注册一个 reducer。 + /// + /// reducer 处理的 action 类型。 + /// 执行归约的委托。 + /// 当前 Store 实例,便于链式配置。 + /// 时抛出。 + public Store RegisterReducer(Func reducer) + { + ArgumentNullException.ThrowIfNull(reducer); + return RegisterReducer(new DelegateReducer(reducer)); + } + + /// + /// 添加一个 Store 中间件。 + /// 中间件按添加顺序包裹 reducer 执行,可用于日志、审计或调试。 + /// + /// 要添加的中间件实例。 + /// 当前 Store 实例,便于链式配置。 + /// 时抛出。 + public Store UseMiddleware(IStoreMiddleware middleware) + { + ArgumentNullException.ThrowIfNull(middleware); + + lock (_lock) + { + _middlewares.Add(middleware); + } + + return this; + } + + /// + /// 执行一次完整分发管线。 + /// + /// 当前分发上下文。 + private void ExecuteDispatchPipeline(StoreDispatchContext context) + { + Action pipeline = () => ApplyReducers(context); + + for (var i = _middlewares.Count - 1; i >= 0; i--) + { + var middleware = _middlewares[i]; + var next = pipeline; + pipeline = () => middleware.Invoke(context, next); + } + + pipeline(); + } + + /// + /// 对当前 action 应用所有匹配的 reducer。 + /// reducer 使用 action 的精确运行时类型进行查找,以保证匹配结果和执行顺序稳定。 + /// + /// 当前分发上下文。 + private void ApplyReducers(StoreDispatchContext context) + { + if (!_reducers.TryGetValue(context.ActionType, out var reducers) || reducers.Count == 0) + { + context.NextState = context.PreviousState; + context.HasStateChanged = false; + return; + } + + var nextState = context.PreviousState; + + // 多个 reducer 共享同一 action 类型时,后一个 reducer 以前一个 reducer 的输出作为输入, + // 从而支持按模块拆分归约逻辑,同时保持总体状态演进顺序明确。 + foreach (var reducer in reducers) + { + nextState = reducer.Reduce(nextState, context.Action); + } + + context.NextState = nextState; + context.HasStateChanged = !_stateComparer.Equals(context.PreviousState, nextState); + } + + /// + /// 确保当前 Store 没有发生重入分发。 + /// + /// 当检测到重入分发时抛出。 + private void EnsureNotDispatching() + { + if (_isDispatching) + { + throw new InvalidOperationException("Nested dispatch on the same store is not allowed."); + } + } + + /// + /// 适配不同 action 类型 reducer 的内部统一接口。 + /// Store 通过该接口在运行时按 action 具体类型执行 reducer,而不暴露内部装配细节。 + /// + private interface IStoreReducerAdapter + { + /// + /// 使用当前 action 对状态进行一次归约。 + /// + /// 当前状态。 + /// 分发中的 action。 + /// 归约后的下一状态。 + TState Reduce(TState currentState, object action); + } + + /// + /// 基于强类型 reducer 的适配器实现。 + /// 该适配器仅负责安全地完成 object 到 action 类型的转换,然后委托给真实 reducer。 + /// + /// 当前适配器负责处理的 action 类型。 + private sealed class ReducerAdapter(IReducer reducer) : IStoreReducerAdapter + { + /// + /// 包装后的强类型 reducer 实例。 + /// + private readonly IReducer _reducer = + reducer ?? throw new ArgumentNullException(nameof(reducer)); + + /// + /// 将运行时 action 转换为强类型 action 后执行归约。 + /// + /// 当前状态。 + /// 运行时 action。 + /// 归约后的下一状态。 + public TState Reduce(TState currentState, object action) + { + return _reducer.Reduce(currentState, (TAction)action); + } + } + + /// + /// 基于委托的 reducer 适配器实现,便于快速在测试和应用代码中声明 reducer。 + /// + /// 当前适配器负责处理的 action 类型。 + private sealed class DelegateReducer(Func reducer) : IReducer + { + /// + /// 真正执行归约的委托。 + /// + private readonly Func _reducer = + reducer ?? throw new ArgumentNullException(nameof(reducer)); + + /// + /// 执行一次委托归约。 + /// + /// 当前状态。 + /// 当前 action。 + /// 归约后的下一状态。 + public TState Reduce(TState currentState, TAction action) + { + return _reducer(currentState, action); + } + } +} \ No newline at end of file diff --git a/GFramework.Core/StateManagement/StoreSelection.cs b/GFramework.Core/StateManagement/StoreSelection.cs new file mode 100644 index 0000000..47bcbbd --- /dev/null +++ b/GFramework.Core/StateManagement/StoreSelection.cs @@ -0,0 +1,230 @@ +using GFramework.Core.Abstractions.Events; +using GFramework.Core.Abstractions.Property; +using GFramework.Core.Abstractions.StateManagement; +using GFramework.Core.Events; + +namespace GFramework.Core.StateManagement; + +/// +/// Store 选择结果的只读绑定视图。 +/// 该类型将整棵状态树上的订阅转换为局部状态片段的订阅, +/// 使现有依赖 IReadonlyBindableProperty 的 UI 代码能够平滑复用到 Store 场景中。 +/// +/// 源状态类型。 +/// 投影后的局部状态类型。 +public class StoreSelection : IReadonlyBindableProperty +{ + /// + /// 用于判断选择结果是否真正变化的比较器。 + /// + private readonly IEqualityComparer _comparer; + + /// + /// 当前监听器列表。 + /// + private readonly List> _listeners = []; + + /// + /// 保护监听器集合和底层 Store 订阅句柄的同步锁。 + /// + private readonly object _lock = new(); + + /// + /// 负责从完整状态中投影出局部状态的选择器。 + /// + private readonly Func _selector; + + /// + /// 源 Store。 + /// + private readonly IReadonlyStore _store; + + /// + /// 当前已缓存的选择结果。 + /// 该缓存仅在存在监听器时用于变化比较和事件通知,直接读取 Value 时始终以 Store 当前状态为准。 + /// + private TSelected _currentValue = default!; + + /// + /// 连接到底层 Store 的订阅句柄。 + /// 仅当当前存在至少一个监听器时才会建立该订阅,以减少长期闲置对象造成的引用链。 + /// + private IUnRegister? _storeSubscription; + + /// + /// 初始化一个新的 Store 选择视图。 + /// + /// 源 Store。 + /// 状态选择器。 + /// 选择结果比较器;未提供时使用 。 + /// + /// 当 时抛出。 + /// + public StoreSelection( + IReadonlyStore store, + Func selector, + IEqualityComparer? comparer = null) + { + _store = store ?? throw new ArgumentNullException(nameof(store)); + _selector = selector ?? throw new ArgumentNullException(nameof(selector)); + _comparer = comparer ?? EqualityComparer.Default; + } + + /// + /// 获取当前选择结果。 + /// + public TSelected Value => _selector(_store.State); + + /// + /// 将无参事件监听适配为带选择结果参数的监听。 + /// + /// 无参事件监听器。 + /// 用于取消订阅的句柄。 + IUnRegister IEvent.Register(Action onEvent) + { + ArgumentNullException.ThrowIfNull(onEvent); + return Register(_ => onEvent()); + } + + /// + /// 注册选择结果变化监听器。 + /// + /// 选择结果变化时的回调。 + /// 用于取消订阅的句柄。 + /// 时抛出。 + public IUnRegister Register(Action onValueChanged) + { + ArgumentNullException.ThrowIfNull(onValueChanged); + + var shouldAttach = false; + + lock (_lock) + { + if (_listeners.Count == 0) + { + _currentValue = Value; + shouldAttach = true; + } + + _listeners.Add(onValueChanged); + } + + if (shouldAttach) + { + AttachToStore(); + } + + return new DefaultUnRegister(() => UnRegister(onValueChanged)); + } + + /// + /// 注册选择结果变化监听器,并立即回放当前值。 + /// + /// 选择结果变化时的回调。 + /// 用于取消订阅的句柄。 + /// 时抛出。 + public IUnRegister RegisterWithInitValue(Action action) + { + ArgumentNullException.ThrowIfNull(action); + + var currentValue = Value; + action(currentValue); + return Register(action); + } + + /// + /// 取消注册选择结果变化监听器。 + /// + /// 需要移除的监听器。 + /// 时抛出。 + public void UnRegister(Action onValueChanged) + { + ArgumentNullException.ThrowIfNull(onValueChanged); + + IUnRegister? storeSubscription = null; + + lock (_lock) + { + _listeners.Remove(onValueChanged); + if (_listeners.Count == 0 && _storeSubscription != null) + { + storeSubscription = _storeSubscription; + _storeSubscription = null; + } + } + + storeSubscription?.UnRegister(); + } + + /// + /// 将当前选择视图连接到底层 Store。 + /// + private void AttachToStore() + { + var subscription = _store.Subscribe(OnStoreChanged); + Action[] listenersSnapshot = Array.Empty>(); + var latestValue = Value; + var shouldNotify = false; + + lock (_lock) + { + // 如果在建立底层订阅期间所有监听器都已被移除,则立即释放刚刚建立的订阅, + // 避免选择视图在无人监听时继续被 Store 保持引用。 + if (_listeners.Count == 0) + { + subscription.UnRegister(); + return; + } + + if (_storeSubscription != null) + { + subscription.UnRegister(); + return; + } + + _storeSubscription = subscription; + if (!_comparer.Equals(_currentValue, latestValue)) + { + _currentValue = latestValue; + listenersSnapshot = _listeners.ToArray(); + shouldNotify = listenersSnapshot.Length > 0; + } + } + + if (!shouldNotify) + { + return; + } + + foreach (var listener in listenersSnapshot) + { + listener(latestValue); + } + } + + /// + /// 响应底层 Store 的状态变化,并在选中片段真正变化时通知监听器。 + /// + /// 新的完整状态。 + private void OnStoreChanged(TState state) + { + var selectedValue = _selector(state); + Action[] listenersSnapshot = Array.Empty>(); + + lock (_lock) + { + if (_listeners.Count == 0 || _comparer.Equals(_currentValue, selectedValue)) + { + return; + } + + _currentValue = selectedValue; + listenersSnapshot = _listeners.ToArray(); + } + + foreach (var listener in listenersSnapshot) + { + listener(selectedValue); + } + } +} \ No newline at end of file From 79f1240e1d8c33f011c37f42cd943b42a8a40d61 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:35:01 +0800 Subject: [PATCH 2/5] =?UTF-8?q?docs(core):=20=E6=B7=BB=E5=8A=A0=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=E6=96=87=E6=A1=A3=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E5=B1=9E=E6=80=A7=E7=BB=91=E5=AE=9A=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 state-management 文档,介绍集中式状态容器方案 - 在 property 文档中补充与 Store 的使用边界说明 - 更新核心功能表格,添加状态管理条目链接 - 在 README 中增加 StateManagement 功能描述 - 添加状态管理相关接口到抽象层文档 - 提供 Store 与 BindableProperty 的选择指导原则 --- GFramework.Core.Abstractions/README.md | 1 + GFramework.Core/README.md | 1 + docs/zh-CN/core/index.md | 41 +++---- docs/zh-CN/core/property.md | 69 ++++++++--- docs/zh-CN/core/state-management.md | 153 +++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 docs/zh-CN/core/state-management.md diff --git a/GFramework.Core.Abstractions/README.md b/GFramework.Core.Abstractions/README.md index f0f4ad4..efd51df 100644 --- a/GFramework.Core.Abstractions/README.md +++ b/GFramework.Core.Abstractions/README.md @@ -12,6 +12,7 @@ GFramework 框架的抽象层定义模块,包含所有核心组件的接口定 - 事件系统接口 (IEvent, IEventBus) - 依赖注入容器接口 (IIocContainer) - 可绑定属性接口 (IBindableProperty) +- 状态管理接口 (IStore, IReducer, IStateSelector) - 日志系统接口 (ILogger) ## 设计原则 diff --git a/GFramework.Core/README.md b/GFramework.Core/README.md index 71339f4..47fc0aa 100644 --- a/GFramework.Core/README.md +++ b/GFramework.Core/README.md @@ -13,6 +13,7 @@ GFramework 框架的核心模块,提供MVC架构的基础设施。 - **Events** - 事件系统,实现组件间松耦合通信 - **IoC** - 轻量级依赖注入容器 - **Property** - 可绑定属性,支持数据绑定和响应式编程 +- **StateManagement** - 集中式状态容器,支持状态归约、选择器和诊断 - **Utility** - 无状态工具类 - **Pool** - 对象池系统,减少GC压力 - **Extensions** - 框架扩展方法 diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md index 4e8618b..a61cfb3 100644 --- a/docs/zh-CN/core/index.md +++ b/docs/zh-CN/core/index.md @@ -105,7 +105,7 @@ Any → FailedInitialization - 初始化/销毁 - Utility 注册 ``` -这种设计遵循单一职责原则,使代码更易维护和测试。 +这种设计遵循单一职责原则,使代码更易维护和测试。 ``` ┌──────────────────┐ @@ -398,30 +398,31 @@ public class PlayerController : IController 4. **易于扩展**: 添加新功能更容易 5. **代码安全**: 消除了 `null!` 断言,所有字段在构造后立即可用 -详细的设计决策已在架构实现重构中落地。 +详细的设计决策已在架构实现重构中落地。 --- ## 包说明 -| 包名 | 职责 | 文档 | -|------------------|-----------------|----------------------| -| **architecture** | 架构核心,管理所有组件生命周期 | [查看](./architecture) | -| **constants** | 框架常量定义 | 本文档 | -| **model** | 数据模型层,存储状态 | [查看](./model) | -| **system** | 业务逻辑层,处理业务规则 | [查看](./system) | -| **controller** | 控制器层,连接视图和逻辑 | (在 Abstractions 中) | -| **utility** | 工具类层,提供无状态工具 | [查看](./utility) | -| **command** | 命令模式,封装写操作 | [查看](./command) | -| **query** | 查询模式,封装读操作 | [查看](./query) | -| **events** | 事件系统,组件间通信 | [查看](./events) | -| **property** | 可绑定属性,响应式编程 | [查看](./property) | -| **ioc** | IoC 容器,依赖注入 | [查看](./ioc) | -| **rule** | 规则接口,定义组件约束 | [查看](./rule) | -| **extensions** | 扩展方法,简化 API 调用 | [查看](./extensions) | -| **logging** | 日志系统,记录运行日志 | [查看](./logging) | -| **environment** | 环境接口,提供运行环境信息 | [查看](./environment) | -| **localization** | 本地化系统,多语言支持 | [查看](./localization) | +| 包名 | 职责 | 文档 | +|----------------------|-----------------|--------------------------| +| **architecture** | 架构核心,管理所有组件生命周期 | [查看](./architecture) | +| **constants** | 框架常量定义 | 本文档 | +| **model** | 数据模型层,存储状态 | [查看](./model) | +| **system** | 业务逻辑层,处理业务规则 | [查看](./system) | +| **controller** | 控制器层,连接视图和逻辑 | (在 Abstractions 中) | +| **utility** | 工具类层,提供无状态工具 | [查看](./utility) | +| **command** | 命令模式,封装写操作 | [查看](./command) | +| **query** | 查询模式,封装读操作 | [查看](./query) | +| **events** | 事件系统,组件间通信 | [查看](./events) | +| **property** | 可绑定属性,响应式编程 | [查看](./property) | +| **state-management** | 集中式状态容器与选择器 | [查看](./state-management) | +| **ioc** | IoC 容器,依赖注入 | [查看](./ioc) | +| **rule** | 规则接口,定义组件约束 | [查看](./rule) | +| **extensions** | 扩展方法,简化 API 调用 | [查看](./extensions) | +| **logging** | 日志系统,记录运行日志 | [查看](./logging) | +| **environment** | 环境接口,提供运行环境信息 | [查看](./environment) | +| **localization** | 本地化系统,多语言支持 | [查看](./localization) | ## 组件联动 diff --git a/docs/zh-CN/core/property.md b/docs/zh-CN/core/property.md index fc16fc1..52070a3 100644 --- a/docs/zh-CN/core/property.md +++ b/docs/zh-CN/core/property.md @@ -2,9 +2,13 @@ ## 概述 -Property 包提供了可绑定属性(BindableProperty)的实现,支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。 - -BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。 +Property 包提供了可绑定属性(BindableProperty)的实现,支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。 + +BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。 + +> 对于简单字段和局部 UI 绑定,`BindableProperty` 仍然是首选方案。 +> 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器, +> 请同时参考 [`state-management`](./state-management)。 ## 核心接口 @@ -136,7 +140,44 @@ BindableProperty 基于事件系统实现属性变化通知: 3. **事件触发**:如果值发生变化,调用所有注册的回调函数 4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期 -## 在 Model 中使用 +## 在 Model 中使用 + +### 什么时候继续使用 BindableProperty + +以下场景仍然优先推荐 `BindableProperty`: + +- 单个字段变化就能驱动视图更新 +- 状态范围局限在单个 Model 内 +- 不需要统一的 action / reducer 写入入口 +- 不需要从聚合状态树中复用局部选择逻辑 + +如果你的状态已经演化为“多个字段必须一起更新”或“多个模块共享同一聚合状态”, +可以在 Model 内部组合 `Store`,而不是把所有字段都继续拆成独立属性。 + +### 与 Store / StateMachine 的边界 + +- `BindableProperty`:字段级响应式值 +- `Store`:聚合状态容器,负责统一归约状态变化 +- `StateMachine`:流程状态切换,不负责数据状态归约 + +一个复杂 Model 可以同时持有 Store 和 BindableProperty: + +```csharp +public class PlayerStateModel : AbstractModel +{ + public Store Store { get; } = new(new PlayerState(100, "Player")); + public BindableProperty IsDirty { get; } = new(false); + + protected override void OnInit() + { + Store.RegisterReducer((state, action) => + state with { Health = Math.Max(0, state.Health - action.Amount) }); + } +} + +public sealed record PlayerState(int Health, string Name); +public sealed record DamageAction(int Amount); +``` ### 定义可绑定属性 @@ -416,18 +457,20 @@ _mOnValueChanged?.Invoke(value); ## 最佳实践 -1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层 -2. **使用只读接口暴露** - 防止外部随意修改 -3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree -4. **使用 RegisterWithInitValue** - UI 绑定时立即获取初始值 -5. **避免循环依赖** - 属性监听器中修改其他属性要小心 -6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性 +1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层 +2. **使用只读接口暴露** - 防止外部随意修改 +3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree +4. **使用 RegisterWithInitValue** - UI 绑定时立即获取初始值 +5. **避免循环依赖** - 属性监听器中修改其他属性要小心 +6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性 +7. **复杂聚合状态使用 Store** - 当多个字段必须统一演进时,使用 Store 管理聚合状态更清晰 ## 相关包 -- [`model`](./model.md) - Model 中大量使用 BindableProperty -- [`events`](./events.md) - BindableProperty 基于事件系统实现 -- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法 +- [`model`](./model.md) - Model 中大量使用 BindableProperty +- [`events`](./events.md) - BindableProperty 基于事件系统实现 +- [`state-management`](./state-management) - 复杂状态树的集中式管理方案 +- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法 --- diff --git a/docs/zh-CN/core/state-management.md b/docs/zh-CN/core/state-management.md new file mode 100644 index 0000000..b659095 --- /dev/null +++ b/docs/zh-CN/core/state-management.md @@ -0,0 +1,153 @@ +# State Management 包使用说明 + +## 概述 + +State Management 提供一个可选的集中式状态容器方案,用于补足 `BindableProperty` 在复杂状态树场景下的能力。 + +当你的状态具有以下特征时,推荐使用 `Store`: + +- 多个字段需要在一次业务操作中协同更新 +- 多个模块或 UI 片段共享同一聚合状态 +- 希望所有状态写入都经过统一的 action / reducer 入口 +- 需要对整棵状态树做局部选择和按片段订阅 + +这套能力不会替代现有 Property 机制,而是与其并存: + +- `BindableProperty`:字段级响应式值 +- `Store`:聚合状态容器 +- `StateMachine`:流程状态切换 + +## 核心接口 + +### IReadonlyStore`` + +只读状态容器接口,提供: + +- `State`:读取当前状态快照 +- `Subscribe()`:订阅状态变化 +- `SubscribeWithInitValue()`:订阅并立即回放当前状态 +- `UnSubscribe()`:取消订阅 + +### IStore`` + +在只读能力上增加: + +- `Dispatch()`:统一分发 action + +### IReducer`` + +定义状态归约逻辑: + +```csharp +public interface IReducer +{ + TState Reduce(TState currentState, TAction action); +} +``` + +### IStateSelector`` + +从整棵状态树中投影局部视图,便于 UI 和 Controller 复用选择逻辑。 + +## Store`` + +`Store` 是默认实现,支持: + +- 初始状态快照 +- reducer 注册 +- middleware 分发管线 +- 只在状态真正变化时通知订阅者 +- 基础诊断信息(最近一次 action、最近一次分发记录、最近一次状态变化时间) + +## 基本示例 + +```csharp +using GFramework.Core.StateManagement; + +public sealed record PlayerState(int Health, string Name); +public sealed record DamageAction(int Amount); +public sealed record RenameAction(string Name); + +var store = new Store(new PlayerState(100, "Player")) + .RegisterReducer((state, action) => + state with { Health = Math.Max(0, state.Health - action.Amount) }) + .RegisterReducer((state, action) => + state with { Name = action.Name }); + +store.SubscribeWithInitValue(state => +{ + Console.WriteLine($"{state.Name}: {state.Health}"); +}); + +store.Dispatch(new DamageAction(25)); +store.Dispatch(new RenameAction("Knight")); +``` + +## 选择器和 Bindable 风格桥接 + +Store 可以通过扩展方法把聚合状态投影成局部只读绑定视图: + +```csharp +using GFramework.Core.Extensions; + +var healthSelection = store.Select(state => state.Health); + +healthSelection.RegisterWithInitValue(health => +{ + Console.WriteLine($"Current HP: {health}"); +}); +``` + +如果现有 UI 代码已经依赖 `IReadonlyBindableProperty`,可以直接桥接: + +```csharp +IReadonlyBindableProperty healthProperty = + store.ToBindableProperty(state => state.Health); +``` + +## 在 Model 中使用 + +推荐把 Store 作为 Model 的内部状态容器,由 Model 暴露领域友好的业务方法: + +```csharp +public class PlayerStateModel : AbstractModel +{ + public Store Store { get; } = new(new PlayerState(100, "Player")); + + protected override void OnInit() + { + Store.RegisterReducer((state, action) => + state with { Health = Math.Max(0, state.Health - action.Amount) }); + } + + public void TakeDamage(int amount) + { + Store.Dispatch(new DamageAction(amount)); + } +} +``` + +这样可以保留 Model 的生命周期和领域边界,同时获得统一状态入口。 + +## 什么时候不用 Store + +以下情况继续优先使用 `BindableProperty`: + +- 单一字段直接绑定 UI +- 状态规模很小,不需要聚合归约 +- 没有跨模块共享状态树的需求 +- 你只需要“值变化通知”,不需要“统一状态演进入口” + +## 最佳实践 + +1. 优先把 `TState` 设计为不可变状态(如 `record`) +2. 让 reducer 保持纯函数风格,不在 reducer 内执行副作用 +3. 使用 selector 暴露局部状态,而不是让 UI 自己解析整棵状态树 +4. 需要日志或诊断时,优先通过 middleware 扩展,而不是把横切逻辑塞进 reducer + +## 相关文档 + +- [`property`](./property) - 字段级响应式属性 +- [`model`](./model) - Store 常见承载位置 +- [`events`](./events) - 组件间事件通信 +- [`state-machine-tutorial`](../tutorials/state-machine-tutorial) - 流程状态切换能力 From 79cebb95b5058ed15636a79f6373a560f6fc120a Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:59:23 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(state):=20=E6=B7=BB=E5=8A=A0=20StoreBu?= =?UTF-8?q?ilder=20=E9=85=8D=E7=BD=AE=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 StoreBuilder 支持模块化配置 reducer 和中间件 - 实现状态选择视图缓存机制提升性能 - 重构订阅管理使用精确订阅对象替代委托链 - 增强 SubscribeWithInitValue 方法防止状态变化遗漏 - 添加完整的状态管理文档示例和测试用例 - 更新接口定义支持新的构建器功能 --- GFramework.Core.Abstractions/README.md | 2 +- .../StateManagement/IStoreBuilder.cs | 46 ++++ .../StateManagement/StoreTests.cs | 122 ++++++++++ GFramework.Core/StateManagement/Store.cs | 217 +++++++++++++++++- .../StateManagement/StoreBuilder.cs | 87 +++++++ .../StateManagement/StoreSelection.cs | 180 ++++++++++++++- docs/zh-CN/core/command.md | 34 ++- docs/zh-CN/core/state-management.md | 200 ++++++++++++++++ 8 files changed, 865 insertions(+), 23 deletions(-) create mode 100644 GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs create mode 100644 GFramework.Core/StateManagement/StoreBuilder.cs diff --git a/GFramework.Core.Abstractions/README.md b/GFramework.Core.Abstractions/README.md index efd51df..632868f 100644 --- a/GFramework.Core.Abstractions/README.md +++ b/GFramework.Core.Abstractions/README.md @@ -12,7 +12,7 @@ GFramework 框架的抽象层定义模块,包含所有核心组件的接口定 - 事件系统接口 (IEvent, IEventBus) - 依赖注入容器接口 (IIocContainer) - 可绑定属性接口 (IBindableProperty) -- 状态管理接口 (IStore, IReducer, IStateSelector) +- 状态管理接口 (IStore, IReducer, IStateSelector, IStoreBuilder) - 日志系统接口 (ILogger) ## 设计原则 diff --git a/GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs b/GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs new file mode 100644 index 0000000..35ed3cc --- /dev/null +++ b/GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs @@ -0,0 +1,46 @@ +namespace GFramework.Core.Abstractions.StateManagement; + +/// +/// 定义 Store 构建器接口,用于在创建 Store 之前完成 reducer、中间件和比较器配置。 +/// 该抽象适用于模块化注册、依赖注入装配和测试工厂,避免调用方必须依赖具体 Store 类型进行配置。 +/// +/// 状态树的根状态类型。 +public interface IStoreBuilder +{ + /// + /// 配置用于判断状态是否真正变化的比较器。 + /// + /// 状态比较器。 + /// 当前构建器实例。 + IStoreBuilder WithComparer(IEqualityComparer comparer); + + /// + /// 添加一个强类型 reducer。 + /// + /// 当前 reducer 处理的 action 类型。 + /// 要添加的 reducer。 + /// 当前构建器实例。 + IStoreBuilder AddReducer(IReducer reducer); + + /// + /// 使用委托快速添加一个 reducer。 + /// + /// 当前 reducer 处理的 action 类型。 + /// 执行归约的委托。 + /// 当前构建器实例。 + IStoreBuilder AddReducer(Func reducer); + + /// + /// 添加一个 Store 中间件。 + /// + /// 要添加的中间件。 + /// 当前构建器实例。 + IStoreBuilder UseMiddleware(IStoreMiddleware middleware); + + /// + /// 基于给定初始状态创建一个新的 Store。 + /// + /// Store 的初始状态。 + /// 已应用当前构建器配置的 Store 实例。 + IStore Build(TState initialState); +} \ No newline at end of file diff --git a/GFramework.Core.Tests/StateManagement/StoreTests.cs b/GFramework.Core.Tests/StateManagement/StoreTests.cs index a8693d4..c9c4360 100644 --- a/GFramework.Core.Tests/StateManagement/StoreTests.cs +++ b/GFramework.Core.Tests/StateManagement/StoreTests.cs @@ -90,6 +90,27 @@ public class StoreTests Assert.That(receivedCounts, Is.EqualTo(new[] { 5, 8 })); } + /// + /// 测试 Store 的 SubscribeWithInitValue 在初始化回放期间不会漏掉后续状态变化。 + /// + [Test] + public void SubscribeWithInitValue_Should_Not_Miss_Changes_During_Init_Callback() + { + var store = CreateStore(); + var receivedCounts = new List(); + + store.SubscribeWithInitValue(state => + { + receivedCounts.Add(state.Count); + if (receivedCounts.Count == 1) + { + store.Dispatch(new IncrementAction(1)); + } + }); + + Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 })); + } + /// /// 测试注销订阅后不会再收到后续通知。 /// @@ -145,6 +166,28 @@ public class StoreTests Assert.That(selectedCounts, Is.EqualTo(new[] { 11 })); } + /// + /// 测试 StoreSelection 的 RegisterWithInitValue 在初始化回放期间不会漏掉后续局部状态变化。 + /// + [Test] + public void Selection_RegisterWithInitValue_Should_Not_Miss_Changes_During_Init_Callback() + { + var store = CreateStore(); + var selection = store.Select(state => state.Count); + var receivedCounts = new List(); + + selection.RegisterWithInitValue(value => + { + receivedCounts.Add(value); + if (receivedCounts.Count == 1) + { + store.Dispatch(new IncrementAction(1)); + } + }); + + Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 })); + } + /// /// 测试 ToBindableProperty 可桥接到现有 BindableProperty 风格用法,并与旧属性系统共存。 /// @@ -233,6 +276,46 @@ public class StoreTests Assert.That(store.LastStateChangedAt, Is.Null); } + /// + /// 测试 Store 能够复用同一个缓存选择视图实例。 + /// + [Test] + public void GetOrCreateSelection_Should_Return_Cached_Instance_For_Same_Key() + { + var store = CreateStore(); + + var first = store.GetOrCreateSelection("count", state => state.Count); + var second = store.GetOrCreateSelection("count", state => state.Count); + + Assert.That(second, Is.SameAs(first)); + } + + /// + /// 测试 StoreBuilder 能够应用 reducer、中间件和状态比较器配置。 + /// + [Test] + public void StoreBuilder_Should_Apply_Configured_Reducers_Middlewares_And_Comparer() + { + var logs = new List(); + var store = (Store)Store + .CreateBuilder() + .WithComparer(new CounterStateNameInsensitiveComparer()) + .AddReducer((state, action) => state with { Count = state.Count + action.Amount }) + .AddReducer((state, action) => state with { Name = action.Name }) + .UseMiddleware(new RecordingMiddleware(logs, "builder")) + .Build(new CounterState(0, "Player")); + + var notifyCount = 0; + store.Subscribe(_ => notifyCount++); + + store.Dispatch(new RenameAction("player")); + store.Dispatch(new IncrementAction(2)); + + Assert.That(notifyCount, Is.EqualTo(1)); + Assert.That(store.State.Count, Is.EqualTo(2)); + Assert.That(logs, Is.EqualTo(new[] { "builder:before", "builder:after", "builder:before", "builder:after" })); + } + /// /// 创建一个带有基础 reducer 的测试 Store。 /// @@ -315,6 +398,45 @@ public class StoreTests } } + /// + /// 用于测试 StoreBuilder 自定义状态比较器的比较器实现。 + /// 该比较器忽略名称字段的大小写差异,并保持计数字段严格比较。 + /// + private sealed class CounterStateNameInsensitiveComparer : IEqualityComparer + { + /// + /// 判断两个状态是否在业务语义上相等。 + /// + /// 左侧状态。 + /// 右侧状态。 + /// 若两个状态在计数相同且名称仅大小写不同,则返回 + public bool Equals(CounterState? x, CounterState? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return x.Count == y.Count && + string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); + } + + /// + /// 返回与业务语义一致的哈希码。 + /// + /// 目标状态。 + /// 忽略名称大小写后的哈希码。 + public int GetHashCode(CounterState obj) + { + return HashCode.Combine(obj.Count, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name)); + } + } + /// /// 记录中间件调用顺序的测试中间件。 /// diff --git a/GFramework.Core/StateManagement/Store.cs b/GFramework.Core/StateManagement/Store.cs index 12c9a51..5d30d5b 100644 --- a/GFramework.Core/StateManagement/Store.cs +++ b/GFramework.Core/StateManagement/Store.cs @@ -14,9 +14,9 @@ public class Store : IStore, IStoreDiagnostics { /// /// 当前状态变化订阅者列表。 - /// 使用列表而不是委托链,便于精确维护订阅数量并生成稳定的快照调用序列。 + /// 使用显式订阅对象而不是委托链,便于处理原子初始化订阅、挂起补发和精确解绑。 /// - private readonly List> _listeners = []; + private readonly List _listeners = []; /// /// Store 内部所有可变状态的同步锁。 @@ -35,6 +35,12 @@ public class Store : IStore, IStoreDiagnostics /// private readonly Dictionary> _reducers = []; + /// + /// 已缓存的局部状态选择视图。 + /// 该缓存用于避免高频访问的 Model 属性在每次 getter 调用时都创建新的选择对象。 + /// + private readonly Dictionary _selectionCache = []; + /// /// 用于判断状态是否发生有效变化的比较器。 /// @@ -101,12 +107,14 @@ public class Store : IStore, IStoreDiagnostics { ArgumentNullException.ThrowIfNull(listener); + var subscription = new ListenerSubscription(listener); + lock (_lock) { - _listeners.Add(listener); + _listeners.Add(subscription); } - return new DefaultUnRegister(() => UnSubscribe(listener)); + return new DefaultUnRegister(() => UnSubscribe(subscription)); } /// @@ -119,9 +127,53 @@ public class Store : IStore, IStoreDiagnostics { ArgumentNullException.ThrowIfNull(listener); - var currentState = State; - listener(currentState); - return Subscribe(listener); + var subscription = new ListenerSubscription(listener) + { + IsActive = false + }; + TState currentState; + TState? pendingState = default; + var hasPendingState = false; + + lock (_lock) + { + currentState = _state; + _listeners.Add(subscription); + } + + try + { + listener(currentState); + } + catch + { + UnSubscribe(subscription); + throw; + } + + lock (_lock) + { + if (!subscription.IsSubscribed) + { + return new DefaultUnRegister(() => { }); + } + + subscription.IsActive = true; + if (subscription.HasPendingState) + { + pendingState = subscription.PendingState; + hasPendingState = true; + subscription.HasPendingState = false; + subscription.PendingState = default!; + } + } + + if (hasPendingState) + { + listener(pendingState!); + } + + return new DefaultUnRegister(() => UnSubscribe(subscription)); } /// @@ -135,7 +187,14 @@ public class Store : IStore, IStoreDiagnostics lock (_lock) { - _listeners.Remove(listener); + var index = _listeners.FindIndex(subscription => subscription.Listener == listener); + if (index < 0) + { + return; + } + + _listeners[index].IsSubscribed = false; + _listeners.RemoveAt(index); } } @@ -181,7 +240,7 @@ public class Store : IStore, IStoreDiagnostics _state = context.NextState; _lastStateChangedAt = context.DispatchedAt; - listenersSnapshot = _listeners.Count > 0 ? _listeners.ToArray() : Array.Empty>(); + listenersSnapshot = SnapshotListenersForNotification(context.NextState); } finally { @@ -252,6 +311,15 @@ public class Store : IStore, IStoreDiagnostics } } + /// + /// 创建一个用于当前状态类型的 Store 构建器。 + /// + /// 新的 Store 构建器实例。 + public static StoreBuilder CreateBuilder() + { + return new StoreBuilder(); + } + /// /// 注册一个强类型 reducer。 /// 同一 action 类型可注册多个 reducer,它们会按照注册顺序依次归约状态。 @@ -311,6 +379,58 @@ public class Store : IStore, IStoreDiagnostics return this; } + /// + /// 获取或创建一个带缓存的局部状态选择视图。 + /// 对于会被频繁读取的 Model 只读属性,推荐使用该方法复用同一个选择实例。 + /// + /// 局部状态类型。 + /// 缓存键,调用方应保证同一个键始终表示同一局部状态语义。 + /// 状态选择委托。 + /// 用于比较局部状态是否变化的比较器。 + /// 稳定复用的选择视图实例。 + public StoreSelection GetOrCreateSelection( + string key, + Func selector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(selector); + + lock (_lock) + { + if (_selectionCache.TryGetValue(key, out var existing)) + { + if (existing is StoreSelection cachedSelection) + { + return cachedSelection; + } + + throw new InvalidOperationException( + $"A cached selection with key '{key}' already exists with a different selected type."); + } + + var selection = new StoreSelection(this, selector, comparer); + _selectionCache[key] = selection; + return selection; + } + } + + /// + /// 获取或创建一个带缓存的只读 BindableProperty 风格视图。 + /// + /// 局部状态类型。 + /// 缓存键,调用方应保证同一个键始终表示同一局部状态语义。 + /// 状态选择委托。 + /// 用于比较局部状态是否变化的比较器。 + /// 稳定复用的只读绑定视图。 + public StoreSelection GetOrCreateBindableProperty( + string key, + Func selector, + IEqualityComparer? comparer = null) + { + return GetOrCreateSelection(key, selector, comparer); + } + /// /// 执行一次完整分发管线。 /// @@ -368,6 +488,52 @@ public class Store : IStore, IStoreDiagnostics } } + /// + /// 从当前订阅集合中提取需要立即通知的监听器快照,并为尚未激活的初始化订阅保存待补发状态。 + /// + /// 本次分发后的最新状态。 + /// 需要在锁外立即调用的监听器快照。 + private Action[] SnapshotListenersForNotification(TState nextState) + { + if (_listeners.Count == 0) + { + return Array.Empty>(); + } + + var activeListeners = new List>(_listeners.Count); + foreach (var subscription in _listeners) + { + if (!subscription.IsSubscribed) + { + continue; + } + + if (subscription.IsActive) + { + activeListeners.Add(subscription.Listener); + continue; + } + + subscription.PendingState = nextState; + subscription.HasPendingState = true; + } + + return activeListeners.Count > 0 ? activeListeners.ToArray() : Array.Empty>(); + } + + /// + /// 解绑一个精确的订阅对象。 + /// + /// 要解绑的订阅对象。 + private void UnSubscribe(ListenerSubscription subscription) + { + lock (_lock) + { + subscription.IsSubscribed = false; + _listeners.Remove(subscription); + } + } + /// /// 适配不同 action 类型 reducer 的内部统一接口。 /// Store 通过该接口在运行时按 action 具体类型执行 reducer,而不暴露内部装配细节。 @@ -431,4 +597,37 @@ public class Store : IStore, IStoreDiagnostics return _reducer(currentState, action); } } + + /// + /// 表示一个 Store 状态监听订阅。 + /// 该对象用于支持初始化回放与正式订阅之间的原子衔接,避免 SubscribeWithInitValue 漏掉状态变化。 + /// + private sealed class ListenerSubscription(Action listener) + { + /// + /// 获取订阅回调。 + /// + public Action Listener { get; } = listener; + + /// + /// 获取或设置订阅是否已激活。 + /// 非激活状态表示正在执行初始化回放,此时新的状态变化会被暂存为待补发值。 + /// + public bool IsActive { get; set; } = true; + + /// + /// 获取或设置订阅是否仍然有效。 + /// + public bool IsSubscribed { get; set; } = true; + + /// + /// 获取或设置是否存在待补发的最新状态。 + /// + public bool HasPendingState { get; set; } + + /// + /// 获取或设置初始化阶段积累的最新状态。 + /// + public TState PendingState { get; set; } = default!; + } } \ No newline at end of file diff --git a/GFramework.Core/StateManagement/StoreBuilder.cs b/GFramework.Core/StateManagement/StoreBuilder.cs new file mode 100644 index 0000000..3d75196 --- /dev/null +++ b/GFramework.Core/StateManagement/StoreBuilder.cs @@ -0,0 +1,87 @@ +using GFramework.Core.Abstractions.StateManagement; + +namespace GFramework.Core.StateManagement; + +/// +/// Store 构建器的默认实现。 +/// 该类型用于在 Store 创建之前集中配置比较器、reducer 和中间件,适合模块安装和测试工厂场景。 +/// +/// 状态树的根状态类型。 +public class StoreBuilder : IStoreBuilder +{ + /// + /// 延迟应用到 Store 的配置操作列表。 + /// 采用延迟配置而不是直接缓存 reducer 适配器,可复用 Store 自身的注册和验证逻辑。 + /// + private readonly List>> _configurators = []; + + /// + /// 状态比较器。 + /// + private IEqualityComparer? _comparer; + + /// + /// 配置状态比较器。 + /// + /// 状态比较器。 + /// 当前构建器实例。 + public IStoreBuilder WithComparer(IEqualityComparer comparer) + { + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + return this; + } + + /// + /// 添加一个强类型 reducer。 + /// + /// 当前 reducer 处理的 action 类型。 + /// 要添加的 reducer。 + /// 当前构建器实例。 + public IStoreBuilder AddReducer(IReducer reducer) + { + ArgumentNullException.ThrowIfNull(reducer); + _configurators.Add(store => store.RegisterReducer(reducer)); + return this; + } + + /// + /// 使用委托快速添加一个 reducer。 + /// + /// 当前 reducer 处理的 action 类型。 + /// 执行归约的委托。 + /// 当前构建器实例。 + public IStoreBuilder AddReducer(Func reducer) + { + ArgumentNullException.ThrowIfNull(reducer); + _configurators.Add(store => store.RegisterReducer(reducer)); + return this; + } + + /// + /// 添加一个 Store 中间件。 + /// + /// 要添加的中间件。 + /// 当前构建器实例。 + public IStoreBuilder UseMiddleware(IStoreMiddleware middleware) + { + ArgumentNullException.ThrowIfNull(middleware); + _configurators.Add(store => store.UseMiddleware(middleware)); + return this; + } + + /// + /// 基于给定初始状态创建一个新的 Store。 + /// + /// Store 的初始状态。 + /// 已应用当前构建器配置的 Store 实例。 + public IStore Build(TState initialState) + { + var store = new Store(initialState, _comparer); + foreach (var configurator in _configurators) + { + configurator(store); + } + + return store; + } +} \ No newline at end of file diff --git a/GFramework.Core/StateManagement/StoreSelection.cs b/GFramework.Core/StateManagement/StoreSelection.cs index 47bcbbd..35224a5 100644 --- a/GFramework.Core/StateManagement/StoreSelection.cs +++ b/GFramework.Core/StateManagement/StoreSelection.cs @@ -22,7 +22,7 @@ public class StoreSelection : IReadonlyBindableProperty /// 当前监听器列表。 /// - private readonly List> _listeners = []; + private readonly List _listeners = []; /// /// 保护监听器集合和底层 Store 订阅句柄的同步锁。 @@ -96,6 +96,7 @@ public class StoreSelection : IReadonlyBindableProperty : IReadonlyBindableProperty : IReadonlyBindableProperty UnRegister(onValueChanged)); + return new DefaultUnRegister(() => UnRegister(subscription)); } /// @@ -127,9 +128,58 @@ public class StoreSelection : IReadonlyBindableProperty { }); + } + + subscription.IsActive = true; + if (subscription.HasPendingValue) + { + pendingValue = subscription.PendingValue; + hasPendingValue = true; + subscription.PendingValue = default!; + subscription.HasPendingValue = false; + } + } + + if (hasPendingValue) + { + action(pendingValue!); + } + + return new DefaultUnRegister(() => UnRegister(subscription)); } /// @@ -141,11 +191,55 @@ public class StoreSelection : IReadonlyBindableProperty subscription.Listener == onValueChanged); + if (index < 0) + { + return; + } + + subscriptionToRemove = _listeners[index]; + } + + if (subscriptionToRemove != null) + { + UnRegister(subscriptionToRemove); + } + } + + /// + /// 确保当前选择视图已连接到底层 Store。 + /// + private void EnsureAttached() + { + var shouldAttach = false; + + lock (_lock) + { + shouldAttach = _listeners.Count > 0 && _storeSubscription == null; + } + + if (shouldAttach) + { + AttachToStore(); + } + } + + /// + /// 取消注册一个精确的选择结果监听器。 + /// + /// 需要移除的订阅对象。 + private void UnRegister(SelectionListenerSubscription subscriptionToRemove) + { IUnRegister? storeSubscription = null; lock (_lock) { - _listeners.Remove(onValueChanged); + subscriptionToRemove.IsSubscribed = false; + _listeners.Remove(subscriptionToRemove); if (_listeners.Count == 0 && _storeSubscription != null) { storeSubscription = _storeSubscription; @@ -186,7 +280,26 @@ public class StoreSelection : IReadonlyBindableProperty listener.IsSubscribed && listener.IsActive) + .Select(listener => listener.Listener) + .ToArray(); shouldNotify = listenersSnapshot.Length > 0; } } @@ -219,7 +332,26 @@ public class StoreSelection : IReadonlyBindableProperty listener.IsSubscribed && listener.IsActive) + .Select(listener => listener.Listener) + .ToArray(); } foreach (var listener in listenersSnapshot) @@ -227,4 +359,36 @@ public class StoreSelection : IReadonlyBindableProperty + /// 表示一个选择结果监听订阅。 + /// 该对象用于保证 RegisterWithInitValue 在初始化回放与后续状态变化之间不会漏掉最近一次更新。 + /// + private sealed class SelectionListenerSubscription(Action listener) + { + /// + /// 获取订阅回调。 + /// + public Action Listener { get; } = listener; + + /// + /// 获取或设置订阅是否已激活。 + /// + public bool IsActive { get; set; } = true; + + /// + /// 获取或设置订阅是否仍然有效。 + /// + public bool IsSubscribed { get; set; } = true; + + /// + /// 获取或设置是否存在待补发的局部状态值。 + /// + public bool HasPendingValue { get; set; } + + /// + /// 获取或设置初始化阶段积累的最新局部状态值。 + /// + public TSelected PendingValue { get; set; } = default!; + } } \ No newline at end of file diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md index a383f56..f4ec2bc 100644 --- a/docs/zh-CN/core/command.md +++ b/docs/zh-CN/core/command.md @@ -109,7 +109,7 @@ public class UISystem : AbstractSystem } ``` -## 命令的生命周期 +## 命令的生命周期 1. **创建命令**:实例化命令对象,传入必要的参数 2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()` @@ -118,9 +118,33 @@ public class UISystem : AbstractSystem **注意事项:** -- 命令应该是无状态的,执行完即可丢弃 -- 避免在命令中保存长期引用 -- 命令执行应该是原子操作 +- 命令应该是无状态的,执行完即可丢弃 +- 避免在命令中保存长期引用 +- 命令执行应该是原子操作 + +### 与 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)); + } +} +``` + +这样可以保持现有职责边界不变: + +- Controller 发送命令 +- Command 执行操作 +- Model 承载状态 +- Store 负责统一归约状态变化 + +完整示例见 [`state-management`](./state-management)。 ## CommandBus - 命令总线 @@ -448,4 +472,4 @@ public class LoadLevelCommand : AbstractCommand --- -**许可证**:Apache 2.0 \ No newline at end of file +**许可证**:Apache 2.0 diff --git a/docs/zh-CN/core/state-management.md b/docs/zh-CN/core/state-management.md index b659095..3b5afae 100644 --- a/docs/zh-CN/core/state-management.md +++ b/docs/zh-CN/core/state-management.md @@ -129,6 +129,206 @@ public class PlayerStateModel : AbstractModel 这样可以保留 Model 的生命周期和领域边界,同时获得统一状态入口。 +## 使用 StoreBuilder 组织配置 + +当一个 Store 需要在模块安装、测试工厂或 DI 装配阶段统一配置时,可以使用 `StoreBuilder`: + +```csharp +var store = (Store)Store + .CreateBuilder() + .AddReducer((state, action) => + state with { Health = Math.Max(0, state.Health - action.Amount) }) + .Build(new PlayerState(100, "Player")); +``` + +适合以下场景: + +- 模块启动时集中注册 reducer 和 middleware +- 测试里快速组装不同配置的 Store +- 不希望把 Store 的装配细节散落在多个调用点 + +## 官方示例:角色面板状态 + +下面给出一个更贴近 GFramework 实战的完整示例,展示如何把 `Store` 放进 Model, +再通过 Command 修改状态,并在 Controller 中使用 selector 做 UI 绑定。 + +### 1. 定义状态和 action + +```csharp +public sealed record PlayerPanelState( + string Name, + int Health, + int MaxHealth, + int Level); + +public sealed record DamagePlayerAction(int Amount); +public sealed record HealPlayerAction(int Amount); +public sealed record RenamePlayerAction(string Name); +``` + +### 2. 在 Model 中承载 Store + +```csharp +using GFramework.Core.Abstractions.Property; +using GFramework.Core.Model; +using GFramework.Core.Extensions; +using GFramework.Core.StateManagement; + +public class PlayerPanelModel : AbstractModel +{ + public Store Store { get; } = + new(new PlayerPanelState("Player", 100, 100, 1)); + + // 使用带缓存的选择视图,避免属性 getter 每次访问都创建新的 StoreSelection 实例。 + public IReadonlyBindableProperty Health => + Store.GetOrCreateBindableProperty("health", state => state.Health); + + public IReadonlyBindableProperty Name => + Store.GetOrCreateBindableProperty("name", state => state.Name); + + public IReadonlyBindableProperty HealthPercent => + Store.GetOrCreateBindableProperty("health_percent", + state => (float)state.Health / state.MaxHealth); + + protected override void OnInit() + { + Store + .RegisterReducer((state, action) => + state with + { + Health = Math.Max(0, state.Health - action.Amount) + }) + .RegisterReducer((state, action) => + state with + { + Health = Math.Min(state.MaxHealth, state.Health + action.Amount) + }) + .RegisterReducer((state, action) => + state with + { + Name = action.Name + }); + } +} +``` + +这个写法的关键点是: + +- 状态结构集中定义在 `PlayerPanelState` +- 所有状态修改都经过 reducer +- 高频访问的局部状态通过缓存选择视图复用实例 +- Controller 只消费局部只读视图,不直接修改 Store + +### 3. 通过 Command 修改状态 + +```csharp +using GFramework.Core.Command; + +public sealed class DamagePlayerCommand(int amount) : AbstractCommand +{ + protected override void OnExecute() + { + var model = this.GetModel(); + model.Store.Dispatch(new DamagePlayerAction(amount)); + } +} + +public sealed class RenamePlayerCommand(string name) : AbstractCommand +{ + protected override void OnExecute() + { + var model = this.GetModel(); + model.Store.Dispatch(new RenamePlayerAction(name)); + } +} +``` + +这里仍然遵循 GFramework 现有分层: + +- Controller 负责转发用户意图 +- Command 负责执行业务操作 +- Model 持有状态 +- Store 负责统一归约状态变化 + +### 4. 在 Controller 中绑定局部状态 + +```csharp +using GFramework.Core.Abstractions.Controller; +using GFramework.Core.Abstractions.Events; +using GFramework.Core.Events; +using GFramework.Core.Extensions; +using GFramework.SourceGenerators.Abstractions.Rule; + +[ContextAware] +public partial class PlayerPanelController : IController +{ + private readonly IUnRegisterList _unRegisterList = new UnRegisterList(); + + public void Initialize() + { + var model = this.GetModel(); + + model.Name + .RegisterWithInitValue(name => + { + Console.WriteLine($"Player Name: {name}"); + }) + .AddToUnregisterList(_unRegisterList); + + model.Health + .RegisterWithInitValue(health => + { + Console.WriteLine($"Health: {health}"); + }) + .AddToUnregisterList(_unRegisterList); + + model.HealthPercent + .RegisterWithInitValue(percent => + { + Console.WriteLine($"Health Percent: {percent:P0}"); + }) + .AddToUnregisterList(_unRegisterList); + } + + public void OnDamageButtonClicked() + { + this.SendCommand(new DamagePlayerCommand(15)); + } + + public void OnRenameButtonClicked(string newName) + { + this.SendCommand(new RenamePlayerCommand(newName)); + } +} +``` + +### 5. 什么时候这个示例比 BindableProperty 更合适 + +如果你只需要: + +- `Health` +- `Name` +- `Level` + +分别独立通知,那么多个 `BindableProperty` 就足够了。 + +如果你很快会遇到以下问题,这个 Store 方案会更稳: + +- 一次操作要同时修改多个字段 +- 同一个业务操作要在多个界面复用 +- 希望把“状态结构”和“状态变化规则”集中在一起 +- 未来要加入 middleware、调试记录或撤销/重做能力 + +### 6. 推荐的落地方式 + +在实际项目里,建议按这个顺序引入: + +1. 先把复杂聚合状态封装到某个 Model 内部 +2. 再把修改入口逐步迁移到 Command +3. 最后在 Controller 层使用 selector 或 `ToBindableProperty()` 做局部绑定 + +这样不会破坏现有 `BindableProperty` 的轻量工作流,也能让复杂状态逐步收敛到统一入口。 + ## 什么时候不用 Store 以下情况继续优先使用 `BindableProperty`: From b7c54743fa8271d8c89d9a1c3c3eaa94d3dc9fc4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:11:10 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor(state):=20=E4=BC=98=E5=8C=96=20Sto?= =?UTF-8?q?re=20=E7=8A=B6=E6=80=81=E5=88=86=E5=8F=91=E7=9A=84=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=8E=A7=E5=88=B6=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Store 类标记为 sealed 以防止继承 - 引入独立的 dispatch 门闩锁,将状态锁的保护范围缩小为仅保护临界区访问 - 实现 dispatch 过程中的快照机制,确保中间件和 reducer 在锁外执行稳定的不可变序列 - 重构 ExecuteDispatchPipeline 方法,接受快照参数并改为静态方法 - 添加 CreateReducerSnapshot 方法为每次分发创建 reducer 快照 - 更新 StoreBuilder 和 StoreSelection 类为 sealed - 新增测试用例验证长时间运行的 middleware 不会阻塞状态读取和订阅操作 - 修复 dispatch 过程中状态锁占用时间过长的问题,提升并发性能 --- .../StateManagement/StoreTests.cs | 55 +++- GFramework.Core/StateManagement/Store.cs | 302 +++++++++++------- .../StateManagement/StoreBuilder.cs | 77 ++--- .../StateManagement/StoreSelection.cs | 2 +- 4 files changed, 273 insertions(+), 163 deletions(-) diff --git a/GFramework.Core.Tests/StateManagement/StoreTests.cs b/GFramework.Core.Tests/StateManagement/StoreTests.cs index c9c4360..9101cab 100644 --- a/GFramework.Core.Tests/StateManagement/StoreTests.cs +++ b/GFramework.Core.Tests/StateManagement/StoreTests.cs @@ -1,5 +1,3 @@ -using GFramework.Core.Abstractions.Property; -using GFramework.Core.Abstractions.StateManagement; using GFramework.Core.Extensions; using GFramework.Core.Property; using GFramework.Core.StateManagement; @@ -316,6 +314,40 @@ public class StoreTests Assert.That(logs, Is.EqualTo(new[] { "builder:before", "builder:after", "builder:before", "builder:after" })); } + /// + /// 测试长时间运行的 middleware 不会长时间占用状态锁, + /// 使读取状态和新增订阅仍能在 dispatch 进行期间完成。 + /// + [Test] + public void Dispatch_Should_Not_Block_State_Read_Or_Subscribe_While_Middleware_Is_Running() + { + using var entered = new ManualResetEventSlim(false); + using var release = new ManualResetEventSlim(false); + + var store = CreateStore(); + store.UseMiddleware(new BlockingMiddleware(entered, release)); + + var dispatchTask = Task.Run(() => store.Dispatch(new IncrementAction(1))); + + Assert.That(entered.Wait(TimeSpan.FromSeconds(2)), Is.True, "middleware 未按预期进入阻塞阶段"); + + var stateReadTask = Task.Run(() => store.State.Count); + Assert.That(stateReadTask.Wait(TimeSpan.FromMilliseconds(200)), Is.True, "State 读取被 dispatch 长时间阻塞"); + Assert.That(stateReadTask.Result, Is.EqualTo(0), "middleware 执行期间应仍能读取到提交前的状态快照"); + + var subscribeTask = Task.Run(() => + { + var unRegister = store.Subscribe(_ => { }); + unRegister.UnRegister(); + }); + Assert.That(subscribeTask.Wait(TimeSpan.FromMilliseconds(200)), Is.True, "Subscribe 被 dispatch 长时间阻塞"); + + release.Set(); + + Assert.That(dispatchTask.Wait(TimeSpan.FromSeconds(2)), Is.True, "dispatch 未在释放 middleware 后完成"); + Assert.That(store.State.Count, Is.EqualTo(1)); + } + /// /// 创建一个带有基础 reducer 的测试 Store。 /// @@ -455,6 +487,25 @@ public class StoreTests } } + /// + /// 用于验证 dispatch 管线在 middleware 执行期间不会占用状态锁的测试中间件。 + /// + private sealed class BlockingMiddleware(ManualResetEventSlim entered, ManualResetEventSlim release) + : IStoreMiddleware + { + /// + /// 通知测试线程 middleware 已进入阻塞点,并等待释放信号后继续执行。 + /// + /// 当前分发上下文。 + /// 后续处理节点。 + public void Invoke(StoreDispatchContext context, Action next) + { + entered.Set(); + release.Wait(TimeSpan.FromSeconds(2)); + next(); + } + } + /// /// 在中间件阶段尝试二次分发的测试中间件,用于验证重入保护。 /// diff --git a/GFramework.Core/StateManagement/Store.cs b/GFramework.Core/StateManagement/Store.cs index 5d30d5b..a828535 100644 --- a/GFramework.Core/StateManagement/Store.cs +++ b/GFramework.Core/StateManagement/Store.cs @@ -10,8 +10,15 @@ namespace GFramework.Core.StateManagement; /// 或需要中间件/诊断能力的状态场景,而不是替代所有简单字段级响应式属性。 /// /// 状态树的根状态类型。 -public class Store : IStore, IStoreDiagnostics +public sealed class Store : IStore, IStoreDiagnostics { + /// + /// Dispatch 串行化门闩。 + /// 该锁保证任意时刻只有一个 action 管线在运行,从而保持状态演进顺序确定, + /// 同时避免让耗时 middleware / reducer 长时间占用状态锁。 + /// + private readonly object _dispatchGate = new(); + /// /// 当前状态变化订阅者列表。 /// 使用显式订阅对象而不是委托链,便于处理原子初始化订阅、挂起补发和精确解绑。 @@ -20,18 +27,20 @@ public class Store : IStore, IStoreDiagnostics /// /// Store 内部所有可变状态的同步锁。 - /// 该锁同时保护订阅集合、reducer 注册表和分发过程,确保状态演进是串行且可预测的。 + /// 该锁仅保护状态快照、订阅集合、缓存选择视图和注册表本身的短临界区访问。 /// private readonly object _lock = new(); /// /// 已注册的中间件链,按添加顺序执行。 + /// Dispatch 开始时会抓取快照,因此运行中的分发不会受到后续注册变化影响。 /// private readonly List> _middlewares = []; /// /// 按 action 具体运行时类型组织的 reducer 注册表。 /// Store 采用精确类型匹配策略,保证 reducer 执行顺序和行为保持确定性。 + /// Dispatch 开始时会抓取对应 action 类型的 reducer 快照。 /// private readonly Dictionary> _reducers = []; @@ -83,6 +92,34 @@ public class Store : IStore, IStoreDiagnostics _stateComparer = comparer ?? EqualityComparer.Default; } + /// + /// 获取最近一次分发的 action 类型。 + /// + public Type? LastActionType + { + get + { + lock (_lock) + { + return _lastActionType; + } + } + } + + /// + /// 获取最近一次真正改变状态的时间戳。 + /// + public DateTimeOffset? LastStateChangedAt + { + get + { + lock (_lock) + { + return _lastStateChangedAt; + } + } + } + /// /// 获取当前状态快照。 /// @@ -97,6 +134,112 @@ public class Store : IStore, IStoreDiagnostics } } + /// + /// 分发一个 action 并按顺序执行匹配的 reducer。 + /// + /// action 的具体类型。 + /// 要分发的 action。 + /// 时抛出。 + /// 当同一 Store 发生重入分发时抛出。 + public void Dispatch(TAction action) + { + ArgumentNullException.ThrowIfNull(action); + + Action[] listenersSnapshot = Array.Empty>(); + IStoreMiddleware[] middlewaresSnapshot = Array.Empty>(); + IStoreReducerAdapter[] reducersSnapshot = Array.Empty(); + IEqualityComparer stateComparerSnapshot = _stateComparer; + StoreDispatchContext? context = null; + var enteredDispatchScope = false; + + lock (_dispatchGate) + { + try + { + lock (_lock) + { + EnsureNotDispatching(); + _isDispatching = true; + enteredDispatchScope = true; + context = new StoreDispatchContext(action!, _state); + stateComparerSnapshot = _stateComparer; + middlewaresSnapshot = _middlewares.Count > 0 + ? _middlewares.ToArray() + : Array.Empty>(); + reducersSnapshot = CreateReducerSnapshot(context.ActionType); + } + + // middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门, + // 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。 + ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot); + + lock (_lock) + { + _lastActionType = context.ActionType; + _lastDispatchRecord = new StoreDispatchRecord( + context.Action, + context.PreviousState, + context.NextState, + context.HasStateChanged, + context.DispatchedAt); + + if (!context.HasStateChanged) + { + return; + } + + _state = context.NextState; + _lastStateChangedAt = context.DispatchedAt; + listenersSnapshot = SnapshotListenersForNotification(context.NextState); + } + } + finally + { + if (enteredDispatchScope) + { + lock (_lock) + { + _isDispatching = false; + } + } + } + } + + // 始终在锁外通知订阅者,避免监听器内部读取 Store 或执行额外逻辑时产生死锁。 + foreach (var listener in listenersSnapshot) + { + listener(context!.NextState); + } + } + + /// + /// 获取当前订阅者数量。 + /// + public int SubscriberCount + { + get + { + lock (_lock) + { + return _listeners.Count; + } + } + } + + /// + /// 获取最近一次分发记录。 + /// + public StoreDispatchRecord? LastDispatchRecord + { + get + { + lock (_lock) + { + return _lastDispatchRecord; + } + } + } + /// /// 订阅状态变化通知。 /// @@ -198,119 +341,6 @@ public class Store : IStore, IStoreDiagnostics } } - /// - /// 分发一个 action 并按顺序执行匹配的 reducer。 - /// - /// action 的具体类型。 - /// 要分发的 action。 - /// 时抛出。 - /// 当同一 Store 发生重入分发时抛出。 - public void Dispatch(TAction action) - { - ArgumentNullException.ThrowIfNull(action); - - Action[] listenersSnapshot = Array.Empty>(); - StoreDispatchContext? context = null; - - lock (_lock) - { - EnsureNotDispatching(); - _isDispatching = true; - - try - { - context = new StoreDispatchContext(action!, _state); - - // 在锁内串行执行完整分发流程,确保 reducer 与中间件看到的是一致的状态序列, - // 并且不会因为并发写入导致 reducer 顺序失效。 - ExecuteDispatchPipeline(context); - - _lastActionType = context.ActionType; - _lastDispatchRecord = new StoreDispatchRecord( - context.Action, - context.PreviousState, - context.NextState, - context.HasStateChanged, - context.DispatchedAt); - - if (!context.HasStateChanged) - { - return; - } - - _state = context.NextState; - _lastStateChangedAt = context.DispatchedAt; - listenersSnapshot = SnapshotListenersForNotification(context.NextState); - } - finally - { - _isDispatching = false; - } - } - - // 始终在锁外通知订阅者,避免监听器内部读取 Store 或执行额外逻辑时产生死锁。 - foreach (var listener in listenersSnapshot) - { - listener(context!.NextState); - } - } - - /// - /// 获取当前订阅者数量。 - /// - public int SubscriberCount - { - get - { - lock (_lock) - { - return _listeners.Count; - } - } - } - - /// - /// 获取最近一次分发的 action 类型。 - /// - public Type? LastActionType - { - get - { - lock (_lock) - { - return _lastActionType; - } - } - } - - /// - /// 获取最近一次真正改变状态的时间戳。 - /// - public DateTimeOffset? LastStateChangedAt - { - get - { - lock (_lock) - { - return _lastStateChangedAt; - } - } - } - - /// - /// 获取最近一次分发记录。 - /// - public StoreDispatchRecord? LastDispatchRecord - { - get - { - lock (_lock) - { - return _lastDispatchRecord; - } - } - } - /// /// 创建一个用于当前状态类型的 Store 构建器。 /// @@ -435,13 +465,20 @@ public class Store : IStore, IStoreDiagnostics /// 执行一次完整分发管线。 /// /// 当前分发上下文。 - private void ExecuteDispatchPipeline(StoreDispatchContext context) + /// 本次分发使用的中间件快照。 + /// 本次分发使用的 reducer 快照。 + /// 本次分发使用的状态比较器快照。 + private static void ExecuteDispatchPipeline( + StoreDispatchContext context, + IReadOnlyList> middlewares, + IReadOnlyList reducers, + IEqualityComparer stateComparer) { - Action pipeline = () => ApplyReducers(context); + Action pipeline = () => ApplyReducers(context, reducers, stateComparer); - for (var i = _middlewares.Count - 1; i >= 0; i--) + for (var i = middlewares.Count - 1; i >= 0; i--) { - var middleware = _middlewares[i]; + var middleware = middlewares[i]; var next = pipeline; pipeline = () => middleware.Invoke(context, next); } @@ -454,9 +491,14 @@ public class Store : IStore, IStoreDiagnostics /// reducer 使用 action 的精确运行时类型进行查找,以保证匹配结果和执行顺序稳定。 /// /// 当前分发上下文。 - private void ApplyReducers(StoreDispatchContext context) + /// 本次分发使用的 reducer 快照。 + /// 本次分发使用的状态比较器快照。 + private static void ApplyReducers( + StoreDispatchContext context, + IReadOnlyList reducers, + IEqualityComparer stateComparer) { - if (!_reducers.TryGetValue(context.ActionType, out var reducers) || reducers.Count == 0) + if (reducers.Count == 0) { context.NextState = context.PreviousState; context.HasStateChanged = false; @@ -473,7 +515,7 @@ public class Store : IStore, IStoreDiagnostics } context.NextState = nextState; - context.HasStateChanged = !_stateComparer.Equals(context.PreviousState, nextState); + context.HasStateChanged = !stateComparer.Equals(context.PreviousState, nextState); } /// @@ -521,6 +563,22 @@ public class Store : IStore, IStoreDiagnostics return activeListeners.Count > 0 ? activeListeners.ToArray() : Array.Empty>(); } + /// + /// 为当前 action 类型创建 reducer 快照。 + /// Dispatch 在离开状态锁前复制列表,以便后续在锁外执行稳定、不可变的 reducer 序列。 + /// + /// 当前分发的 action 类型。 + /// 对应 action 类型的 reducer 快照;若未注册则返回空数组。 + private IStoreReducerAdapter[] CreateReducerSnapshot(Type actionType) + { + if (!_reducers.TryGetValue(actionType, out var reducers) || reducers.Count == 0) + { + return Array.Empty(); + } + + return reducers.ToArray(); + } + /// /// 解绑一个精确的订阅对象。 /// diff --git a/GFramework.Core/StateManagement/StoreBuilder.cs b/GFramework.Core/StateManagement/StoreBuilder.cs index 3d75196..97d788e 100644 --- a/GFramework.Core/StateManagement/StoreBuilder.cs +++ b/GFramework.Core/StateManagement/StoreBuilder.cs @@ -7,7 +7,7 @@ namespace GFramework.Core.StateManagement; /// 该类型用于在 Store 创建之前集中配置比较器、reducer 和中间件,适合模块安装和测试工厂场景。 /// /// 状态树的根状态类型。 -public class StoreBuilder : IStoreBuilder +public sealed class StoreBuilder : IStoreBuilder { /// /// 延迟应用到 Store 的配置操作列表。 @@ -20,43 +20,6 @@ public class StoreBuilder : IStoreBuilder /// private IEqualityComparer? _comparer; - /// - /// 配置状态比较器。 - /// - /// 状态比较器。 - /// 当前构建器实例。 - public IStoreBuilder WithComparer(IEqualityComparer comparer) - { - _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); - return this; - } - - /// - /// 添加一个强类型 reducer。 - /// - /// 当前 reducer 处理的 action 类型。 - /// 要添加的 reducer。 - /// 当前构建器实例。 - public IStoreBuilder AddReducer(IReducer reducer) - { - ArgumentNullException.ThrowIfNull(reducer); - _configurators.Add(store => store.RegisterReducer(reducer)); - return this; - } - - /// - /// 使用委托快速添加一个 reducer。 - /// - /// 当前 reducer 处理的 action 类型。 - /// 执行归约的委托。 - /// 当前构建器实例。 - public IStoreBuilder AddReducer(Func reducer) - { - ArgumentNullException.ThrowIfNull(reducer); - _configurators.Add(store => store.RegisterReducer(reducer)); - return this; - } - /// /// 添加一个 Store 中间件。 /// @@ -84,4 +47,42 @@ public class StoreBuilder : IStoreBuilder return store; } + + + /// + /// 添加一个强类型 reducer。 + /// + /// 当前 reducer 处理的 action 类型。 + /// 要添加的 reducer。 + /// 当前构建器实例。 + public IStoreBuilder AddReducer(IReducer reducer) + { + ArgumentNullException.ThrowIfNull(reducer); + _configurators.Add(store => store.RegisterReducer(reducer)); + return this; + } + + /// + /// 配置状态比较器。 + /// + /// 状态比较器。 + /// 当前构建器实例。 + public IStoreBuilder WithComparer(IEqualityComparer comparer) + { + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + return this; + } + + /// + /// 使用委托快速添加一个 reducer。 + /// + /// 当前 reducer 处理的 action 类型。 + /// 执行归约的委托。 + /// 当前构建器实例。 + public IStoreBuilder AddReducer(Func reducer) + { + ArgumentNullException.ThrowIfNull(reducer); + _configurators.Add(store => store.RegisterReducer(reducer)); + return this; + } } \ No newline at end of file diff --git a/GFramework.Core/StateManagement/StoreSelection.cs b/GFramework.Core/StateManagement/StoreSelection.cs index 35224a5..b912da4 100644 --- a/GFramework.Core/StateManagement/StoreSelection.cs +++ b/GFramework.Core/StateManagement/StoreSelection.cs @@ -12,7 +12,7 @@ namespace GFramework.Core.StateManagement; /// /// 源状态类型。 /// 投影后的局部状态类型。 -public class StoreSelection : IReadonlyBindableProperty +public sealed class StoreSelection : IReadonlyBindableProperty { /// /// 用于判断选择结果是否真正变化的比较器。 From 3d212716d6b74cc0449d0d467abd5c824683c563 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:14:23 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor(tests):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E6=B5=8B=E8=AF=95=E7=9A=84?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了属性抽象层的命名空间引用 - 添加了状态管理抽象层的命名空间引用 - 保持了原有的核心扩展、属性和状态管理命名空间引用 --- GFramework.Core.Tests/StateManagement/StoreTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GFramework.Core.Tests/StateManagement/StoreTests.cs b/GFramework.Core.Tests/StateManagement/StoreTests.cs index 9101cab..e55f631 100644 --- a/GFramework.Core.Tests/StateManagement/StoreTests.cs +++ b/GFramework.Core.Tests/StateManagement/StoreTests.cs @@ -1,3 +1,5 @@ +using GFramework.Core.Abstractions.Property; +using GFramework.Core.Abstractions.StateManagement; using GFramework.Core.Extensions; using GFramework.Core.Property; using GFramework.Core.StateManagement;