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