feat(state): 添加 StoreBuilder 配置功能并优化状态管理

- 引入 StoreBuilder<TState> 支持模块化配置 reducer 和中间件
- 实现状态选择视图缓存机制提升性能
- 重构订阅管理使用精确订阅对象替代委托链
- 增强 SubscribeWithInitValue 方法防止状态变化遗漏
- 添加完整的状态管理文档示例和测试用例
- 更新接口定义支持新的构建器功能
This commit is contained in:
GeWuYou 2026-03-23 19:59:23 +08:00
parent 79f1240e1d
commit 79cebb95b5
8 changed files with 865 additions and 23 deletions

View File

@ -12,7 +12,7 @@ GFramework 框架的抽象层定义模块,包含所有核心组件的接口定
- 事件系统接口 (IEvent, IEventBus)
- 依赖注入容器接口 (IIocContainer)
- 可绑定属性接口 (IBindableProperty)
- 状态管理接口 (IStore, IReducer, IStateSelector)
- 状态管理接口 (IStore, IReducer, IStateSelector, IStoreBuilder)
- 日志系统接口 (ILogger)
## 设计原则

View File

@ -0,0 +1,46 @@
namespace GFramework.Core.Abstractions.StateManagement;
/// <summary>
/// 定义 Store 构建器接口,用于在创建 Store 之前完成 reducer、中间件和比较器配置。
/// 该抽象适用于模块化注册、依赖注入装配和测试工厂,避免调用方必须依赖具体 Store 类型进行配置。
/// </summary>
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
public interface IStoreBuilder<TState>
{
/// <summary>
/// 配置用于判断状态是否真正变化的比较器。
/// </summary>
/// <param name="comparer">状态比较器。</param>
/// <returns>当前构建器实例。</returns>
IStoreBuilder<TState> WithComparer(IEqualityComparer<TState> comparer);
/// <summary>
/// 添加一个强类型 reducer。
/// </summary>
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
/// <param name="reducer">要添加的 reducer。</param>
/// <returns>当前构建器实例。</returns>
IStoreBuilder<TState> AddReducer<TAction>(IReducer<TState, TAction> reducer);
/// <summary>
/// 使用委托快速添加一个 reducer。
/// </summary>
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
/// <param name="reducer">执行归约的委托。</param>
/// <returns>当前构建器实例。</returns>
IStoreBuilder<TState> AddReducer<TAction>(Func<TState, TAction, TState> reducer);
/// <summary>
/// 添加一个 Store 中间件。
/// </summary>
/// <param name="middleware">要添加的中间件。</param>
/// <returns>当前构建器实例。</returns>
IStoreBuilder<TState> UseMiddleware(IStoreMiddleware<TState> middleware);
/// <summary>
/// 基于给定初始状态创建一个新的 Store。
/// </summary>
/// <param name="initialState">Store 的初始状态。</param>
/// <returns>已应用当前构建器配置的 Store 实例。</returns>
IStore<TState> Build(TState initialState);
}

View File

@ -90,6 +90,27 @@ public class StoreTests
Assert.That(receivedCounts, Is.EqualTo(new[] { 5, 8 }));
}
/// <summary>
/// 测试 Store 的 SubscribeWithInitValue 在初始化回放期间不会漏掉后续状态变化。
/// </summary>
[Test]
public void SubscribeWithInitValue_Should_Not_Miss_Changes_During_Init_Callback()
{
var store = CreateStore();
var receivedCounts = new List<int>();
store.SubscribeWithInitValue(state =>
{
receivedCounts.Add(state.Count);
if (receivedCounts.Count == 1)
{
store.Dispatch(new IncrementAction(1));
}
});
Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 }));
}
/// <summary>
/// 测试注销订阅后不会再收到后续通知。
/// </summary>
@ -145,6 +166,28 @@ public class StoreTests
Assert.That(selectedCounts, Is.EqualTo(new[] { 11 }));
}
/// <summary>
/// 测试 StoreSelection 的 RegisterWithInitValue 在初始化回放期间不会漏掉后续局部状态变化。
/// </summary>
[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<int>();
selection.RegisterWithInitValue(value =>
{
receivedCounts.Add(value);
if (receivedCounts.Count == 1)
{
store.Dispatch(new IncrementAction(1));
}
});
Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 }));
}
/// <summary>
/// 测试 ToBindableProperty 可桥接到现有 BindableProperty 风格用法,并与旧属性系统共存。
/// </summary>
@ -233,6 +276,46 @@ public class StoreTests
Assert.That(store.LastStateChangedAt, Is.Null);
}
/// <summary>
/// 测试 Store 能够复用同一个缓存选择视图实例。
/// </summary>
[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));
}
/// <summary>
/// 测试 StoreBuilder 能够应用 reducer、中间件和状态比较器配置。
/// </summary>
[Test]
public void StoreBuilder_Should_Apply_Configured_Reducers_Middlewares_And_Comparer()
{
var logs = new List<string>();
var store = (Store<CounterState>)Store<CounterState>
.CreateBuilder()
.WithComparer(new CounterStateNameInsensitiveComparer())
.AddReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount })
.AddReducer<RenameAction>((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" }));
}
/// <summary>
/// 创建一个带有基础 reducer 的测试 Store。
/// </summary>
@ -315,6 +398,45 @@ public class StoreTests
}
}
/// <summary>
/// 用于测试 StoreBuilder 自定义状态比较器的比较器实现。
/// 该比较器忽略名称字段的大小写差异,并保持计数字段严格比较。
/// </summary>
private sealed class CounterStateNameInsensitiveComparer : IEqualityComparer<CounterState>
{
/// <summary>
/// 判断两个状态是否在业务语义上相等。
/// </summary>
/// <param name="x">左侧状态。</param>
/// <param name="y">右侧状态。</param>
/// <returns>若两个状态在计数相同且名称仅大小写不同,则返回 <see langword="true"/>。</returns>
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);
}
/// <summary>
/// 返回与业务语义一致的哈希码。
/// </summary>
/// <param name="obj">目标状态。</param>
/// <returns>忽略名称大小写后的哈希码。</returns>
public int GetHashCode(CounterState obj)
{
return HashCode.Combine(obj.Count, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name));
}
}
/// <summary>
/// 记录中间件调用顺序的测试中间件。
/// </summary>

View File

@ -14,9 +14,9 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
{
/// <summary>
/// 当前状态变化订阅者列表。
/// 使用列表而不是委托链,便于精确维护订阅数量并生成稳定的快照调用序列
/// 使用显式订阅对象而不是委托链,便于处理原子初始化订阅、挂起补发和精确解绑
/// </summary>
private readonly List<Action<TState>> _listeners = [];
private readonly List<ListenerSubscription> _listeners = [];
/// <summary>
/// Store 内部所有可变状态的同步锁。
@ -35,6 +35,12 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
/// </summary>
private readonly Dictionary<Type, List<IStoreReducerAdapter>> _reducers = [];
/// <summary>
/// 已缓存的局部状态选择视图。
/// 该缓存用于避免高频访问的 Model 属性在每次 getter 调用时都创建新的选择对象。
/// </summary>
private readonly Dictionary<string, object> _selectionCache = [];
/// <summary>
/// 用于判断状态是否发生有效变化的比较器。
/// </summary>
@ -101,12 +107,14 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
{
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));
}
/// <summary>
@ -119,9 +127,53 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
{
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));
}
/// <summary>
@ -135,7 +187,14 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
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<TState> : IStore<TState>, IStoreDiagnostics<TState>
_state = context.NextState;
_lastStateChangedAt = context.DispatchedAt;
listenersSnapshot = _listeners.Count > 0 ? _listeners.ToArray() : Array.Empty<Action<TState>>();
listenersSnapshot = SnapshotListenersForNotification(context.NextState);
}
finally
{
@ -252,6 +311,15 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
}
}
/// <summary>
/// 创建一个用于当前状态类型的 Store 构建器。
/// </summary>
/// <returns>新的 Store 构建器实例。</returns>
public static StoreBuilder<TState> CreateBuilder()
{
return new StoreBuilder<TState>();
}
/// <summary>
/// 注册一个强类型 reducer。
/// 同一 action 类型可注册多个 reducer它们会按照注册顺序依次归约状态。
@ -311,6 +379,58 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
return this;
}
/// <summary>
/// 获取或创建一个带缓存的局部状态选择视图。
/// 对于会被频繁读取的 Model 只读属性,推荐使用该方法复用同一个选择实例。
/// </summary>
/// <typeparam name="TSelected">局部状态类型。</typeparam>
/// <param name="key">缓存键,调用方应保证同一个键始终表示同一局部状态语义。</param>
/// <param name="selector">状态选择委托。</param>
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
/// <returns>稳定复用的选择视图实例。</returns>
public StoreSelection<TState, TSelected> GetOrCreateSelection<TSelected>(
string key,
Func<TState, TSelected> selector,
IEqualityComparer<TSelected>? comparer = null)
{
ArgumentNullException.ThrowIfNull(key);
ArgumentNullException.ThrowIfNull(selector);
lock (_lock)
{
if (_selectionCache.TryGetValue(key, out var existing))
{
if (existing is StoreSelection<TState, TSelected> cachedSelection)
{
return cachedSelection;
}
throw new InvalidOperationException(
$"A cached selection with key '{key}' already exists with a different selected type.");
}
var selection = new StoreSelection<TState, TSelected>(this, selector, comparer);
_selectionCache[key] = selection;
return selection;
}
}
/// <summary>
/// 获取或创建一个带缓存的只读 BindableProperty 风格视图。
/// </summary>
/// <typeparam name="TSelected">局部状态类型。</typeparam>
/// <param name="key">缓存键,调用方应保证同一个键始终表示同一局部状态语义。</param>
/// <param name="selector">状态选择委托。</param>
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
/// <returns>稳定复用的只读绑定视图。</returns>
public StoreSelection<TState, TSelected> GetOrCreateBindableProperty<TSelected>(
string key,
Func<TState, TSelected> selector,
IEqualityComparer<TSelected>? comparer = null)
{
return GetOrCreateSelection(key, selector, comparer);
}
/// <summary>
/// 执行一次完整分发管线。
/// </summary>
@ -368,6 +488,52 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
}
}
/// <summary>
/// 从当前订阅集合中提取需要立即通知的监听器快照,并为尚未激活的初始化订阅保存待补发状态。
/// </summary>
/// <param name="nextState">本次分发后的最新状态。</param>
/// <returns>需要在锁外立即调用的监听器快照。</returns>
private Action<TState>[] SnapshotListenersForNotification(TState nextState)
{
if (_listeners.Count == 0)
{
return Array.Empty<Action<TState>>();
}
var activeListeners = new List<Action<TState>>(_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<Action<TState>>();
}
/// <summary>
/// 解绑一个精确的订阅对象。
/// </summary>
/// <param name="subscription">要解绑的订阅对象。</param>
private void UnSubscribe(ListenerSubscription subscription)
{
lock (_lock)
{
subscription.IsSubscribed = false;
_listeners.Remove(subscription);
}
}
/// <summary>
/// 适配不同 action 类型 reducer 的内部统一接口。
/// Store 通过该接口在运行时按 action 具体类型执行 reducer而不暴露内部装配细节。
@ -431,4 +597,37 @@ public class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
return _reducer(currentState, action);
}
}
/// <summary>
/// 表示一个 Store 状态监听订阅。
/// 该对象用于支持初始化回放与正式订阅之间的原子衔接,避免 SubscribeWithInitValue 漏掉状态变化。
/// </summary>
private sealed class ListenerSubscription(Action<TState> listener)
{
/// <summary>
/// 获取订阅回调。
/// </summary>
public Action<TState> Listener { get; } = listener;
/// <summary>
/// 获取或设置订阅是否已激活。
/// 非激活状态表示正在执行初始化回放,此时新的状态变化会被暂存为待补发值。
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 获取或设置订阅是否仍然有效。
/// </summary>
public bool IsSubscribed { get; set; } = true;
/// <summary>
/// 获取或设置是否存在待补发的最新状态。
/// </summary>
public bool HasPendingState { get; set; }
/// <summary>
/// 获取或设置初始化阶段积累的最新状态。
/// </summary>
public TState PendingState { get; set; } = default!;
}
}

View File

@ -0,0 +1,87 @@
using GFramework.Core.Abstractions.StateManagement;
namespace GFramework.Core.StateManagement;
/// <summary>
/// Store 构建器的默认实现。
/// 该类型用于在 Store 创建之前集中配置比较器、reducer 和中间件,适合模块安装和测试工厂场景。
/// </summary>
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
public class StoreBuilder<TState> : IStoreBuilder<TState>
{
/// <summary>
/// 延迟应用到 Store 的配置操作列表。
/// 采用延迟配置而不是直接缓存 reducer 适配器,可复用 Store 自身的注册和验证逻辑。
/// </summary>
private readonly List<Action<Store<TState>>> _configurators = [];
/// <summary>
/// 状态比较器。
/// </summary>
private IEqualityComparer<TState>? _comparer;
/// <summary>
/// 配置状态比较器。
/// </summary>
/// <param name="comparer">状态比较器。</param>
/// <returns>当前构建器实例。</returns>
public IStoreBuilder<TState> WithComparer(IEqualityComparer<TState> comparer)
{
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
return this;
}
/// <summary>
/// 添加一个强类型 reducer。
/// </summary>
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
/// <param name="reducer">要添加的 reducer。</param>
/// <returns>当前构建器实例。</returns>
public IStoreBuilder<TState> AddReducer<TAction>(IReducer<TState, TAction> reducer)
{
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;
}
/// <summary>
/// 使用委托快速添加一个 reducer。
/// </summary>
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
/// <param name="reducer">执行归约的委托。</param>
/// <returns>当前构建器实例。</returns>
public IStoreBuilder<TState> AddReducer<TAction>(Func<TState, TAction, TState> reducer)
{
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;
}
/// <summary>
/// 添加一个 Store 中间件。
/// </summary>
/// <param name="middleware">要添加的中间件。</param>
/// <returns>当前构建器实例。</returns>
public IStoreBuilder<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
{
ArgumentNullException.ThrowIfNull(middleware);
_configurators.Add(store => store.UseMiddleware(middleware));
return this;
}
/// <summary>
/// 基于给定初始状态创建一个新的 Store。
/// </summary>
/// <param name="initialState">Store 的初始状态。</param>
/// <returns>已应用当前构建器配置的 Store 实例。</returns>
public IStore<TState> Build(TState initialState)
{
var store = new Store<TState>(initialState, _comparer);
foreach (var configurator in _configurators)
{
configurator(store);
}
return store;
}
}

View File

@ -22,7 +22,7 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
/// <summary>
/// 当前监听器列表。
/// </summary>
private readonly List<Action<TSelected>> _listeners = [];
private readonly List<SelectionListenerSubscription> _listeners = [];
/// <summary>
/// 保护监听器集合和底层 Store 订阅句柄的同步锁。
@ -96,6 +96,7 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
{
ArgumentNullException.ThrowIfNull(onValueChanged);
var subscription = new SelectionListenerSubscription(onValueChanged);
var shouldAttach = false;
lock (_lock)
@ -106,7 +107,7 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
shouldAttach = true;
}
_listeners.Add(onValueChanged);
_listeners.Add(subscription);
}
if (shouldAttach)
@ -114,7 +115,7 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
AttachToStore();
}
return new DefaultUnRegister(() => UnRegister(onValueChanged));
return new DefaultUnRegister(() => UnRegister(subscription));
}
/// <summary>
@ -127,9 +128,58 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
{
ArgumentNullException.ThrowIfNull(action);
var subscription = new SelectionListenerSubscription(action)
{
IsActive = false
};
var currentValue = Value;
action(currentValue);
return Register(action);
TSelected? pendingValue = default;
var hasPendingValue = false;
lock (_lock)
{
if (_listeners.Count == 0)
{
_currentValue = currentValue;
}
_listeners.Add(subscription);
}
EnsureAttached();
try
{
action(currentValue);
}
catch
{
UnRegister(subscription);
throw;
}
lock (_lock)
{
if (!subscription.IsSubscribed)
{
return new DefaultUnRegister(() => { });
}
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));
}
/// <summary>
@ -141,11 +191,55 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
{
ArgumentNullException.ThrowIfNull(onValueChanged);
SelectionListenerSubscription? subscriptionToRemove = null;
lock (_lock)
{
var index = _listeners.FindIndex(subscription => subscription.Listener == onValueChanged);
if (index < 0)
{
return;
}
subscriptionToRemove = _listeners[index];
}
if (subscriptionToRemove != null)
{
UnRegister(subscriptionToRemove);
}
}
/// <summary>
/// 确保当前选择视图已连接到底层 Store。
/// </summary>
private void EnsureAttached()
{
var shouldAttach = false;
lock (_lock)
{
shouldAttach = _listeners.Count > 0 && _storeSubscription == null;
}
if (shouldAttach)
{
AttachToStore();
}
}
/// <summary>
/// 取消注册一个精确的选择结果监听器。
/// </summary>
/// <param name="subscriptionToRemove">需要移除的订阅对象。</param>
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<TState, TSelected> : IReadonlyBindableProperty<TSele
if (!_comparer.Equals(_currentValue, latestValue))
{
_currentValue = latestValue;
listenersSnapshot = _listeners.ToArray();
foreach (var listener in _listeners)
{
if (!listener.IsSubscribed)
{
continue;
}
if (listener.IsActive)
{
continue;
}
listener.PendingValue = latestValue;
listener.HasPendingValue = true;
}
listenersSnapshot = _listeners
.Where(listener => listener.IsSubscribed && listener.IsActive)
.Select(listener => listener.Listener)
.ToArray();
shouldNotify = listenersSnapshot.Length > 0;
}
}
@ -219,7 +332,26 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
}
_currentValue = selectedValue;
listenersSnapshot = _listeners.ToArray();
foreach (var listener in _listeners)
{
if (!listener.IsSubscribed)
{
continue;
}
if (listener.IsActive)
{
continue;
}
listener.PendingValue = selectedValue;
listener.HasPendingValue = true;
}
listenersSnapshot = _listeners
.Where(listener => listener.IsSubscribed && listener.IsActive)
.Select(listener => listener.Listener)
.ToArray();
}
foreach (var listener in listenersSnapshot)
@ -227,4 +359,36 @@ public class StoreSelection<TState, TSelected> : IReadonlyBindableProperty<TSele
listener(selectedValue);
}
}
/// <summary>
/// 表示一个选择结果监听订阅。
/// 该对象用于保证 RegisterWithInitValue 在初始化回放与后续状态变化之间不会漏掉最近一次更新。
/// </summary>
private sealed class SelectionListenerSubscription(Action<TSelected> listener)
{
/// <summary>
/// 获取订阅回调。
/// </summary>
public Action<TSelected> Listener { get; } = listener;
/// <summary>
/// 获取或设置订阅是否已激活。
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 获取或设置订阅是否仍然有效。
/// </summary>
public bool IsSubscribed { get; set; } = true;
/// <summary>
/// 获取或设置是否存在待补发的局部状态值。
/// </summary>
public bool HasPendingValue { get; set; }
/// <summary>
/// 获取或设置初始化阶段积累的最新局部状态值。
/// </summary>
public TSelected PendingValue { get; set; } = default!;
}
}

View File

@ -109,7 +109,7 @@ public class UISystem : AbstractSystem
}
```
## 命令的生命周期
## 命令的生命周期
1. **创建命令**:实例化命令对象,传入必要的参数
2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()`
@ -118,9 +118,33 @@ public class UISystem : AbstractSystem
**注意事项:**
- 命令应该是无状态的,执行完即可丢弃
- 避免在命令中保存长期引用
- 命令执行应该是原子操作
- 命令应该是无状态的,执行完即可丢弃
- 避免在命令中保存长期引用
- 命令执行应该是原子操作
### 与 Store 配合使用
当某个 Model 内部使用 `Store<TState>` 管理复杂聚合状态时Command 依然是推荐的写入口。
```csharp
public sealed class DamagePlayerCommand(int amount) : AbstractCommand
{
protected override void OnExecute()
{
var model = this.GetModel<PlayerPanelModel>();
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
**许可证**Apache 2.0

View File

@ -129,6 +129,206 @@ public class PlayerStateModel : AbstractModel
这样可以保留 Model 的生命周期和领域边界,同时获得统一状态入口。
## 使用 StoreBuilder 组织配置
当一个 Store 需要在模块安装、测试工厂或 DI 装配阶段统一配置时,可以使用 `StoreBuilder<TState>`
```csharp
var store = (Store<PlayerState>)Store<PlayerState>
.CreateBuilder()
.AddReducer<DamageAction>((state, action) =>
state with { Health = Math.Max(0, state.Health - action.Amount) })
.Build(new PlayerState(100, "Player"));
```
适合以下场景:
- 模块启动时集中注册 reducer 和 middleware
- 测试里快速组装不同配置的 Store
- 不希望把 Store 的装配细节散落在多个调用点
## 官方示例:角色面板状态
下面给出一个更贴近 GFramework 实战的完整示例,展示如何把 `Store<TState>` 放进 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<PlayerPanelState> Store { get; } =
new(new PlayerPanelState("Player", 100, 100, 1));
// 使用带缓存的选择视图,避免属性 getter 每次访问都创建新的 StoreSelection 实例。
public IReadonlyBindableProperty<int> Health =>
Store.GetOrCreateBindableProperty("health", state => state.Health);
public IReadonlyBindableProperty<string> Name =>
Store.GetOrCreateBindableProperty("name", state => state.Name);
public IReadonlyBindableProperty<float> HealthPercent =>
Store.GetOrCreateBindableProperty("health_percent",
state => (float)state.Health / state.MaxHealth);
protected override void OnInit()
{
Store
.RegisterReducer<DamagePlayerAction>((state, action) =>
state with
{
Health = Math.Max(0, state.Health - action.Amount)
})
.RegisterReducer<HealPlayerAction>((state, action) =>
state with
{
Health = Math.Min(state.MaxHealth, state.Health + action.Amount)
})
.RegisterReducer<RenamePlayerAction>((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<PlayerPanelModel>();
model.Store.Dispatch(new DamagePlayerAction(amount));
}
}
public sealed class RenamePlayerCommand(string name) : AbstractCommand
{
protected override void OnExecute()
{
var model = this.GetModel<PlayerPanelModel>();
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<PlayerPanelModel>();
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<T>` 就足够了。
如果你很快会遇到以下问题,这个 Store 方案会更稳:
- 一次操作要同时修改多个字段
- 同一个业务操作要在多个界面复用
- 希望把“状态结构”和“状态变化规则”集中在一起
- 未来要加入 middleware、调试记录或撤销/重做能力
### 6. 推荐的落地方式
在实际项目里,建议按这个顺序引入:
1. 先把复杂聚合状态封装到某个 Model 内部
2. 再把修改入口逐步迁移到 Command
3. 最后在 Controller 层使用 selector 或 `ToBindableProperty()` 做局部绑定
这样不会破坏现有 `BindableProperty<T>` 的轻量工作流,也能让复杂状态逐步收敛到统一入口。
## 什么时候不用 Store
以下情况继续优先使用 `BindableProperty<T>`