23 KiB
FlowScope ArchitectureScope / Container 实现规格
背景
本文基于 docs/requirements/scope-feature-architecture-requirements.md,用于约束下一版 FlowScope 核心架构实现。
本轮设计不再沿用当前 RootContext + FeatureScope 的试验模型,也不继续在 IFeatureScope、ICommandContext、IQueryContext 上叠加规则。新方向是吸收 ft_3 Container 的核心能力和 API 手感,自研 FlowScope Container / Scope Tree。
第一阶段只解决一件事:
用统一的 ArchitectureScope 建立 Root -> Feature -> Child Scope 的容器树、注册规则、解析规则和生命周期语义。
暂时不进入:
- Command / Query / Event。
- 完整 MVC / MVVM。
- UI Panel 生命周期统一接管。
- 自动扫描所有 Feature。
- 复杂诊断面板。
设计目标
- Root、Feature、Child Scope 都是同一种
ArchitectureScope。 - Root 只是
Parent == null的 Scope,不拥有另一套接口模型。 - Feature 是业务模块安装定义,不是另一种容器。
CreateFeature<TFeature>()默认使用typeof(TFeature).Name创建 Scope,并自动调用Feature.Install(scope)。- 任意 Scope 可以随时
Register,但只能写入自己。 Resolve从当前 Scope 开始,找不到再向父 Scope 查找。- 子 Scope 可以覆盖父 Scope 的注册。
- 默认业务边界是
Root -> Feature,Child Scope 只作为复杂生命周期能力保留。 - 实现必须避免“外层说 Scope-local Register,内层只有 Root Register”的双重语义。
核心类型清单
FlowScope
静态入口类型,负责创建根 Scope。
建议职责:
Create()创建一个没有 Parent 的ArchitectureScope。- 不持有全局单例。
- 不提供静态
Resolve<T>()。
建议 API:
public static class FlowScope
{
public static IArchitectureScope Create();
public static IArchitectureScope Create(string rootName);
}
ArchitectureScope
统一 Scope 节点的内部实现类型。
建议职责:
- 保存 Scope 名称、父节点、子节点。
- 保存当前 Scope 的注册表。
- 保存当前 Scope 持有的实例和释放栈。
- 执行 Register / Resolve / CreateScope / CreateFeature / Dispose。
- 对外通过
IArchitectureScope暴露。
Root、Feature、Child Scope 不应分别实现不同运行时类型。它们的差异来自创建位置和安装内容,而不是来自继承层级。
IArchitectureScope
业务侧默认接触的 Scope 接口。
它既是容器访问入口,也是规则约束入口。
public interface IArchitectureScope : IScopeInfo, IDisposable
{
IArchitectureScope CreateScope(string name);
IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new();
IRegistrationBuilder Register<TImplementation>();
IRegistrationBuilder Register<TImplementation>(string name);
IRegistrationBuilder Register<TService, TImplementation>()
where TImplementation : TService;
IRegistrationBuilder Register<TService, TImplementation>(string name)
where TImplementation : TService;
void RegisterInstance<TService>(TService instance);
void RegisterInstance<TService>(TService instance, string name);
TService Resolve<TService>();
TService Resolve<TService>(string name);
bool TryResolve<TService>(out TService value);
bool TryResolve<TService>(out TService value, string name);
}
IScopeInfo
只读 Scope 元信息,避免业务代码通过 Parent 写父级。
public interface IScopeInfo
{
string Name { get; }
IScopeInfo Parent { get; }
string Path { get; }
bool IsDisposed { get; }
}
Parent 第一阶段建议只暴露为 IScopeInfo,而不是 IArchitectureScope。如果后续需要高级管理能力,应通过单独的内部接口或显式管理 API 暴露。
IFeature
Feature 是业务模块安装定义。
public interface IFeature
{
void Install(IArchitectureScope scope);
}
第一阶段不要求 IFeature : IDisposable。Feature 自身不是生命周期容器,Feature 相关对象的生命周期应由安装后的 Scope 管理。
如果某个 Feature 安装对象确实需要释放,应把它注册进 Scope,或由它在 Install 中注册需要释放的服务。
ServiceKey
注册和解析使用的键。
public readonly struct ServiceKey : IEquatable<ServiceKey>
{
public Type Type { get; }
public string Name { get; }
}
语义:
Type必须非空。Name可以为空或空字符串,但内部应归一化为同一种无名状态。(Type, Name)完全相同才视为同一个注册。
Registration
注册记录。
建议字段:
internal sealed class Registration
{
public ServiceKey Key { get; }
public Type ImplementationType { get; }
public object Instance { get; }
public ServiceLifetime Lifetime { get; }
public bool OwnsInstance { get; }
}
Registration 只描述“如何创建”,不应把所有 PerScope 缓存都放在注册所在 Scope 上。
ServiceLifetime
第一阶段支持四种生命周期。
public enum ServiceLifetime
{
Singleton,
PerScope,
Transient,
Instance
}
IRegistrationBuilder
声明生命周期的构建器。
public interface IRegistrationBuilder
{
void AsSingleton();
void PerScope();
void AsTransient();
}
第一阶段建议每次 Register 返回一个待完成 builder,只有调用生命周期方法后才写入注册表;或者默认 PerScope,但必须在 spec 与测试中固定。为了避免隐式行为,推荐第一阶段要求显式调用生命周期方法。
接口设计
第一阶段接口应围绕“统一 Scope 节点”组织,而不是围绕 Root / Feature 两种上下文组织。
公开接口分三层:
FlowScope
创建 Root Scope。
IArchitectureScope
默认业务接口,负责创建子 Scope、创建 Feature、注册、解析和释放。
IScopeInfo
只读诊断接口,暴露 Name / Parent / Path / IsDisposed。
Feature 接口只负责安装:
IFeature.Install(IArchitectureScope scope)
不在第一阶段公开:
IRootContextIFeatureScopeICommandContextIQueryContextIEventContext
原因是这些接口会把核心重新拉回“Root 特殊、Feature 特殊、通信模型提前进入核心”的旧方向。第一阶段所有业务和测试都应以 IArchitectureScope 为主入口。
容器生命周期语义
Singleton
Singleton 表示注册所在 Scope 持有唯一实例。
解析规则:
- 在某个 Scope 找到 Singleton 注册后,实例缓存到该注册所在 Scope。
- 子 Scope Resolve 到父 Scope 的 Singleton 注册时,拿到父 Scope 的同一个实例。
- 子 Scope Dispose 不释放父 Scope Singleton。
- 注册所在 Scope Dispose 时释放该 Singleton 实例。
适用:
- Root 注册全局服务。
- Feature 注册该 Feature 内唯一共享服务。
PerScope
PerScope 表示每个发起 Resolve 的 Scope 持有一个实例。
解析规则:
- 从当前 Scope 开始查找注册。
- 找到注册后,实例缓存到“发起 Resolve 的 Scope”,不是注册所在 Scope。
- 同一个发起 Scope 多次 Resolve 返回同一个实例。
- 不同子 Scope Resolve 同一个父级 PerScope 注册时,各自得到不同实例。
- 发起 Scope Dispose 时释放自己缓存的 PerScope 实例。
这是吸收 ft_3 Container 后最重要的生命周期能力,用于“同一个注册规则,在不同 Scope 下得到不同运行实例”。
Transient
Transient 表示每次 Resolve 都创建新实例。
解析规则:
- 不缓存。
- 如果创建出的实例实现
IDisposable,第一阶段建议由发起 Scope 跟踪并在 Dispose 时释放。 - 如果不跟踪 Transient Disposable,必须在接口文档里明确调用方负责释放。为了减少 Unity 项目泄漏风险,推荐跟踪释放。
Instance
Instance 表示注册外部传入实例。
解析规则:
- 每次 Resolve 返回同一个传入对象。
- 默认
OwnsInstance = false,Scope Dispose 不释放外部传入实例。 - 如后续需要 Scope 接管实例释放,可以增加显式 API,例如
RegisterOwnedInstance<T>(),第一阶段不做。
Dispose 顺序
Scope Dispose 必须满足:
- 幂等,多次 Dispose 不重复释放。
- 先 Dispose 子 Scope,再 Dispose 当前 Scope 自己持有的实例。
- 子 Scope Dispose 不影响父 Scope。
- 父 Scope Dispose 会递归 Dispose 所有子 Scope。
- 当前 Scope Dispose 后不能继续 Register / Resolve / CreateScope / CreateFeature。
- Dispose 过程中出现多个异常时,第一阶段可以选择:
- 记录第一个异常并继续释放剩余对象,最后抛出聚合异常。
- 或使用
AggregateException。
建议使用 LIFO 顺序释放当前 Scope 持有的实例,使后创建对象先释放。
Register 算法
规则
- 任意 Scope 可以随时 Register。
- Register 永远只写当前 Scope。
- Register 不会向父 Scope 写入。
- Register 不会向子 Scope 广播。
- 当前 Scope 已 Dispose 时 Register 失败。
- 同一 Scope 内相同
ServiceKey重复注册,第一阶段建议直接失败,避免隐式覆盖。
子 Scope 覆盖父 Scope 不是通过修改父注册实现,而是子 Scope 自己注册相同 ServiceKey。
算法
Register(scope, key, registration):
1. 如果 scope 已 Dispose,抛 ObjectDisposedException。
2. 校验 key.Type 非空。
3. 归一化 key.Name。
4. 如果 scope._registrations 已存在 key,抛 DuplicateRegistrationException。
5. 把 registration 写入 scope._registrations。
6. 返回或完成 builder。
命名注册
命名注册必须和无名注册共存。
Register<IRewardService, DefaultRewardService>()
Register<IRewardService, BossRewardService>("boss")
以上是两个不同 ServiceKey。
Resolve 算法
规则
- Resolve 从当前 Scope 开始。
- 当前 Scope 找不到时,再向 Parent 查找。
- 查找路径直到 Root。
- 子 Scope 的相同
ServiceKey会覆盖父 Scope。 - Resolve 不允许绕过当前 Scope 直接查 Root。
- 当前 Scope 已 Dispose 时 Resolve 失败。
查找算法
FindRegistration(scope, key):
current = scope
while current != null:
if current._registrations.TryGetValue(key, out registration):
return (registrationOwner: current, registration)
current = current.Parent
return null
创建算法
Resolve(scope, key):
1. 如果 scope 已 Dispose,抛 ObjectDisposedException。
2. result = FindRegistration(scope, key)。
3. 如果找不到,抛 ServiceNotFoundException。
4. 根据 registration.Lifetime 创建或读取实例。
5. 返回实例。
生命周期分支:
Singleton:
缓存位置 = registrationOwner
若缓存不存在,在 registrationOwner 中创建并记录释放
返回 registrationOwner 缓存
PerScope:
缓存位置 = resolveStartScope
若缓存不存在,在 resolveStartScope 中创建并记录释放
返回 resolveStartScope 缓存
Transient:
每次创建新实例
若实例需要 Scope 接管释放,记录到 resolveStartScope 释放栈
返回新实例
Instance:
返回 registration.Instance
TryResolve 算法
TryResolve(scope, key, out value):
1. 如果 scope 已 Dispose,抛 ObjectDisposedException。
2. 如果找不到注册,value = default,返回 false。
3. 如果找到注册,走 Resolve 创建流程。
4. 创建成功返回 true。
5. 创建过程中发生构造异常,不吞异常。
TryResolve 只表示“是否存在注册”,不表示“构造失败也返回 false”。
CreateScope 算法
规则
- 任意未 Dispose Scope 都可以创建子 Scope。
- 子 Scope 的 Parent 指向当前 Scope。
- 子 Scope 加入当前 Scope 的 Children 集合。
- 子 Scope 名称由调用方传入。
- 子 Scope 名称用于诊断和 Path,不作为服务注册 key。
- 同一父级下是否允许重名第一阶段建议禁止,便于诊断。
算法
CreateScope(parent, name):
1. 如果 parent 已 Dispose,抛 ObjectDisposedException。
2. 校验 name 非空。
3. 如果 parent.Children 已存在同名未释放 Scope,抛 DuplicateScopeNameException。
4. 创建 child = new ArchitectureScope(name, parent)。
5. parent.Children.Add(child)。
6. 返回 child。
Path 示例:
Root
Root/BossFightFeature
Root/BossFightFeature/RevivePanel
CreateFeature 算法
规则
CreateFeature<TFeature>()默认使用typeof(TFeature).Name。CreateFeature<TFeature>(name)创建当前 Scope 的子 Scope。- 创建成功后实例化
TFeature。 - 自动调用
feature.Install(featureScope)。 - 如果 Install 失败,必须 Dispose 已创建的 featureScope,并从父级 Children 中移除。
- 调用者不应手写 Scope 创建和 Feature 安装流程。
第一阶段接口
IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new();
算法
CreateFeature<TFeature>(parent):
return CreateFeature<TFeature>(parent, typeof(TFeature).Name)
CreateFeature<TFeature>(parent, name):
1. featureScope = parent.CreateScope(name)。
2. try:
feature = new TFeature()
feature.Install(featureScope)
return featureScope
catch:
featureScope.Dispose()
从 parent.Children 移除 featureScope
rethrow
后续扩展点
第一阶段先使用 new() 约束,避免把构造注入、工厂注册和 Feature 安装绑在一起。
后续如需 Feature 构造依赖注入,可增加:
IArchitectureScope CreateFeature<TFeature>(string name, Func<IArchitectureScope, TFeature> factory)
where TFeature : IFeature;
不要在第一阶段默认扫描或反射构造 Feature。
Dispose 算法
Scope 状态
建议内部状态:
Active
Disposing
Disposed
算法
Dispose(scope):
1. 如果 scope.State == Disposed,直接返回。
2. 如果 scope.State == Disposing,直接返回或抛 InvalidOperationException;推荐直接返回保证幂等。
3. scope.State = Disposing。
4. 复制 Children 列表。
5. 按创建顺序反向 Dispose 子 Scope。
6. 清空 Children。
7. 按 LIFO Dispose 当前 Scope 持有的实例。
8. 清空当前 Scope 的 PerScope / Singleton / Transient 跟踪缓存。
9. 从 Parent.Children 中移除自己。
10. scope.State = Disposed。
11. 如有释放异常,抛出聚合异常。
持有关系
当前 Scope 只释放自己持有的对象:
- 当前 Scope 创建的 Singleton。
- 当前 Scope 发起 Resolve 后缓存的 PerScope。
- 当前 Scope 接管的 Transient Disposable。
- 当前 Scope 创建的子 Scope。
当前 Scope 不释放:
- 父 Scope 持有的 Singleton。
- 父 Scope 持有的 PerScope 实例。
- 默认 RegisterInstance 传入的外部对象。
文件组织建议
第一阶段建议把核心集中在 Runtime Core,不引入 Command / Query / Event 目录。
My project/Assets/FlowScope/Runtime/
Core/
FlowScope.cs
IArchitectureScope.cs
IScopeInfo.cs
IFeature.cs
Core/Container/
ArchitectureScope.cs
ServiceKey.cs
Registration.cs
RegistrationBuilder.cs
ServiceLifetime.cs
ScopeExceptions.cs
测试建议:
My project/Assets/FlowScope/Tests/EditMode/
Core/
ArchitectureScopeRegistrationTests.cs
ArchitectureScopeResolveTests.cs
ArchitectureScopeFeatureTests.cs
ArchitectureScopeDisposeTests.cs
如果后续迁移到 package 结构,应保持相同命名空间和目录职责:
Packages/com.flowscope.kernel/Runtime/Core/
Packages/com.flowscope.kernel/Runtime/Core/Container/
Packages/com.flowscope.kernel/Tests/EditMode/Core/
命名空间建议:
FlowScope
FlowScope.Container
不要继续把新核心放到 FlowScope.Composition,避免和旧 RootContext + FeatureScope + Command/Query 试验模型混在一起。
测试用例清单
Root / Scope 统一模型
Create_ReturnsRootArchitectureScopeRootScope_HasNoParentChildScope_UsesSameArchitectureScopeContractAsRootScopePath_BuildsFromRootToChild
Register 规则
Register_WritesOnlyCurrentScopeRegister_OnDisposedScopeThrowsRegister_DuplicateKeyInSameScopeThrowsRegister_SameKeyInChildOverridesParentWithoutChangingParentRegister_NamedAndUnnamedKeysAreIndependentRegisterInstance_DefaultDoesNotDisposeExternalInstance
Resolve 规则
Resolve_FindsCurrentScopeRegistrationFirstResolve_FallsBackToParentWhenCurrentMissingResolve_ChildRegistrationOverridesParentResolve_MissingRegistrationThrowsTryResolve_MissingRegistrationReturnsFalseTryResolve_ConstructorFailureThrowsResolve_OnDisposedScopeThrows
生命周期
Singleton_IsCachedInRegistrationOwnerScopeSingleton_FromParentIsSharedByChildrenPerScope_IsCachedInResolveStartScopePerScope_FromParentCreatesDifferentInstancesPerChildScopeTransient_CreatesNewInstanceEveryResolveTransient_DisposableIsReleasedWhenOwningScopeDisposesChildDispose_DoesNotDisposeParentOwnedInstancesParentDispose_DisposesChildrenAndOwnedInstancesDispose_IsIdempotentDispose_UsesLifoForOwnedInstances
CreateScope
CreateScope_CreatesChildWithParentCreateScope_OnDisposedParentThrowsCreateScope_EmptyNameThrowsCreateScope_DuplicateSiblingNameThrowsChildDispose_RemovesChildFromParent
CreateFeature
CreateFeature_UsesFeatureTypeNameAsDefaultScopeNameCreateFeature_WithNameUsesExplicitNameCreateFeature_CallsInstallWithCreatedScopeCreateFeature_ReturnsInstalledScopeCreateFeature_InstallCanRegisterServicesToFeatureScopeCreateFeature_InstallFailureDisposesCreatedScopeCreateFeature_InstallFailureRemovesScopeFromParent
默认业务边界
FeatureScope_CanResolveRootServicesFeatureScope_CanOwnFeatureServicesTwoFeatureScopes_DoNotSharePerScopeFeatureServicesChildScope_CanOverrideFeatureServiceForLocalLifetimeFeatureWithoutChildScope_IsEnoughForNormalBusinessServices
当前旧实现迁移 / 替换策略
要替换的旧模型
当前 My project/Assets/FlowScope/Runtime/Composition 中的试验模型应作为替换对象,而不是继续扩展对象。
旧模型特征:
IRootContext与IFeatureScope是不同接口。RootContext和FeatureScope是不同实现。IFeatureScope同时混入IArchitectureScope、ICommandContext、IQueryContext。CreateFeatureScope(name)只创建 Feature Scope,没有统一 Child Scope 模型。- Command / Query / Architecture roles 与 Scope 基础能力耦合过早。
新模型应改为:
FlowScope.Create()
-> IArchitectureScope Root
Root.CreateFeature<TFeature>()
-> IArchitectureScope Feature
Feature.CreateScope("LocalLifetime")
-> IArchitectureScope Child
迁移步骤建议
第一阶段迁移只做核心替换,不迁移 Command / Query / Event。
- 新增
Core/Core.Container下的统一 Scope 类型和接口。 - 新增测试覆盖 Register / Resolve / CreateFeature / Dispose。
- 将旧 Composition 测试中仍有价值的用例迁移到新 Core 测试:
- Root 服务可被 Feature Resolve。
- Feature 级服务彼此隔离。
- 命名注册可解析。
- 删除或降级旧
RootContext/FeatureScope作为历史试验实现。 - 暂时不把旧
ICommandContext/IQueryContext接回新 Scope。 - 后续如果要恢复 Command / Query / Event,应作为 Scope 上层扩展模块,而不是第一阶段核心职责。
兼容策略
第一阶段不建议保留旧 API 兼容层。
原因:
CreateFeatureScope(name)会把心智继续拉回“Root 特殊、Feature 特殊”的两层模型。IFeatureScope会让调用侧继续依赖旧接口名。- Command / Query 混入旧 FeatureScope 会污染新核心边界。
如果必须短期兼容样例,可用单独 adapter 文件临时桥接,但不得作为新核心主入口:
LegacyCompositionAdapter
CreateFeatureScope(name) -> root.CreateScope(name)
该 adapter 应标记为迁移期代码,并在第一阶段验收后移除。
第一阶段验收标准
文档与边界
- 新核心 spec 明确以
ArchitectureScope为唯一 Scope 模型。 - 文档明确旧
RootContext + FeatureScope是替换对象。 - 文档明确第一阶段不进入 Command / Query / Event、完整 MVC/MVVM、UI Panel 生命周期统一接管。
API
- 存在
FlowScope.Create()根入口。 - 存在统一
IArchitectureScope。 Root、Feature、Child都通过IArchitectureScope表达。- 存在
IFeature.Install(IArchitectureScope scope)。 - 存在
CreateFeature<TFeature>()和CreateFeature<TFeature>(string name)。 CreateFeature<TFeature>()默认 Scope 名称为typeof(TFeature).Name。
Register / Resolve
- 任意 Scope 可以 Register。
- Register 只写当前 Scope。
- Resolve 从当前 Scope 开始。
- Resolve 找不到时向 Parent 查找。
- 子 Scope 相同 key 覆盖父 Scope。
- 子 Scope 不能修改父 Scope 注册。
生命周期
- 支持
Singleton、PerScope、Transient、Instance。 - 父级 Singleton 被子级共享。
- 父级 PerScope 注册在不同子 Scope 下生成不同实例。
- 子 Scope Dispose 不释放父 Scope 对象。
- 父 Scope Dispose 会释放全部子 Scope。
- Dispose 幂等。
测试
- EditMode 测试覆盖 Root / Feature / Child 统一模型。
- EditMode 测试覆盖 Register 只写当前 Scope。
- EditMode 测试覆盖 Resolve 向父级查找与子级覆盖。
- EditMode 测试覆盖 CreateFeature 默认名称、自动 Install、失败清理。
- EditMode 测试覆盖 Dispose 递归释放、子不释放父、幂等。
负向验收
第一阶段完成时,不应出现:
- 新核心依赖旧
RootContext。 - 新核心依赖旧
FeatureScope。 - 新核心把 Command / Query / Event 作为必需接口。
- 新核心要求每个 UI Panel 都创建 Scope。
- 新核心要求 Feature 内部必须使用 MVC 或 MVVM。
后续阶段预留
后续可以在 Scope Tree 稳定后再讨论:
- Command / Query / Event 作为扩展模块如何挂到 Scope。
- Feature 内部 MVC / MVVM 推荐层。
- UI Panel 生命周期是否需要独立模块接管。
- Scope 诊断树、注册来源、Resolve Trace。
- Feature 工厂的依赖注入构造。
- 自动扫描 Feature 的 marker-based 方案。
这些能力不应反向改变第一阶段核心事实:
FlowScope 的第一主角是统一 ArchitectureScope。