mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-24 04:06:48 +08:00
feat(state): 添加状态管理框架核心功能
- 实现 Store 类作为集中式状态容器,默认支持状态归约和订阅通知 - 添加 IReadonlyStore、IStore、IReducer 等状态管理相关抽象接口 - 实现 StoreExtensions 扩展方法,提供 Select 和 ToBindableProperty 选择器功能 - 添加 StoreSelection 类,支持从完整状态树中投影局部状态视图 - 实现 StoreDispatchContext 和 StoreDispatchRecord 用于分发过程诊断 - 添加 IStoreMiddleware 中间件接口,支持在分发过程中插入日志和审计逻辑 - 实现完整的状态选择器和绑定属性桥接功能,便于现有 UI 代码复用 - 添加 Store 相关单元测试,覆盖状态归约、订阅通知和选择器桥接场景
This commit is contained in:
parent
cf486cbeff
commit
2b4b87baba
@ -0,0 +1,40 @@
|
||||
using GFramework.Core.Abstractions.Events;
|
||||
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 只读状态容器接口,用于暴露应用状态快照和订阅能力。
|
||||
/// 该抽象适用于 Controller、Query、ViewModel 等只需要观察状态的调用方,
|
||||
/// 使其无需依赖写入能力即可响应复杂状态树的变化。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
public interface IReadonlyStore<out TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前状态快照。
|
||||
/// Store 负责保证返回值与最近一次成功分发后的状态一致。
|
||||
/// </summary>
|
||||
TState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 订阅状态变化通知。
|
||||
/// 仅当 Store 判断状态发生有效变化时,才会调用该监听器。
|
||||
/// </summary>
|
||||
/// <param name="listener">状态变化时的监听器,参数为新的状态快照。</param>
|
||||
/// <returns>用于取消订阅的句柄。</returns>
|
||||
IUnRegister Subscribe(Action<TState> listener);
|
||||
|
||||
/// <summary>
|
||||
/// 订阅状态变化通知,并立即以当前状态调用一次监听器。
|
||||
/// 该方法适合在 UI 初始化或 ViewModel 首次绑定时建立同步视图。
|
||||
/// </summary>
|
||||
/// <param name="listener">状态变化时的监听器,参数为新的状态快照。</param>
|
||||
/// <returns>用于取消订阅的句柄。</returns>
|
||||
IUnRegister SubscribeWithInitValue(Action<TState> listener);
|
||||
|
||||
/// <summary>
|
||||
/// 取消订阅指定的状态监听器。
|
||||
/// </summary>
|
||||
/// <param name="listener">需要移除的监听器。</param>
|
||||
void UnSubscribe(Action<TState> listener);
|
||||
}
|
||||
19
GFramework.Core.Abstractions/StateManagement/IReducer.cs
Normal file
19
GFramework.Core.Abstractions/StateManagement/IReducer.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 定义状态归约器接口。
|
||||
/// Reducer 应保持纯函数风格:根据当前状态和 action 计算下一状态,
|
||||
/// 不直接产生副作用,也不依赖外部可变环境。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
|
||||
public interface IReducer<TState, in TAction>
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据当前状态和 action 计算下一状态。
|
||||
/// </summary>
|
||||
/// <param name="currentState">当前状态快照。</param>
|
||||
/// <param name="action">触发本次归约的 action。</param>
|
||||
/// <returns>归约后的下一状态。</returns>
|
||||
TState Reduce(TState currentState, TAction action);
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 定义状态选择器接口,用于从整棵状态树中投影出局部状态视图。
|
||||
/// 该抽象适用于复用复杂选择逻辑,避免在 UI 或 Controller 中重复编写投影代码。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
||||
/// <typeparam name="TSelected">投影后的局部状态类型。</typeparam>
|
||||
public interface IStateSelector<in TState, out TSelected>
|
||||
{
|
||||
/// <summary>
|
||||
/// 从给定状态中选择目标片段。
|
||||
/// </summary>
|
||||
/// <param name="state">当前完整状态。</param>
|
||||
/// <returns>投影后的局部状态。</returns>
|
||||
TSelected Select(TState state);
|
||||
}
|
||||
17
GFramework.Core.Abstractions/StateManagement/IStore.cs
Normal file
17
GFramework.Core.Abstractions/StateManagement/IStore.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 可写状态容器接口,提供统一的状态分发入口。
|
||||
/// 所有状态变更都应通过分发 action 触发,以保持单向数据流和可测试性。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
public interface IStore<out TState> : IReadonlyStore<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 分发一个 action 以触发状态演进。
|
||||
/// Store 会按注册顺序执行与该 action 类型匹配的 reducer,并在状态变化后通知订阅者。
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">action 的具体类型。</typeparam>
|
||||
/// <param name="action">要分发的 action 实例。</param>
|
||||
void Dispatch<TAction>(TAction action);
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 暴露 Store 的诊断信息。
|
||||
/// 该接口用于调试、监控和后续时间旅行能力的扩展,不参与状态写入流程。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
public interface IStoreDiagnostics<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前已注册的订阅者数量。
|
||||
/// </summary>
|
||||
int SubscriberCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次分发的 action 类型。
|
||||
/// 即使该次分发未引起状态变化,该值也会更新。
|
||||
/// </summary>
|
||||
Type? LastActionType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次真正改变状态的时间戳。
|
||||
/// 若尚未发生状态变化,则返回 <see langword="null"/>。
|
||||
/// </summary>
|
||||
DateTimeOffset? LastStateChangedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次分发记录。
|
||||
/// </summary>
|
||||
StoreDispatchRecord<TState>? LastDispatchRecord { get; }
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 定义 Store 分发中间件接口。
|
||||
/// 中间件用于在 action 分发前后插入日志、诊断、审计或拦截逻辑,
|
||||
/// 同时保持核心 Store 实现专注于状态归约与订阅通知。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
public interface IStoreMiddleware<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行一次分发管线节点。
|
||||
/// 实现通常应调用 <paramref name="next"/> 继续后续处理;若选择短路,
|
||||
/// 需要自行保证上下文状态对调用方仍然是可解释的。
|
||||
/// </summary>
|
||||
/// <param name="context">当前分发上下文。</param>
|
||||
/// <param name="next">继续执行后续中间件或 reducer 的委托。</param>
|
||||
void Invoke(StoreDispatchContext<TState> context, Action next);
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一次 Store 分发流程中的上下文数据。
|
||||
/// 中间件和 Store 实现通过该对象共享当前 action、分发时间以及归约结果。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
public sealed class StoreDispatchContext<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个新的分发上下文。
|
||||
/// </summary>
|
||||
/// <param name="action">当前分发的 action。</param>
|
||||
/// <param name="previousState">分发前的状态快照。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public StoreDispatchContext(object action, TState previousState)
|
||||
{
|
||||
Action = action ?? throw new ArgumentNullException(nameof(action));
|
||||
PreviousState = previousState;
|
||||
NextState = previousState;
|
||||
DispatchedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前分发的 action 实例。
|
||||
/// </summary>
|
||||
public object Action { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前分发的 action 运行时类型。
|
||||
/// </summary>
|
||||
public Type ActionType => Action.GetType();
|
||||
|
||||
/// <summary>
|
||||
/// 获取分发前的状态快照。
|
||||
/// </summary>
|
||||
public TState PreviousState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置归约后的下一状态。
|
||||
/// Store 会在 reducer 执行完成后使用该值更新内部状态。
|
||||
/// </summary>
|
||||
public TState NextState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置本次分发是否导致状态发生变化。
|
||||
/// 中间件可读取该值进行日志和诊断,但通常应由 Store 负责最终判定。
|
||||
/// </summary>
|
||||
public bool HasStateChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取本次分发创建时的时间戳。
|
||||
/// </summary>
|
||||
public DateTimeOffset DispatchedAt { get; }
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
namespace GFramework.Core.Abstractions.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 记录最近一次 Store 分发的结果。
|
||||
/// 该结构为调试和诊断提供稳定的只读视图,避免调用方直接依赖 Store 的内部状态。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
public sealed class StoreDispatchRecord<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一条分发记录。
|
||||
/// </summary>
|
||||
/// <param name="action">本次分发的 action。</param>
|
||||
/// <param name="previousState">分发前状态。</param>
|
||||
/// <param name="nextState">分发后状态。</param>
|
||||
/// <param name="hasStateChanged">是否发生了有效状态变化。</param>
|
||||
/// <param name="dispatchedAt">分发时间。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本次分发的 action 实例。
|
||||
/// </summary>
|
||||
public object Action { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取本次分发的 action 运行时类型。
|
||||
/// </summary>
|
||||
public Type ActionType => Action.GetType();
|
||||
|
||||
/// <summary>
|
||||
/// 获取分发前状态。
|
||||
/// </summary>
|
||||
public TState PreviousState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取分发后状态。
|
||||
/// </summary>
|
||||
public TState NextState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取本次分发是否产生了有效状态变化。
|
||||
/// </summary>
|
||||
public bool HasStateChanged { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取分发时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset DispatchedAt { get; }
|
||||
}
|
||||
362
GFramework.Core.Tests/StateManagement/StoreTests.cs
Normal file
362
GFramework.Core.Tests/StateManagement/StoreTests.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Store 状态管理能力的单元测试。
|
||||
/// 这些测试覆盖集中式状态容器的核心职责:状态归约、订阅通知、选择器桥接和诊断行为。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class StoreTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 Store 在创建后能够暴露初始状态。
|
||||
/// </summary>
|
||||
[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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 Dispatch 能够执行 reducer 并向订阅者广播新状态。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Dispatch_Should_Update_State_And_Notify_Subscribers()
|
||||
{
|
||||
var store = CreateStore();
|
||||
var receivedStates = new List<CounterState>();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试当 reducer 返回逻辑相等状态时不会触发通知。
|
||||
/// </summary>
|
||||
[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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试同一 action 类型的多个 reducer 会按注册顺序执行。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Dispatch_Should_Run_Multiple_Reducers_In_Registration_Order()
|
||||
{
|
||||
var store = CreateStore();
|
||||
store.RegisterReducer<IncrementAction>((state, action) =>
|
||||
state with { Count = state.Count + action.Amount * 10 });
|
||||
|
||||
store.Dispatch(new IncrementAction(1));
|
||||
|
||||
Assert.That(store.State.Count, Is.EqualTo(11));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 SubscribeWithInitValue 会立即回放当前状态并继续接收后续变化。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SubscribeWithInitValue_Should_Replay_Current_State_And_Future_Changes()
|
||||
{
|
||||
var store = CreateStore(new CounterState(5, "Player"));
|
||||
var receivedCounts = new List<int>();
|
||||
|
||||
store.SubscribeWithInitValue(state => receivedCounts.Add(state.Count));
|
||||
store.Dispatch(new IncrementAction(3));
|
||||
|
||||
Assert.That(receivedCounts, Is.EqualTo(new[] { 5, 8 }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试注销订阅后不会再收到后续通知。
|
||||
/// </summary>
|
||||
[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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试选择器仅在所选状态片段变化时触发通知。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Select_Should_Only_Notify_When_Selected_Slice_Changes()
|
||||
{
|
||||
var store = CreateStore();
|
||||
var selectedCounts = new List<int>();
|
||||
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 }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试选择器支持自定义比较器,从而抑制无意义的局部状态通知。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Select_Should_Respect_Custom_Selected_Value_Comparer()
|
||||
{
|
||||
var store = CreateStore();
|
||||
var selectedCounts = new List<int>();
|
||||
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 }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 ToBindableProperty 可桥接到现有 BindableProperty 风格用法,并与旧属性系统共存。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ToBindableProperty_Should_Work_With_Existing_BindableProperty_Pattern()
|
||||
{
|
||||
var store = CreateStore();
|
||||
var mirror = new BindableProperty<int>(0);
|
||||
IReadonlyBindableProperty<int> bindableProperty = store.ToBindableProperty(state => state.Count);
|
||||
|
||||
bindableProperty.Register(value => mirror.Value = value);
|
||||
store.Dispatch(new IncrementAction(3));
|
||||
|
||||
Assert.That(mirror.Value, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 IStateSelector 接口重载能够复用显式选择逻辑。
|
||||
/// </summary>
|
||||
[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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 Store 在中间件内部发生同一实例的嵌套分发时会抛出异常。
|
||||
/// </summary>
|
||||
[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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试中间件链执行顺序和 Store 诊断信息更新。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Dispatch_Should_Run_Middlewares_In_Order_And_Update_Diagnostics()
|
||||
{
|
||||
var store = CreateStore();
|
||||
var logs = new List<string>();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个带有基础 reducer 的测试 Store。
|
||||
/// </summary>
|
||||
/// <param name="initialState">可选初始状态。</param>
|
||||
/// <returns>已配置基础 reducer 的 Store 实例。</returns>
|
||||
private static Store<CounterState> CreateStore(CounterState? initialState = null)
|
||||
{
|
||||
var store = new Store<CounterState>(initialState ?? new CounterState(0, "Player"));
|
||||
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
|
||||
store.RegisterReducer<RenameAction>((state, action) => state with { Name = action.Name });
|
||||
return store;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于测试的计数器状态。
|
||||
/// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。
|
||||
/// </summary>
|
||||
/// <param name="Count">当前计数值。</param>
|
||||
/// <param name="Name">当前名称。</param>
|
||||
private sealed record CounterState(int Count, string Name);
|
||||
|
||||
/// <summary>
|
||||
/// 表示增加计数的 action。
|
||||
/// </summary>
|
||||
/// <param name="Amount">要增加的数量。</param>
|
||||
private sealed record IncrementAction(int Amount);
|
||||
|
||||
/// <summary>
|
||||
/// 表示修改名称的 action。
|
||||
/// </summary>
|
||||
/// <param name="Name">新的名称。</param>
|
||||
private sealed record RenameAction(string Name);
|
||||
|
||||
/// <summary>
|
||||
/// 表示没有匹配 reducer 的 action,用于验证无变更分发路径。
|
||||
/// </summary>
|
||||
private sealed record NoopAction;
|
||||
|
||||
/// <summary>
|
||||
/// 显式选择器实现,用于验证 IStateSelector 重载。
|
||||
/// </summary>
|
||||
private sealed class CounterNameSelector : IStateSelector<CounterState, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// 从状态中选择名称字段。
|
||||
/// </summary>
|
||||
/// <param name="state">完整状态。</param>
|
||||
/// <returns>名称字段。</returns>
|
||||
public string Select(CounterState state)
|
||||
{
|
||||
return state.Name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将计数值按十位分桶比较的测试比较器。
|
||||
/// 该比较器用于验证选择器只在局部状态“语义变化”时才触发通知。
|
||||
/// </summary>
|
||||
private sealed class TensBucketEqualityComparer : IEqualityComparer<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断两个值是否落在同一个十位分桶中。
|
||||
/// </summary>
|
||||
/// <param name="x">左侧值。</param>
|
||||
/// <param name="y">右侧值。</param>
|
||||
/// <returns>若位于同一分桶则返回 <see langword="true"/>,否则返回 <see langword="false"/>。</returns>
|
||||
public bool Equals(int x, int y)
|
||||
{
|
||||
return x / 10 == y / 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回基于十位分桶的哈希码。
|
||||
/// </summary>
|
||||
/// <param name="obj">目标值。</param>
|
||||
/// <returns>分桶哈希码。</returns>
|
||||
public int GetHashCode(int obj)
|
||||
{
|
||||
return obj / 10;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录中间件调用顺序的测试中间件。
|
||||
/// </summary>
|
||||
private sealed class RecordingMiddleware(List<string> logs, string name) : IStoreMiddleware<CounterState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录当前中间件在分发前后的调用顺序。
|
||||
/// </summary>
|
||||
/// <param name="context">当前分发上下文。</param>
|
||||
/// <param name="next">后续处理节点。</param>
|
||||
public void Invoke(StoreDispatchContext<CounterState> context, Action next)
|
||||
{
|
||||
logs.Add($"{name}:before");
|
||||
next();
|
||||
logs.Add($"{name}:after");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在中间件阶段尝试二次分发的测试中间件,用于验证重入保护。
|
||||
/// </summary>
|
||||
private sealed class NestedDispatchMiddleware(Store<CounterState> store) : IStoreMiddleware<CounterState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记是否已经触发过一次嵌套分发,避免因测试实现本身导致无限递归。
|
||||
/// </summary>
|
||||
private bool _hasTriggered;
|
||||
|
||||
/// <summary>
|
||||
/// 在第一次进入中间件时执行嵌套分发。
|
||||
/// </summary>
|
||||
/// <param name="context">当前分发上下文。</param>
|
||||
/// <param name="next">后续处理节点。</param>
|
||||
public void Invoke(StoreDispatchContext<CounterState> context, Action next)
|
||||
{
|
||||
if (!_hasTriggered)
|
||||
{
|
||||
_hasTriggered = true;
|
||||
store.Dispatch(new IncrementAction(1));
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
90
GFramework.Core/Extensions/StoreExtensions.cs
Normal file
90
GFramework.Core/Extensions/StoreExtensions.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using GFramework.Core.Abstractions.Property;
|
||||
using GFramework.Core.Abstractions.StateManagement;
|
||||
using GFramework.Core.StateManagement;
|
||||
|
||||
namespace GFramework.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 为 Store 提供选择器和 BindableProperty 风格桥接扩展。
|
||||
/// 这些扩展用于在集中式状态容器和现有 Property/UI 生态之间建立最小侵入的互操作层。
|
||||
/// </summary>
|
||||
public static class StoreExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 从 Store 中选择一个局部状态视图。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
||||
/// <param name="store">源 Store。</param>
|
||||
/// <param name="selector">状态选择委托。</param>
|
||||
/// <returns>可用于订阅局部状态变化的只读绑定视图。</returns>
|
||||
public static StoreSelection<TState, TSelected> Select<TState, TSelected>(
|
||||
this IReadonlyStore<TState> store,
|
||||
Func<TState, TSelected> selector)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
|
||||
return new StoreSelection<TState, TSelected>(store, selector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Store 中选择一个局部状态视图,并指定局部状态比较器。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
||||
/// <param name="store">源 Store。</param>
|
||||
/// <param name="selector">状态选择委托。</param>
|
||||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
||||
/// <returns>可用于订阅局部状态变化的只读绑定视图。</returns>
|
||||
public static StoreSelection<TState, TSelected> Select<TState, TSelected>(
|
||||
this IReadonlyStore<TState> store,
|
||||
Func<TState, TSelected> selector,
|
||||
IEqualityComparer<TSelected>? comparer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
|
||||
return new StoreSelection<TState, TSelected>(store, selector, comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用显式选择器对象从 Store 中选择一个局部状态视图。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
||||
/// <param name="store">源 Store。</param>
|
||||
/// <param name="selector">状态选择器实例。</param>
|
||||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
||||
/// <returns>可用于订阅局部状态变化的只读绑定视图。</returns>
|
||||
public static StoreSelection<TState, TSelected> Select<TState, TSelected>(
|
||||
this IReadonlyStore<TState> store,
|
||||
IStateSelector<TState, TSelected> selector,
|
||||
IEqualityComparer<TSelected>? comparer = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
|
||||
return new StoreSelection<TState, TSelected>(store, selector.Select, comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Store 中选中的局部状态桥接为 IReadonlyBindableProperty 风格接口。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
||||
/// <param name="store">源 Store。</param>
|
||||
/// <param name="selector">状态选择委托。</param>
|
||||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
||||
/// <returns>只读绑定属性视图。</returns>
|
||||
public static IReadonlyBindableProperty<TSelected> ToBindableProperty<TState, TSelected>(
|
||||
this IReadonlyStore<TState> store,
|
||||
Func<TState, TSelected> selector,
|
||||
IEqualityComparer<TSelected>? comparer = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(store);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
|
||||
return new StoreSelection<TState, TSelected>(store, selector, comparer);
|
||||
}
|
||||
}
|
||||
434
GFramework.Core/StateManagement/Store.cs
Normal file
434
GFramework.Core/StateManagement/Store.cs
Normal file
@ -0,0 +1,434 @@
|
||||
using GFramework.Core.Abstractions.Events;
|
||||
using GFramework.Core.Abstractions.StateManagement;
|
||||
using GFramework.Core.Events;
|
||||
|
||||
namespace GFramework.Core.StateManagement;
|
||||
|
||||
/// <summary>
|
||||
/// 集中式状态容器的默认实现,用于统一管理复杂状态树的读取、归约和订阅通知。
|
||||
/// 该类型定位于现有 BindableProperty 之上的可选能力,适合跨模块共享、需要统一变更入口
|
||||
/// 或需要中间件/诊断能力的状态场景,而不是替代所有简单字段级响应式属性。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
||||
public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前状态变化订阅者列表。
|
||||
/// 使用列表而不是委托链,便于精确维护订阅数量并生成稳定的快照调用序列。
|
||||
/// </summary>
|
||||
private readonly List<Action<TState>> _listeners = [];
|
||||
|
||||
/// <summary>
|
||||
/// Store 内部所有可变状态的同步锁。
|
||||
/// 该锁同时保护订阅集合、reducer 注册表和分发过程,确保状态演进是串行且可预测的。
|
||||
/// </summary>
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 已注册的中间件链,按添加顺序执行。
|
||||
/// </summary>
|
||||
private readonly List<IStoreMiddleware<TState>> _middlewares = [];
|
||||
|
||||
/// <summary>
|
||||
/// 按 action 具体运行时类型组织的 reducer 注册表。
|
||||
/// Store 采用精确类型匹配策略,保证 reducer 执行顺序和行为保持确定性。
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, List<IStoreReducerAdapter>> _reducers = [];
|
||||
|
||||
/// <summary>
|
||||
/// 用于判断状态是否发生有效变化的比较器。
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<TState> _stateComparer;
|
||||
|
||||
/// <summary>
|
||||
/// 标记当前 Store 是否正在执行分发。
|
||||
/// 该标记用于阻止同一 Store 的重入分发,避免产生难以推导的执行顺序和状态回滚问题。
|
||||
/// </summary>
|
||||
private bool _isDispatching;
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次分发的 action 类型。
|
||||
/// </summary>
|
||||
private Type? _lastActionType;
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次分发记录。
|
||||
/// </summary>
|
||||
private StoreDispatchRecord<TState>? _lastDispatchRecord;
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次真正改变状态的时间戳。
|
||||
/// </summary>
|
||||
private DateTimeOffset? _lastStateChangedAt;
|
||||
|
||||
/// <summary>
|
||||
/// 当前 Store 持有的状态快照。
|
||||
/// </summary>
|
||||
private TState _state;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个新的 Store。
|
||||
/// </summary>
|
||||
/// <param name="initialState">Store 的初始状态。</param>
|
||||
/// <param name="comparer">状态比较器;未提供时使用 <see cref="EqualityComparer{T}.Default"/>。</param>
|
||||
public Store(TState initialState, IEqualityComparer<TState>? comparer = null)
|
||||
{
|
||||
_state = initialState;
|
||||
_stateComparer = comparer ?? EqualityComparer<TState>.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前状态快照。
|
||||
/// </summary>
|
||||
public TState State
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅状态变化通知。
|
||||
/// </summary>
|
||||
/// <param name="listener">状态变化时的监听器。</param>
|
||||
/// <returns>用于取消订阅的句柄。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="listener"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public IUnRegister Subscribe(Action<TState> listener)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(listener);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_listeners.Add(listener);
|
||||
}
|
||||
|
||||
return new DefaultUnRegister(() => UnSubscribe(listener));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅状态变化通知,并立即回放当前状态。
|
||||
/// </summary>
|
||||
/// <param name="listener">状态变化时的监听器。</param>
|
||||
/// <returns>用于取消订阅的句柄。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="listener"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public IUnRegister SubscribeWithInitValue(Action<TState> listener)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(listener);
|
||||
|
||||
var currentState = State;
|
||||
listener(currentState);
|
||||
return Subscribe(listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消订阅指定监听器。
|
||||
/// </summary>
|
||||
/// <param name="listener">需要移除的监听器。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="listener"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void UnSubscribe(Action<TState> listener)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(listener);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_listeners.Remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分发一个 action 并按顺序执行匹配的 reducer。
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">action 的具体类型。</typeparam>
|
||||
/// <param name="action">要分发的 action。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
/// <exception cref="InvalidOperationException">当同一 Store 发生重入分发时抛出。</exception>
|
||||
public void Dispatch<TAction>(TAction action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
|
||||
StoreDispatchContext<TState>? context = null;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureNotDispatching();
|
||||
_isDispatching = true;
|
||||
|
||||
try
|
||||
{
|
||||
context = new StoreDispatchContext<TState>(action!, _state);
|
||||
|
||||
// 在锁内串行执行完整分发流程,确保 reducer 与中间件看到的是一致的状态序列,
|
||||
// 并且不会因为并发写入导致 reducer 顺序失效。
|
||||
ExecuteDispatchPipeline(context);
|
||||
|
||||
_lastActionType = context.ActionType;
|
||||
_lastDispatchRecord = new StoreDispatchRecord<TState>(
|
||||
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<Action<TState>>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDispatching = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 始终在锁外通知订阅者,避免监听器内部读取 Store 或执行额外逻辑时产生死锁。
|
||||
foreach (var listener in listenersSnapshot)
|
||||
{
|
||||
listener(context!.NextState);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前订阅者数量。
|
||||
/// </summary>
|
||||
public int SubscriberCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _listeners.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次分发的 action 类型。
|
||||
/// </summary>
|
||||
public Type? LastActionType
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lastActionType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次真正改变状态的时间戳。
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastStateChangedAt
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lastStateChangedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最近一次分发记录。
|
||||
/// </summary>
|
||||
public StoreDispatchRecord<TState>? LastDispatchRecord
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lastDispatchRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个强类型 reducer。
|
||||
/// 同一 action 类型可注册多个 reducer,它们会按照注册顺序依次归约状态。
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||||
/// <param name="reducer">要注册的 reducer 实例。</param>
|
||||
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public Store<TState> RegisterReducer<TAction>(IReducer<TState, TAction> 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<TAction>(reducer));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用委托快速注册一个 reducer。
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">reducer 处理的 action 类型。</typeparam>
|
||||
/// <param name="reducer">执行归约的委托。</param>
|
||||
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="reducer"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public Store<TState> RegisterReducer<TAction>(Func<TState, TAction, TState> reducer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reducer);
|
||||
return RegisterReducer(new DelegateReducer<TAction>(reducer));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加一个 Store 中间件。
|
||||
/// 中间件按添加顺序包裹 reducer 执行,可用于日志、审计或调试。
|
||||
/// </summary>
|
||||
/// <param name="middleware">要添加的中间件实例。</param>
|
||||
/// <returns>当前 Store 实例,便于链式配置。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="middleware"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public Store<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(middleware);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_middlewares.Add(middleware);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行一次完整分发管线。
|
||||
/// </summary>
|
||||
/// <param name="context">当前分发上下文。</param>
|
||||
private void ExecuteDispatchPipeline(StoreDispatchContext<TState> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对当前 action 应用所有匹配的 reducer。
|
||||
/// reducer 使用 action 的精确运行时类型进行查找,以保证匹配结果和执行顺序稳定。
|
||||
/// </summary>
|
||||
/// <param name="context">当前分发上下文。</param>
|
||||
private void ApplyReducers(StoreDispatchContext<TState> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保当前 Store 没有发生重入分发。
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">当检测到重入分发时抛出。</exception>
|
||||
private void EnsureNotDispatching()
|
||||
{
|
||||
if (_isDispatching)
|
||||
{
|
||||
throw new InvalidOperationException("Nested dispatch on the same store is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 适配不同 action 类型 reducer 的内部统一接口。
|
||||
/// Store 通过该接口在运行时按 action 具体类型执行 reducer,而不暴露内部装配细节。
|
||||
/// </summary>
|
||||
private interface IStoreReducerAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用当前 action 对状态进行一次归约。
|
||||
/// </summary>
|
||||
/// <param name="currentState">当前状态。</param>
|
||||
/// <param name="action">分发中的 action。</param>
|
||||
/// <returns>归约后的下一状态。</returns>
|
||||
TState Reduce(TState currentState, object action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于强类型 reducer 的适配器实现。
|
||||
/// 该适配器仅负责安全地完成 object 到 action 类型的转换,然后委托给真实 reducer。
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">当前适配器负责处理的 action 类型。</typeparam>
|
||||
private sealed class ReducerAdapter<TAction>(IReducer<TState, TAction> reducer) : IStoreReducerAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 包装后的强类型 reducer 实例。
|
||||
/// </summary>
|
||||
private readonly IReducer<TState, TAction> _reducer =
|
||||
reducer ?? throw new ArgumentNullException(nameof(reducer));
|
||||
|
||||
/// <summary>
|
||||
/// 将运行时 action 转换为强类型 action 后执行归约。
|
||||
/// </summary>
|
||||
/// <param name="currentState">当前状态。</param>
|
||||
/// <param name="action">运行时 action。</param>
|
||||
/// <returns>归约后的下一状态。</returns>
|
||||
public TState Reduce(TState currentState, object action)
|
||||
{
|
||||
return _reducer.Reduce(currentState, (TAction)action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于委托的 reducer 适配器实现,便于快速在测试和应用代码中声明 reducer。
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">当前适配器负责处理的 action 类型。</typeparam>
|
||||
private sealed class DelegateReducer<TAction>(Func<TState, TAction, TState> reducer) : IReducer<TState, TAction>
|
||||
{
|
||||
/// <summary>
|
||||
/// 真正执行归约的委托。
|
||||
/// </summary>
|
||||
private readonly Func<TState, TAction, TState> _reducer =
|
||||
reducer ?? throw new ArgumentNullException(nameof(reducer));
|
||||
|
||||
/// <summary>
|
||||
/// 执行一次委托归约。
|
||||
/// </summary>
|
||||
/// <param name="currentState">当前状态。</param>
|
||||
/// <param name="action">当前 action。</param>
|
||||
/// <returns>归约后的下一状态。</returns>
|
||||
public TState Reduce(TState currentState, TAction action)
|
||||
{
|
||||
return _reducer(currentState, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
230
GFramework.Core/StateManagement/StoreSelection.cs
Normal file
230
GFramework.Core/StateManagement/StoreSelection.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Store 选择结果的只读绑定视图。
|
||||
/// 该类型将整棵状态树上的订阅转换为局部状态片段的订阅,
|
||||
/// 使现有依赖 IReadonlyBindableProperty 的 UI 代码能够平滑复用到 Store 场景中。
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
||||
/// <typeparam name="TSelected">投影后的局部状态类型。</typeparam>
|
||||
public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSelected>
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于判断选择结果是否真正变化的比较器。
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<TSelected> _comparer;
|
||||
|
||||
/// <summary>
|
||||
/// 当前监听器列表。
|
||||
/// </summary>
|
||||
private readonly List<Action<TSelected>> _listeners = [];
|
||||
|
||||
/// <summary>
|
||||
/// 保护监听器集合和底层 Store 订阅句柄的同步锁。
|
||||
/// </summary>
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 负责从完整状态中投影出局部状态的选择器。
|
||||
/// </summary>
|
||||
private readonly Func<TState, TSelected> _selector;
|
||||
|
||||
/// <summary>
|
||||
/// 源 Store。
|
||||
/// </summary>
|
||||
private readonly IReadonlyStore<TState> _store;
|
||||
|
||||
/// <summary>
|
||||
/// 当前已缓存的选择结果。
|
||||
/// 该缓存仅在存在监听器时用于变化比较和事件通知,直接读取 Value 时始终以 Store 当前状态为准。
|
||||
/// </summary>
|
||||
private TSelected _currentValue = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 连接到底层 Store 的订阅句柄。
|
||||
/// 仅当当前存在至少一个监听器时才会建立该订阅,以减少长期闲置对象造成的引用链。
|
||||
/// </summary>
|
||||
private IUnRegister? _storeSubscription;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化一个新的 Store 选择视图。
|
||||
/// </summary>
|
||||
/// <param name="store">源 Store。</param>
|
||||
/// <param name="selector">状态选择器。</param>
|
||||
/// <param name="comparer">选择结果比较器;未提供时使用 <see cref="EqualityComparer{T}.Default"/>。</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// 当 <paramref name="store"/> 或 <paramref name="selector"/> 为 <see langword="null"/> 时抛出。
|
||||
/// </exception>
|
||||
public StoreSelection(
|
||||
IReadonlyStore<TState> store,
|
||||
Func<TState, TSelected> selector,
|
||||
IEqualityComparer<TSelected>? comparer = null)
|
||||
{
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_selector = selector ?? throw new ArgumentNullException(nameof(selector));
|
||||
_comparer = comparer ?? EqualityComparer<TSelected>.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前选择结果。
|
||||
/// </summary>
|
||||
public TSelected Value => _selector(_store.State);
|
||||
|
||||
/// <summary>
|
||||
/// 将无参事件监听适配为带选择结果参数的监听。
|
||||
/// </summary>
|
||||
/// <param name="onEvent">无参事件监听器。</param>
|
||||
/// <returns>用于取消订阅的句柄。</returns>
|
||||
IUnRegister IEvent.Register(Action onEvent)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(onEvent);
|
||||
return Register(_ => onEvent());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册选择结果变化监听器。
|
||||
/// </summary>
|
||||
/// <param name="onValueChanged">选择结果变化时的回调。</param>
|
||||
/// <returns>用于取消订阅的句柄。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="onValueChanged"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public IUnRegister Register(Action<TSelected> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册选择结果变化监听器,并立即回放当前值。
|
||||
/// </summary>
|
||||
/// <param name="action">选择结果变化时的回调。</param>
|
||||
/// <returns>用于取消订阅的句柄。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public IUnRegister RegisterWithInitValue(Action<TSelected> action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
|
||||
var currentValue = Value;
|
||||
action(currentValue);
|
||||
return Register(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消注册选择结果变化监听器。
|
||||
/// </summary>
|
||||
/// <param name="onValueChanged">需要移除的监听器。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="onValueChanged"/> 为 <see langword="null"/> 时抛出。</exception>
|
||||
public void UnRegister(Action<TSelected> onValueChanged)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(onValueChanged);
|
||||
|
||||
IUnRegister? storeSubscription = null;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_listeners.Remove(onValueChanged);
|
||||
if (_listeners.Count == 0 && _storeSubscription != null)
|
||||
{
|
||||
storeSubscription = _storeSubscription;
|
||||
_storeSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
storeSubscription?.UnRegister();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前选择视图连接到底层 Store。
|
||||
/// </summary>
|
||||
private void AttachToStore()
|
||||
{
|
||||
var subscription = _store.Subscribe(OnStoreChanged);
|
||||
Action<TSelected>[] listenersSnapshot = Array.Empty<Action<TSelected>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 响应底层 Store 的状态变化,并在选中片段真正变化时通知监听器。
|
||||
/// </summary>
|
||||
/// <param name="state">新的完整状态。</param>
|
||||
private void OnStoreChanged(TState state)
|
||||
{
|
||||
var selectedValue = _selector(state);
|
||||
Action<TSelected>[] listenersSnapshot = Array.Empty<Action<TSelected>>();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_listeners.Count == 0 || _comparer.Equals(_currentValue, selectedValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentValue = selectedValue;
|
||||
listenersSnapshot = _listeners.ToArray();
|
||||
}
|
||||
|
||||
foreach (var listener in listenersSnapshot)
|
||||
{
|
||||
listener(selectedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user