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