diff --git a/GFramework.Core.Abstractions/README.md b/GFramework.Core.Abstractions/README.md
index efd51df..632868f 100644
--- a/GFramework.Core.Abstractions/README.md
+++ b/GFramework.Core.Abstractions/README.md
@@ -12,7 +12,7 @@ GFramework 框架的抽象层定义模块,包含所有核心组件的接口定
- 事件系统接口 (IEvent, IEventBus)
- 依赖注入容器接口 (IIocContainer)
- 可绑定属性接口 (IBindableProperty)
-- 状态管理接口 (IStore, IReducer, IStateSelector)
+- 状态管理接口 (IStore, IReducer, IStateSelector, IStoreBuilder)
- 日志系统接口 (ILogger)
## 设计原则
diff --git a/GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs b/GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs
new file mode 100644
index 0000000..35ed3cc
--- /dev/null
+++ b/GFramework.Core.Abstractions/StateManagement/IStoreBuilder.cs
@@ -0,0 +1,46 @@
+namespace GFramework.Core.Abstractions.StateManagement;
+
+///
+/// 定义 Store 构建器接口,用于在创建 Store 之前完成 reducer、中间件和比较器配置。
+/// 该抽象适用于模块化注册、依赖注入装配和测试工厂,避免调用方必须依赖具体 Store 类型进行配置。
+///
+/// 状态树的根状态类型。
+public interface IStoreBuilder
+{
+ ///
+ /// 配置用于判断状态是否真正变化的比较器。
+ ///
+ /// 状态比较器。
+ /// 当前构建器实例。
+ IStoreBuilder WithComparer(IEqualityComparer comparer);
+
+ ///
+ /// 添加一个强类型 reducer。
+ ///
+ /// 当前 reducer 处理的 action 类型。
+ /// 要添加的 reducer。
+ /// 当前构建器实例。
+ IStoreBuilder AddReducer(IReducer reducer);
+
+ ///
+ /// 使用委托快速添加一个 reducer。
+ ///
+ /// 当前 reducer 处理的 action 类型。
+ /// 执行归约的委托。
+ /// 当前构建器实例。
+ IStoreBuilder AddReducer(Func reducer);
+
+ ///
+ /// 添加一个 Store 中间件。
+ ///
+ /// 要添加的中间件。
+ /// 当前构建器实例。
+ IStoreBuilder UseMiddleware(IStoreMiddleware middleware);
+
+ ///
+ /// 基于给定初始状态创建一个新的 Store。
+ ///
+ /// Store 的初始状态。
+ /// 已应用当前构建器配置的 Store 实例。
+ IStore Build(TState initialState);
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/StateManagement/StoreTests.cs b/GFramework.Core.Tests/StateManagement/StoreTests.cs
index a8693d4..c9c4360 100644
--- a/GFramework.Core.Tests/StateManagement/StoreTests.cs
+++ b/GFramework.Core.Tests/StateManagement/StoreTests.cs
@@ -90,6 +90,27 @@ public class StoreTests
Assert.That(receivedCounts, Is.EqualTo(new[] { 5, 8 }));
}
+ ///
+ /// 测试 Store 的 SubscribeWithInitValue 在初始化回放期间不会漏掉后续状态变化。
+ ///
+ [Test]
+ public void SubscribeWithInitValue_Should_Not_Miss_Changes_During_Init_Callback()
+ {
+ var store = CreateStore();
+ var receivedCounts = new List();
+
+ store.SubscribeWithInitValue(state =>
+ {
+ receivedCounts.Add(state.Count);
+ if (receivedCounts.Count == 1)
+ {
+ store.Dispatch(new IncrementAction(1));
+ }
+ });
+
+ Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 }));
+ }
+
///
/// 测试注销订阅后不会再收到后续通知。
///
@@ -145,6 +166,28 @@ public class StoreTests
Assert.That(selectedCounts, Is.EqualTo(new[] { 11 }));
}
+ ///
+ /// 测试 StoreSelection 的 RegisterWithInitValue 在初始化回放期间不会漏掉后续局部状态变化。
+ ///
+ [Test]
+ public void Selection_RegisterWithInitValue_Should_Not_Miss_Changes_During_Init_Callback()
+ {
+ var store = CreateStore();
+ var selection = store.Select(state => state.Count);
+ var receivedCounts = new List();
+
+ selection.RegisterWithInitValue(value =>
+ {
+ receivedCounts.Add(value);
+ if (receivedCounts.Count == 1)
+ {
+ store.Dispatch(new IncrementAction(1));
+ }
+ });
+
+ Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 }));
+ }
+
///
/// 测试 ToBindableProperty 可桥接到现有 BindableProperty 风格用法,并与旧属性系统共存。
///
@@ -233,6 +276,46 @@ public class StoreTests
Assert.That(store.LastStateChangedAt, Is.Null);
}
+ ///
+ /// 测试 Store 能够复用同一个缓存选择视图实例。
+ ///
+ [Test]
+ public void GetOrCreateSelection_Should_Return_Cached_Instance_For_Same_Key()
+ {
+ var store = CreateStore();
+
+ var first = store.GetOrCreateSelection("count", state => state.Count);
+ var second = store.GetOrCreateSelection("count", state => state.Count);
+
+ Assert.That(second, Is.SameAs(first));
+ }
+
+ ///
+ /// 测试 StoreBuilder 能够应用 reducer、中间件和状态比较器配置。
+ ///
+ [Test]
+ public void StoreBuilder_Should_Apply_Configured_Reducers_Middlewares_And_Comparer()
+ {
+ var logs = new List();
+ var store = (Store)Store
+ .CreateBuilder()
+ .WithComparer(new CounterStateNameInsensitiveComparer())
+ .AddReducer((state, action) => state with { Count = state.Count + action.Amount })
+ .AddReducer((state, action) => state with { Name = action.Name })
+ .UseMiddleware(new RecordingMiddleware(logs, "builder"))
+ .Build(new CounterState(0, "Player"));
+
+ var notifyCount = 0;
+ store.Subscribe(_ => notifyCount++);
+
+ store.Dispatch(new RenameAction("player"));
+ store.Dispatch(new IncrementAction(2));
+
+ Assert.That(notifyCount, Is.EqualTo(1));
+ Assert.That(store.State.Count, Is.EqualTo(2));
+ Assert.That(logs, Is.EqualTo(new[] { "builder:before", "builder:after", "builder:before", "builder:after" }));
+ }
+
///
/// 创建一个带有基础 reducer 的测试 Store。
///
@@ -315,6 +398,45 @@ public class StoreTests
}
}
+ ///
+ /// 用于测试 StoreBuilder 自定义状态比较器的比较器实现。
+ /// 该比较器忽略名称字段的大小写差异,并保持计数字段严格比较。
+ ///
+ private sealed class CounterStateNameInsensitiveComparer : IEqualityComparer
+ {
+ ///
+ /// 判断两个状态是否在业务语义上相等。
+ ///
+ /// 左侧状态。
+ /// 右侧状态。
+ /// 若两个状态在计数相同且名称仅大小写不同,则返回 。
+ public bool Equals(CounterState? x, CounterState? y)
+ {
+ if (ReferenceEquals(x, y))
+ {
+ return true;
+ }
+
+ if (x is null || y is null)
+ {
+ return false;
+ }
+
+ return x.Count == y.Count &&
+ string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// 返回与业务语义一致的哈希码。
+ ///
+ /// 目标状态。
+ /// 忽略名称大小写后的哈希码。
+ public int GetHashCode(CounterState obj)
+ {
+ return HashCode.Combine(obj.Count, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name));
+ }
+ }
+
///
/// 记录中间件调用顺序的测试中间件。
///
diff --git a/GFramework.Core/StateManagement/Store.cs b/GFramework.Core/StateManagement/Store.cs
index 12c9a51..5d30d5b 100644
--- a/GFramework.Core/StateManagement/Store.cs
+++ b/GFramework.Core/StateManagement/Store.cs
@@ -14,9 +14,9 @@ public class Store : IStore, IStoreDiagnostics
{
///
/// 当前状态变化订阅者列表。
- /// 使用列表而不是委托链,便于精确维护订阅数量并生成稳定的快照调用序列。
+ /// 使用显式订阅对象而不是委托链,便于处理原子初始化订阅、挂起补发和精确解绑。
///
- private readonly List> _listeners = [];
+ private readonly List _listeners = [];
///
/// Store 内部所有可变状态的同步锁。
@@ -35,6 +35,12 @@ public class Store : IStore, IStoreDiagnostics
///
private readonly Dictionary> _reducers = [];
+ ///
+ /// 已缓存的局部状态选择视图。
+ /// 该缓存用于避免高频访问的 Model 属性在每次 getter 调用时都创建新的选择对象。
+ ///
+ private readonly Dictionary _selectionCache = [];
+
///
/// 用于判断状态是否发生有效变化的比较器。
///
@@ -101,12 +107,14 @@ public class Store : IStore, IStoreDiagnostics
{
ArgumentNullException.ThrowIfNull(listener);
+ var subscription = new ListenerSubscription(listener);
+
lock (_lock)
{
- _listeners.Add(listener);
+ _listeners.Add(subscription);
}
- return new DefaultUnRegister(() => UnSubscribe(listener));
+ return new DefaultUnRegister(() => UnSubscribe(subscription));
}
///
@@ -119,9 +127,53 @@ public class Store : IStore, IStoreDiagnostics
{
ArgumentNullException.ThrowIfNull(listener);
- var currentState = State;
- listener(currentState);
- return Subscribe(listener);
+ var subscription = new ListenerSubscription(listener)
+ {
+ IsActive = false
+ };
+ TState currentState;
+ TState? pendingState = default;
+ var hasPendingState = false;
+
+ lock (_lock)
+ {
+ currentState = _state;
+ _listeners.Add(subscription);
+ }
+
+ try
+ {
+ listener(currentState);
+ }
+ catch
+ {
+ UnSubscribe(subscription);
+ throw;
+ }
+
+ lock (_lock)
+ {
+ if (!subscription.IsSubscribed)
+ {
+ return new DefaultUnRegister(() => { });
+ }
+
+ subscription.IsActive = true;
+ if (subscription.HasPendingState)
+ {
+ pendingState = subscription.PendingState;
+ hasPendingState = true;
+ subscription.HasPendingState = false;
+ subscription.PendingState = default!;
+ }
+ }
+
+ if (hasPendingState)
+ {
+ listener(pendingState!);
+ }
+
+ return new DefaultUnRegister(() => UnSubscribe(subscription));
}
///
@@ -135,7 +187,14 @@ public class Store : IStore, IStoreDiagnostics
lock (_lock)
{
- _listeners.Remove(listener);
+ var index = _listeners.FindIndex(subscription => subscription.Listener == listener);
+ if (index < 0)
+ {
+ return;
+ }
+
+ _listeners[index].IsSubscribed = false;
+ _listeners.RemoveAt(index);
}
}
@@ -181,7 +240,7 @@ public class Store : IStore, IStoreDiagnostics
_state = context.NextState;
_lastStateChangedAt = context.DispatchedAt;
- listenersSnapshot = _listeners.Count > 0 ? _listeners.ToArray() : Array.Empty>();
+ listenersSnapshot = SnapshotListenersForNotification(context.NextState);
}
finally
{
@@ -252,6 +311,15 @@ public class Store : IStore, IStoreDiagnostics
}
}
+ ///
+ /// 创建一个用于当前状态类型的 Store 构建器。
+ ///
+ /// 新的 Store 构建器实例。
+ public static StoreBuilder CreateBuilder()
+ {
+ return new StoreBuilder();
+ }
+
///
/// 注册一个强类型 reducer。
/// 同一 action 类型可注册多个 reducer,它们会按照注册顺序依次归约状态。
@@ -311,6 +379,58 @@ public class Store : IStore, IStoreDiagnostics
return this;
}
+ ///
+ /// 获取或创建一个带缓存的局部状态选择视图。
+ /// 对于会被频繁读取的 Model 只读属性,推荐使用该方法复用同一个选择实例。
+ ///
+ /// 局部状态类型。
+ /// 缓存键,调用方应保证同一个键始终表示同一局部状态语义。
+ /// 状态选择委托。
+ /// 用于比较局部状态是否变化的比较器。
+ /// 稳定复用的选择视图实例。
+ public StoreSelection GetOrCreateSelection(
+ string key,
+ Func selector,
+ IEqualityComparer? comparer = null)
+ {
+ ArgumentNullException.ThrowIfNull(key);
+ ArgumentNullException.ThrowIfNull(selector);
+
+ lock (_lock)
+ {
+ if (_selectionCache.TryGetValue(key, out var existing))
+ {
+ if (existing is StoreSelection cachedSelection)
+ {
+ return cachedSelection;
+ }
+
+ throw new InvalidOperationException(
+ $"A cached selection with key '{key}' already exists with a different selected type.");
+ }
+
+ var selection = new StoreSelection(this, selector, comparer);
+ _selectionCache[key] = selection;
+ return selection;
+ }
+ }
+
+ ///
+ /// 获取或创建一个带缓存的只读 BindableProperty 风格视图。
+ ///
+ /// 局部状态类型。
+ /// 缓存键,调用方应保证同一个键始终表示同一局部状态语义。
+ /// 状态选择委托。
+ /// 用于比较局部状态是否变化的比较器。
+ /// 稳定复用的只读绑定视图。
+ public StoreSelection GetOrCreateBindableProperty(
+ string key,
+ Func selector,
+ IEqualityComparer? comparer = null)
+ {
+ return GetOrCreateSelection(key, selector, comparer);
+ }
+
///
/// 执行一次完整分发管线。
///
@@ -368,6 +488,52 @@ public class Store : IStore, IStoreDiagnostics
}
}
+ ///
+ /// 从当前订阅集合中提取需要立即通知的监听器快照,并为尚未激活的初始化订阅保存待补发状态。
+ ///
+ /// 本次分发后的最新状态。
+ /// 需要在锁外立即调用的监听器快照。
+ private Action[] SnapshotListenersForNotification(TState nextState)
+ {
+ if (_listeners.Count == 0)
+ {
+ return Array.Empty>();
+ }
+
+ var activeListeners = new List>(_listeners.Count);
+ foreach (var subscription in _listeners)
+ {
+ if (!subscription.IsSubscribed)
+ {
+ continue;
+ }
+
+ if (subscription.IsActive)
+ {
+ activeListeners.Add(subscription.Listener);
+ continue;
+ }
+
+ subscription.PendingState = nextState;
+ subscription.HasPendingState = true;
+ }
+
+ return activeListeners.Count > 0 ? activeListeners.ToArray() : Array.Empty>();
+ }
+
+ ///
+ /// 解绑一个精确的订阅对象。
+ ///
+ /// 要解绑的订阅对象。
+ private void UnSubscribe(ListenerSubscription subscription)
+ {
+ lock (_lock)
+ {
+ subscription.IsSubscribed = false;
+ _listeners.Remove(subscription);
+ }
+ }
+
///
/// 适配不同 action 类型 reducer 的内部统一接口。
/// Store 通过该接口在运行时按 action 具体类型执行 reducer,而不暴露内部装配细节。
@@ -431,4 +597,37 @@ public class Store : IStore, IStoreDiagnostics
return _reducer(currentState, action);
}
}
+
+ ///
+ /// 表示一个 Store 状态监听订阅。
+ /// 该对象用于支持初始化回放与正式订阅之间的原子衔接,避免 SubscribeWithInitValue 漏掉状态变化。
+ ///
+ private sealed class ListenerSubscription(Action listener)
+ {
+ ///
+ /// 获取订阅回调。
+ ///
+ public Action Listener { get; } = listener;
+
+ ///
+ /// 获取或设置订阅是否已激活。
+ /// 非激活状态表示正在执行初始化回放,此时新的状态变化会被暂存为待补发值。
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// 获取或设置订阅是否仍然有效。
+ ///
+ public bool IsSubscribed { get; set; } = true;
+
+ ///
+ /// 获取或设置是否存在待补发的最新状态。
+ ///
+ public bool HasPendingState { get; set; }
+
+ ///
+ /// 获取或设置初始化阶段积累的最新状态。
+ ///
+ public TState PendingState { get; set; } = default!;
+ }
}
\ No newline at end of file
diff --git a/GFramework.Core/StateManagement/StoreBuilder.cs b/GFramework.Core/StateManagement/StoreBuilder.cs
new file mode 100644
index 0000000..3d75196
--- /dev/null
+++ b/GFramework.Core/StateManagement/StoreBuilder.cs
@@ -0,0 +1,87 @@
+using GFramework.Core.Abstractions.StateManagement;
+
+namespace GFramework.Core.StateManagement;
+
+///
+/// Store 构建器的默认实现。
+/// 该类型用于在 Store 创建之前集中配置比较器、reducer 和中间件,适合模块安装和测试工厂场景。
+///
+/// 状态树的根状态类型。
+public class StoreBuilder : IStoreBuilder
+{
+ ///
+ /// 延迟应用到 Store 的配置操作列表。
+ /// 采用延迟配置而不是直接缓存 reducer 适配器,可复用 Store 自身的注册和验证逻辑。
+ ///
+ private readonly List>> _configurators = [];
+
+ ///
+ /// 状态比较器。
+ ///
+ private IEqualityComparer? _comparer;
+
+ ///
+ /// 配置状态比较器。
+ ///
+ /// 状态比较器。
+ /// 当前构建器实例。
+ public IStoreBuilder WithComparer(IEqualityComparer comparer)
+ {
+ _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
+ return this;
+ }
+
+ ///
+ /// 添加一个强类型 reducer。
+ ///
+ /// 当前 reducer 处理的 action 类型。
+ /// 要添加的 reducer。
+ /// 当前构建器实例。
+ public IStoreBuilder AddReducer(IReducer reducer)
+ {
+ ArgumentNullException.ThrowIfNull(reducer);
+ _configurators.Add(store => store.RegisterReducer(reducer));
+ return this;
+ }
+
+ ///
+ /// 使用委托快速添加一个 reducer。
+ ///
+ /// 当前 reducer 处理的 action 类型。
+ /// 执行归约的委托。
+ /// 当前构建器实例。
+ public IStoreBuilder AddReducer(Func reducer)
+ {
+ ArgumentNullException.ThrowIfNull(reducer);
+ _configurators.Add(store => store.RegisterReducer(reducer));
+ return this;
+ }
+
+ ///
+ /// 添加一个 Store 中间件。
+ ///
+ /// 要添加的中间件。
+ /// 当前构建器实例。
+ public IStoreBuilder UseMiddleware(IStoreMiddleware middleware)
+ {
+ ArgumentNullException.ThrowIfNull(middleware);
+ _configurators.Add(store => store.UseMiddleware(middleware));
+ return this;
+ }
+
+ ///
+ /// 基于给定初始状态创建一个新的 Store。
+ ///
+ /// Store 的初始状态。
+ /// 已应用当前构建器配置的 Store 实例。
+ public IStore Build(TState initialState)
+ {
+ var store = new Store(initialState, _comparer);
+ foreach (var configurator in _configurators)
+ {
+ configurator(store);
+ }
+
+ return store;
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/StateManagement/StoreSelection.cs b/GFramework.Core/StateManagement/StoreSelection.cs
index 47bcbbd..35224a5 100644
--- a/GFramework.Core/StateManagement/StoreSelection.cs
+++ b/GFramework.Core/StateManagement/StoreSelection.cs
@@ -22,7 +22,7 @@ public class StoreSelection : IReadonlyBindableProperty
/// 当前监听器列表。
///
- private readonly List> _listeners = [];
+ private readonly List _listeners = [];
///
/// 保护监听器集合和底层 Store 订阅句柄的同步锁。
@@ -96,6 +96,7 @@ public class StoreSelection : IReadonlyBindableProperty : IReadonlyBindableProperty : IReadonlyBindableProperty UnRegister(onValueChanged));
+ return new DefaultUnRegister(() => UnRegister(subscription));
}
///
@@ -127,9 +128,58 @@ public class StoreSelection : IReadonlyBindableProperty { });
+ }
+
+ subscription.IsActive = true;
+ if (subscription.HasPendingValue)
+ {
+ pendingValue = subscription.PendingValue;
+ hasPendingValue = true;
+ subscription.PendingValue = default!;
+ subscription.HasPendingValue = false;
+ }
+ }
+
+ if (hasPendingValue)
+ {
+ action(pendingValue!);
+ }
+
+ return new DefaultUnRegister(() => UnRegister(subscription));
}
///
@@ -141,11 +191,55 @@ public class StoreSelection : IReadonlyBindableProperty subscription.Listener == onValueChanged);
+ if (index < 0)
+ {
+ return;
+ }
+
+ subscriptionToRemove = _listeners[index];
+ }
+
+ if (subscriptionToRemove != null)
+ {
+ UnRegister(subscriptionToRemove);
+ }
+ }
+
+ ///
+ /// 确保当前选择视图已连接到底层 Store。
+ ///
+ private void EnsureAttached()
+ {
+ var shouldAttach = false;
+
+ lock (_lock)
+ {
+ shouldAttach = _listeners.Count > 0 && _storeSubscription == null;
+ }
+
+ if (shouldAttach)
+ {
+ AttachToStore();
+ }
+ }
+
+ ///
+ /// 取消注册一个精确的选择结果监听器。
+ ///
+ /// 需要移除的订阅对象。
+ private void UnRegister(SelectionListenerSubscription subscriptionToRemove)
+ {
IUnRegister? storeSubscription = null;
lock (_lock)
{
- _listeners.Remove(onValueChanged);
+ subscriptionToRemove.IsSubscribed = false;
+ _listeners.Remove(subscriptionToRemove);
if (_listeners.Count == 0 && _storeSubscription != null)
{
storeSubscription = _storeSubscription;
@@ -186,7 +280,26 @@ public class StoreSelection : IReadonlyBindableProperty listener.IsSubscribed && listener.IsActive)
+ .Select(listener => listener.Listener)
+ .ToArray();
shouldNotify = listenersSnapshot.Length > 0;
}
}
@@ -219,7 +332,26 @@ public class StoreSelection : IReadonlyBindableProperty listener.IsSubscribed && listener.IsActive)
+ .Select(listener => listener.Listener)
+ .ToArray();
}
foreach (var listener in listenersSnapshot)
@@ -227,4 +359,36 @@ public class StoreSelection : IReadonlyBindableProperty
+ /// 表示一个选择结果监听订阅。
+ /// 该对象用于保证 RegisterWithInitValue 在初始化回放与后续状态变化之间不会漏掉最近一次更新。
+ ///
+ private sealed class SelectionListenerSubscription(Action listener)
+ {
+ ///
+ /// 获取订阅回调。
+ ///
+ public Action Listener { get; } = listener;
+
+ ///
+ /// 获取或设置订阅是否已激活。
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// 获取或设置订阅是否仍然有效。
+ ///
+ public bool IsSubscribed { get; set; } = true;
+
+ ///
+ /// 获取或设置是否存在待补发的局部状态值。
+ ///
+ public bool HasPendingValue { get; set; }
+
+ ///
+ /// 获取或设置初始化阶段积累的最新局部状态值。
+ ///
+ public TSelected PendingValue { get; set; } = default!;
+ }
}
\ No newline at end of file
diff --git a/docs/zh-CN/core/command.md b/docs/zh-CN/core/command.md
index a383f56..f4ec2bc 100644
--- a/docs/zh-CN/core/command.md
+++ b/docs/zh-CN/core/command.md
@@ -109,7 +109,7 @@ public class UISystem : AbstractSystem
}
```
-## 命令的生命周期
+## 命令的生命周期
1. **创建命令**:实例化命令对象,传入必要的参数
2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()`
@@ -118,9 +118,33 @@ public class UISystem : AbstractSystem
**注意事项:**
-- 命令应该是无状态的,执行完即可丢弃
-- 避免在命令中保存长期引用
-- 命令执行应该是原子操作
+- 命令应该是无状态的,执行完即可丢弃
+- 避免在命令中保存长期引用
+- 命令执行应该是原子操作
+
+### 与 Store 配合使用
+
+当某个 Model 内部使用 `Store` 管理复杂聚合状态时,Command 依然是推荐的写入口。
+
+```csharp
+public sealed class DamagePlayerCommand(int amount) : AbstractCommand
+{
+ protected override void OnExecute()
+ {
+ var model = this.GetModel();
+ model.Store.Dispatch(new DamagePlayerAction(amount));
+ }
+}
+```
+
+这样可以保持现有职责边界不变:
+
+- Controller 发送命令
+- Command 执行操作
+- Model 承载状态
+- Store 负责统一归约状态变化
+
+完整示例见 [`state-management`](./state-management)。
## CommandBus - 命令总线
@@ -448,4 +472,4 @@ public class LoadLevelCommand : AbstractCommand
---
-**许可证**:Apache 2.0
\ No newline at end of file
+**许可证**:Apache 2.0
diff --git a/docs/zh-CN/core/state-management.md b/docs/zh-CN/core/state-management.md
index b659095..3b5afae 100644
--- a/docs/zh-CN/core/state-management.md
+++ b/docs/zh-CN/core/state-management.md
@@ -129,6 +129,206 @@ public class PlayerStateModel : AbstractModel
这样可以保留 Model 的生命周期和领域边界,同时获得统一状态入口。
+## 使用 StoreBuilder 组织配置
+
+当一个 Store 需要在模块安装、测试工厂或 DI 装配阶段统一配置时,可以使用 `StoreBuilder`:
+
+```csharp
+var store = (Store)Store
+ .CreateBuilder()
+ .AddReducer((state, action) =>
+ state with { Health = Math.Max(0, state.Health - action.Amount) })
+ .Build(new PlayerState(100, "Player"));
+```
+
+适合以下场景:
+
+- 模块启动时集中注册 reducer 和 middleware
+- 测试里快速组装不同配置的 Store
+- 不希望把 Store 的装配细节散落在多个调用点
+
+## 官方示例:角色面板状态
+
+下面给出一个更贴近 GFramework 实战的完整示例,展示如何把 `Store` 放进 Model,
+再通过 Command 修改状态,并在 Controller 中使用 selector 做 UI 绑定。
+
+### 1. 定义状态和 action
+
+```csharp
+public sealed record PlayerPanelState(
+ string Name,
+ int Health,
+ int MaxHealth,
+ int Level);
+
+public sealed record DamagePlayerAction(int Amount);
+public sealed record HealPlayerAction(int Amount);
+public sealed record RenamePlayerAction(string Name);
+```
+
+### 2. 在 Model 中承载 Store
+
+```csharp
+using GFramework.Core.Abstractions.Property;
+using GFramework.Core.Model;
+using GFramework.Core.Extensions;
+using GFramework.Core.StateManagement;
+
+public class PlayerPanelModel : AbstractModel
+{
+ public Store Store { get; } =
+ new(new PlayerPanelState("Player", 100, 100, 1));
+
+ // 使用带缓存的选择视图,避免属性 getter 每次访问都创建新的 StoreSelection 实例。
+ public IReadonlyBindableProperty Health =>
+ Store.GetOrCreateBindableProperty("health", state => state.Health);
+
+ public IReadonlyBindableProperty Name =>
+ Store.GetOrCreateBindableProperty("name", state => state.Name);
+
+ public IReadonlyBindableProperty HealthPercent =>
+ Store.GetOrCreateBindableProperty("health_percent",
+ state => (float)state.Health / state.MaxHealth);
+
+ protected override void OnInit()
+ {
+ Store
+ .RegisterReducer((state, action) =>
+ state with
+ {
+ Health = Math.Max(0, state.Health - action.Amount)
+ })
+ .RegisterReducer((state, action) =>
+ state with
+ {
+ Health = Math.Min(state.MaxHealth, state.Health + action.Amount)
+ })
+ .RegisterReducer((state, action) =>
+ state with
+ {
+ Name = action.Name
+ });
+ }
+}
+```
+
+这个写法的关键点是:
+
+- 状态结构集中定义在 `PlayerPanelState`
+- 所有状态修改都经过 reducer
+- 高频访问的局部状态通过缓存选择视图复用实例
+- Controller 只消费局部只读视图,不直接修改 Store
+
+### 3. 通过 Command 修改状态
+
+```csharp
+using GFramework.Core.Command;
+
+public sealed class DamagePlayerCommand(int amount) : AbstractCommand
+{
+ protected override void OnExecute()
+ {
+ var model = this.GetModel();
+ model.Store.Dispatch(new DamagePlayerAction(amount));
+ }
+}
+
+public sealed class RenamePlayerCommand(string name) : AbstractCommand
+{
+ protected override void OnExecute()
+ {
+ var model = this.GetModel();
+ model.Store.Dispatch(new RenamePlayerAction(name));
+ }
+}
+```
+
+这里仍然遵循 GFramework 现有分层:
+
+- Controller 负责转发用户意图
+- Command 负责执行业务操作
+- Model 持有状态
+- Store 负责统一归约状态变化
+
+### 4. 在 Controller 中绑定局部状态
+
+```csharp
+using GFramework.Core.Abstractions.Controller;
+using GFramework.Core.Abstractions.Events;
+using GFramework.Core.Events;
+using GFramework.Core.Extensions;
+using GFramework.SourceGenerators.Abstractions.Rule;
+
+[ContextAware]
+public partial class PlayerPanelController : IController
+{
+ private readonly IUnRegisterList _unRegisterList = new UnRegisterList();
+
+ public void Initialize()
+ {
+ var model = this.GetModel();
+
+ model.Name
+ .RegisterWithInitValue(name =>
+ {
+ Console.WriteLine($"Player Name: {name}");
+ })
+ .AddToUnregisterList(_unRegisterList);
+
+ model.Health
+ .RegisterWithInitValue(health =>
+ {
+ Console.WriteLine($"Health: {health}");
+ })
+ .AddToUnregisterList(_unRegisterList);
+
+ model.HealthPercent
+ .RegisterWithInitValue(percent =>
+ {
+ Console.WriteLine($"Health Percent: {percent:P0}");
+ })
+ .AddToUnregisterList(_unRegisterList);
+ }
+
+ public void OnDamageButtonClicked()
+ {
+ this.SendCommand(new DamagePlayerCommand(15));
+ }
+
+ public void OnRenameButtonClicked(string newName)
+ {
+ this.SendCommand(new RenamePlayerCommand(newName));
+ }
+}
+```
+
+### 5. 什么时候这个示例比 BindableProperty 更合适
+
+如果你只需要:
+
+- `Health`
+- `Name`
+- `Level`
+
+分别独立通知,那么多个 `BindableProperty` 就足够了。
+
+如果你很快会遇到以下问题,这个 Store 方案会更稳:
+
+- 一次操作要同时修改多个字段
+- 同一个业务操作要在多个界面复用
+- 希望把“状态结构”和“状态变化规则”集中在一起
+- 未来要加入 middleware、调试记录或撤销/重做能力
+
+### 6. 推荐的落地方式
+
+在实际项目里,建议按这个顺序引入:
+
+1. 先把复杂聚合状态封装到某个 Model 内部
+2. 再把修改入口逐步迁移到 Command
+3. 最后在 Controller 层使用 selector 或 `ToBindableProperty()` 做局部绑定
+
+这样不会破坏现有 `BindableProperty` 的轻量工作流,也能让复杂状态逐步收敛到统一入口。
+
## 什么时候不用 Store
以下情况继续优先使用 `BindableProperty`: