36 KiB
ArchitectureScope Container Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 按 docs/specs/architecture-scope-container-implementation-spec.md 实现统一的 ArchitectureScope / FlowScope Container / Scope Tree,并替换当前 RootContext + FeatureScope 试验入口。
Architecture: 第一阶段只做核心 Scope 与容器边界:FlowScope.Create() 创建 Root,Root / Feature / Child 都通过同一个 IArchitectureScope 表达。保留现有 FlowScope.Container.Container 中已经成立的父子查找、局部覆盖、PerScope()、Dispose 子树等能力,在其上新增 Core 接口与 ArchitectureScope 包装/约束层;旧 FlowScope.Composition 不继续扩展,最后删除或降级。
Tech Stack: Unity C#,NUnit EditMode tests,现有 FlowScope.Runtime.asmdef / FlowScope.Tests.EditMode.asmdef,验证优先使用 dotnet build --no-restore。
执行边界
本计划只实现以下内容:
FlowScope.Create()根入口。IArchitectureScope/IScopeInfo/IFeature/IRegistrationBuilder。ArchitectureScope统一 Scope 节点。- Register / Resolve / CreateScope / CreateFeature / Dispose 规则。
- 旧 Composition 测试中仍有价值的 Root 服务、Feature 隔离、命名注册用例迁移到 Core 测试。
- 移除或降级旧
RootContext + FeatureScope + Command/Query试验入口。
本计划不实现:
- Command / Query / Event。
- 完整 MVC / MVVM。
- UI Panel 生命周期统一接管。
- 自动扫描 Feature。
- 资源、存档、音频、UI 模块改造。
执行前必须先确认当前工作树已有改动,不能覆盖用户已有修改:
git status --short
git diff -- "My project/Assets/FlowScope/Runtime/Container/Container.cs"
git diff -- "My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs"
git diff -- "My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs"
如果这些文件已有未合并改动,执行者必须在当前改动基础上续写,不得 git restore 或重写文件。
文件结构
新增 Runtime Core 文件
- Create:
My project/Assets/FlowScope/Runtime/Core/FlowScope.cs- 静态入口,创建 Root Scope。
- Create:
My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs- 统一 Scope 主接口。
- Create:
My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs- 只读 Scope 元信息接口。
- Create:
My project/Assets/FlowScope/Runtime/Core/IFeature.cs- Feature 安装接口。
- Create:
My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs- Core 层注册生命周期 builder 接口。
- Create:
My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs- 统一 Scope 节点实现。
- Create:
My project/Assets/FlowScope/Runtime/Core/Container/ScopeExceptions.cs- 核心 Scope 异常类型。
修改现有 Container 文件
- Modify:
My project/Assets/FlowScope/Runtime/Container/Container.cs- 暴露
ParentInfo/Path/IsDisposed所需的最小只读能力。 - 增加同级重名 Scope 检查。
- 确保
CreateScope(string name)空名失败。 - 保持注册只写当前 Container,不改变父级。
- 暴露
- Modify:
My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs- 将
ServiceLifetime.Scoped命名调整为PerScope,或在 Core 层适配为PerScope。 - 保持
PerScope缓存到 Resolve 发起 Scope 的语义。
- 将
新增 Core EditMode 测试
- Create:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs - Create:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs - Create:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs - Create:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs
删除或降级旧 Composition
- Delete or quarantine:
My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs - Delete or quarantine:
My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs - Delete or migrate:
My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs
如果执行时选择“降级”而不是删除,必须放到 FlowScope.LegacyComposition 命名空间,并且不得被新 Core 测试依赖。
Task 1: 新增 Core 接口与根入口
Files:
-
Create:
My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs -
Create:
My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs -
Create:
My project/Assets/FlowScope/Runtime/Core/IFeature.cs -
Create:
My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs -
Create:
My project/Assets/FlowScope/Runtime/Core/FlowScope.cs -
Test:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs -
Step 1: 创建目录和 Unity meta
执行者在 Unity 项目中创建目录:
My project/Assets/FlowScope/Runtime/Core/
My project/Assets/FlowScope/Runtime/Core/Container/
My project/Assets/FlowScope/Tests/EditMode/Core/
如果通过文件系统创建,执行后打开 Unity 或保持后续生成 .meta 被纳入同一提交。不要手写 .meta。
- Step 2: 先写 Root 入口测试
在 ArchitectureScopeRegistrationTests.cs 写入:
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeRegistrationTests
{
[Test]
public void Create_ReturnsRootArchitectureScope()
{
using var root = FlowScope.FlowScope.Create();
Assert.That(root.Name, Is.EqualTo("Root"));
Assert.That(root.Parent, Is.Null);
Assert.That(root.Path, Is.EqualTo("Root"));
Assert.That(root.IsDisposed, Is.False);
}
}
}
- Step 3: 运行测试确认失败
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: FAIL,缺少 FlowScope.FlowScope / IArchitectureScope。
- Step 4: 新增接口代码
IScopeInfo.cs:
namespace FlowScope
{
public interface IScopeInfo
{
string Name { get; }
IScopeInfo Parent { get; }
string Path { get; }
bool IsDisposed { get; }
}
}
IRegistrationBuilder.cs:
namespace FlowScope
{
public interface IRegistrationBuilder
{
void AsSingleton();
void PerScope();
void AsTransient();
}
}
IFeature.cs:
namespace FlowScope
{
public interface IFeature
{
void Install(IArchitectureScope scope);
}
}
IArchitectureScope.cs:
using System;
namespace FlowScope
{
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);
}
}
- Step 5: 新增临时可编译入口
FlowScope.cs:
using FlowScope.Core.Container;
namespace FlowScope
{
public static class FlowScope
{
public static IArchitectureScope Create()
{
return Create("Root");
}
public static IArchitectureScope Create(string rootName)
{
return new ArchitectureScope(rootName);
}
}
}
- Step 6: 暂存后续实现需要的空壳
ArchitectureScope.cs 先写可编译空壳,但未实现行为:
using System;
namespace FlowScope.Core.Container
{
internal sealed class ArchitectureScope : IArchitectureScope
{
public ArchitectureScope(string name)
{
Name = name;
Path = name;
}
public string Name { get; }
public IScopeInfo Parent => null;
public string Path { get; }
public bool IsDisposed { get; private set; }
public IArchitectureScope CreateScope(string name) => throw new NotImplementedException();
public IArchitectureScope CreateFeature<TFeature>() where TFeature : IFeature, new() => throw new NotImplementedException();
public IArchitectureScope CreateFeature<TFeature>(string name) where TFeature : IFeature, new() => throw new NotImplementedException();
public IRegistrationBuilder Register<TImplementation>() => throw new NotImplementedException();
public IRegistrationBuilder Register<TImplementation>(string name) => throw new NotImplementedException();
public IRegistrationBuilder Register<TService, TImplementation>() where TImplementation : TService => throw new NotImplementedException();
public IRegistrationBuilder Register<TService, TImplementation>(string name) where TImplementation : TService => throw new NotImplementedException();
public void RegisterInstance<TService>(TService instance) => throw new NotImplementedException();
public void RegisterInstance<TService>(TService instance, string name) => throw new NotImplementedException();
public TService Resolve<TService>() => throw new NotImplementedException();
public TService Resolve<TService>(string name) => throw new NotImplementedException();
public bool TryResolve<TService>(out TService value, string name) => throw new NotImplementedException();
public bool TryResolve<TService>(out TService value) => throw new NotImplementedException();
public void Dispose() => IsDisposed = true;
}
}
- Step 7: 运行测试确认入口测试通过
dotnet build "My project/FlowScope.Runtime.csproj" --no-restore
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: PASS build。此时只有 Root 入口行为成立。
建议提交点: feat: 新增 ArchitectureScope 核心接口入口
Task 2: 实现 Register 只写当前 Scope
Files:
-
Modify:
My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs -
Create:
My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs -
Test:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs -
Step 1: 增加 Register 测试
向 ArchitectureScopeRegistrationTests.cs 增加:
[Test]
public void Register_WritesOnlyCurrentScope()
{
using var root = FlowScope.FlowScope.Create();
using var child = root.CreateScope("Child");
root.Register<IService, RootService>().AsSingleton();
child.Register<IService, ChildService>().AsSingleton();
Assert.That(root.Resolve<IService>(), Is.TypeOf<RootService>());
Assert.That(child.Resolve<IService>(), Is.TypeOf<ChildService>());
}
[Test]
public void Register_DuplicateKeyInSameScopeThrows()
{
using var root = FlowScope.FlowScope.Create();
root.Register<IService, RootService>().AsSingleton();
Assert.Throws<InvalidOperationException>(() =>
root.Register<IService, ChildService>().AsSingleton());
}
private interface IService { }
private sealed class RootService : IService { }
private sealed class ChildService : IService { }
- Step 2: 运行测试确认失败
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: FAIL,CreateScope / Register / Resolve 未实现。
- Step 3: 用现有 Container 实现 Register 代理
RegistrationBuilderAdapter.cs:
using FlowScope.Container;
namespace FlowScope.Core.Container
{
internal sealed class RegistrationBuilderAdapter : IRegistrationBuilder
{
private readonly ContainerRegistrationBuilder _inner;
public RegistrationBuilderAdapter(ContainerRegistrationBuilder inner)
{
_inner = inner;
}
public void AsSingleton() => _inner.AsSingleton();
public void PerScope() => _inner.PerScope();
public void AsTransient() => _inner.AsTransient();
}
}
在 ArchitectureScope 中持有现有 Container:
private readonly FlowScope.Container.Container _container;
public IRegistrationBuilder Register<TImplementation>()
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TImplementation>());
}
public IRegistrationBuilder Register<TImplementation>(string name)
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TImplementation>(name));
}
public IRegistrationBuilder Register<TService, TImplementation>()
where TImplementation : TService
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>());
}
public IRegistrationBuilder Register<TService, TImplementation>(string name)
where TImplementation : TService
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>(name));
}
public void RegisterInstance<TService>(TService instance)
{
RegisterInstance(instance, null);
}
public void RegisterInstance<TService>(TService instance, string name)
{
ThrowIfDisposed();
_container.RegisterInstance(instance, name);
}
- Step 4: 实现 Resolve 代理
public TService Resolve<TService>()
{
ThrowIfDisposed();
return _container.Resolve<TService>();
}
public TService Resolve<TService>(string name)
{
ThrowIfDisposed();
return _container.Resolve<TService>(name);
}
public bool TryResolve<TService>(out TService value)
{
ThrowIfDisposed();
return _container.TryResolve(out value);
}
public bool TryResolve<TService>(out TService value, string name)
{
ThrowIfDisposed();
return _container.TryResolve(out value, name);
}
- Step 5: 运行测试确认通过
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: PASS build,新增 Register 测试通过。
建议提交点: feat: 实现 ArchitectureScope 注册解析入口
Task 3: 实现 Scope Tree 与 Path
Files:
-
Modify:
My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs -
Modify:
My project/Assets/FlowScope/Runtime/Container/Container.cs -
Test:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs -
Step 1: 写 Scope Tree 测试
ArchitectureScopeResolveTests.cs:
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeResolveTests
{
[Test]
public void CreateScope_CreatesChildWithParentAndPath()
{
using var root = FlowScope.FlowScope.Create();
using var feature = root.CreateScope("BossFightFeature");
using var child = feature.CreateScope("RevivePanel");
Assert.That(feature.Parent, Is.SameAs(root));
Assert.That(child.Parent, Is.SameAs(feature));
Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature"));
Assert.That(child.Path, Is.EqualTo("Root/BossFightFeature/RevivePanel"));
}
[Test]
public void CreateScope_DuplicateSiblingNameThrows()
{
using var root = FlowScope.FlowScope.Create();
using var first = root.CreateScope("BossFightFeature");
Assert.Throws<InvalidOperationException>(() => root.CreateScope("BossFightFeature"));
}
}
}
- Step 2: 运行测试确认失败
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: FAIL,CreateScope 未实现或未检查重名。
- Step 3: 在 ArchitectureScope 内维护子 Scope
实现要点:
private readonly ArchitectureScope _parent;
private readonly List<ArchitectureScope> _children = new List<ArchitectureScope>();
public IScopeInfo Parent => _parent;
public IArchitectureScope CreateScope(string name)
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
}
if (_children.Exists(child => !child.IsDisposed && child.Name == name))
{
throw new InvalidOperationException($"Scope '{name}' already exists under '{Path}'.");
}
var childContainer = _container.CreateScope(name);
var child = new ArchitectureScope(name, this, childContainer);
_children.Add(child);
return child;
}
构造函数需要变成:
public ArchitectureScope(string name)
: this(name, null, new FlowScope.Container.Container())
{
}
private ArchitectureScope(string name, ArchitectureScope parent, FlowScope.Container.Container container)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
}
Name = name;
_parent = parent;
_container = container;
Path = parent == null ? name : $"{parent.Path}/{name}";
}
- Step 4: 确保 Dispose 从父节点移除
在 Dispose 中释放子 Scope 后:
_parent?._children.Remove(this);
- Step 5: 运行测试确认通过
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: PASS build,Scope Tree / Path 测试通过。
建议提交点: feat: 实现 ArchitectureScope 树结构
Task 4: 固化 Resolve 父级查找与子级覆盖
Files:
-
Modify:
My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs -
Modify:
My project/Assets/FlowScope/Runtime/Container/Container.cs -
Test:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs -
Step 1: 增加父级查找与覆盖测试
[Test]
public void Resolve_FallsBackToParentWhenCurrentMissing()
{
using var root = FlowScope.FlowScope.Create();
using var feature = root.CreateScope("BossFightFeature");
var clock = new RootClock(42);
root.RegisterInstance<IRootClock>(clock);
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
}
[Test]
public void Resolve_ChildRegistrationOverridesParent()
{
using var root = FlowScope.FlowScope.Create();
using var feature = root.CreateScope("BossFightFeature");
root.Register<IRewardService, GlobalRewardService>().AsSingleton();
feature.Register<IRewardService, BossRewardService>().AsSingleton();
Assert.That(root.Resolve<IRewardService>(), Is.TypeOf<GlobalRewardService>());
Assert.That(feature.Resolve<IRewardService>(), Is.TypeOf<BossRewardService>());
}
private interface IRootClock { int Value { get; } }
private sealed class RootClock : IRootClock
{
public RootClock(int value) => Value = value;
public int Value { get; }
}
private interface IRewardService { }
private sealed class GlobalRewardService : IRewardService { }
private sealed class BossRewardService : IRewardService { }
- Step 2: 运行测试
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: PASS。现有 Container.TryFindRegistration 已支持向父查找;如果失败,修复 Container 父子链传递。
- Step 3: 增加 TryResolve 语义测试
[Test]
public void TryResolve_MissingRegistrationReturnsFalse()
{
using var root = FlowScope.FlowScope.Create();
Assert.That(root.TryResolve<IRewardService>(out var value), Is.False);
Assert.That(value, Is.Null);
}
- Step 4: 确保构造异常不被 TryResolve 吞掉
[Test]
public void TryResolve_ConstructorFailureThrows()
{
using var root = FlowScope.FlowScope.Create();
root.Register<BrokenService>().AsTransient();
Assert.Throws<InvalidOperationException>(() =>
root.TryResolve<BrokenService>(out _));
}
private sealed class BrokenService
{
public BrokenService()
{
throw new InvalidOperationException("broken");
}
}
Expected: PASS。若现有反射调用把异常包成 TargetInvocationException,按当前实际行为断言外层异常,但不要返回 false。
建议提交点: test: 固化 Scope 解析查找语义
Task 5: 固化生命周期语义
Files:
-
Modify:
My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs -
Modify:
My project/Assets/FlowScope/Runtime/Container/Container.cs -
Test:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs -
Step 1: 写生命周期测试
ArchitectureScopeDisposeTests.cs:
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeDisposeTests
{
[Test]
public void Singleton_FromParentIsSharedByChildrenAndDisposedByParent()
{
using var root = FlowScope.FlowScope.Create();
var first = root.CreateScope("First");
var second = root.CreateScope("Second");
root.Register<IDisposableService, DisposableService>().AsSingleton();
var rootInstance = root.Resolve<IDisposableService>();
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(rootInstance));
Assert.That(second.Resolve<IDisposableService>(), Is.SameAs(rootInstance));
first.Dispose();
Assert.That(rootInstance.IsDisposed, Is.False);
root.Dispose();
Assert.That(rootInstance.IsDisposed, Is.True);
}
[Test]
public void PerScope_FromParentCreatesDifferentInstancesPerChildScope()
{
using var root = FlowScope.FlowScope.Create();
var first = root.CreateScope("First");
var second = root.CreateScope("Second");
root.Register<IDisposableService, DisposableService>().PerScope();
var firstInstance = first.Resolve<IDisposableService>();
var secondInstance = second.Resolve<IDisposableService>();
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(firstInstance));
Assert.That(second.Resolve<IDisposableService>(), Is.SameAs(secondInstance));
Assert.That(firstInstance, Is.Not.SameAs(secondInstance));
first.Dispose();
Assert.That(firstInstance.IsDisposed, Is.True);
Assert.That(secondInstance.IsDisposed, Is.False);
}
private interface IDisposableService
{
bool IsDisposed { get; }
}
private sealed class DisposableService : IDisposableService, System.IDisposable
{
public bool IsDisposed { get; private set; }
public void Dispose() => IsDisposed = true;
}
}
}
- Step 2: 运行测试
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: PASS。现有 Container 大概率已满足;如果失败,修复 ContainerRegistration.ResolveScoped(requester) 的缓存归属。
- Step 3: 写 Dispose 顺序与幂等测试
[Test]
public void Dispose_ParentDisposesChildrenBeforeOwnInstancesAndIsIdempotent()
{
DisposeRecorder.Order.Clear();
var root = FlowScope.FlowScope.Create();
var child = root.CreateScope("Child");
root.Register<ITracked, RootTracked>().AsSingleton();
child.Register<IChildTracked, ChildTracked>().AsSingleton();
root.Resolve<ITracked>();
child.Resolve<IChildTracked>();
root.Dispose();
root.Dispose();
CollectionAssert.AreEqual(new[] { "child", "root" }, DisposeRecorder.Order);
}
private interface ITracked { }
private interface IChildTracked { }
private static class DisposeRecorder
{
public static readonly System.Collections.Generic.List<string> Order =
new System.Collections.Generic.List<string>();
}
private sealed class RootTracked : ITracked, System.IDisposable
{
public void Dispose() => DisposeRecorder.Order.Add("root");
}
private sealed class ChildTracked : IChildTracked, System.IDisposable
{
public void Dispose() => DisposeRecorder.Order.Add("child");
}
- Step 4: 明确 Instance 不释放外部实例
保留或迁移现有 ContainerTests.RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance,在 Core 测试中也覆盖:
[Test]
public void RegisterInstance_DefaultDoesNotDisposeExternalInstance()
{
var root = FlowScope.FlowScope.Create();
var service = new DisposableService();
root.RegisterInstance<IDisposableService>(service);
root.Dispose();
Assert.That(service.IsDisposed, Is.False);
}
建议提交点: test: 固化 ArchitectureScope 生命周期语义
Task 6: 实现 CreateFeature 自动创建与安装
Files:
-
Modify:
My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs -
Test:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs -
Step 1: 写 CreateFeature 测试
ArchitectureScopeFeatureTests.cs:
using System;
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeFeatureTests
{
[Test]
public void CreateFeature_UsesFeatureTypeNameAndCallsInstall()
{
using var root = FlowScope.FlowScope.Create();
using var feature = root.CreateFeature<BossFightFeature>();
Assert.That(feature.Name, Is.EqualTo(nameof(BossFightFeature)));
Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature"));
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
}
[Test]
public void CreateFeature_WithNameUsesExplicitName()
{
using var root = FlowScope.FlowScope.Create();
using var feature = root.CreateFeature<BossFightFeature>("BossFight");
Assert.That(feature.Name, Is.EqualTo("BossFight"));
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
}
private sealed class BossFightFeature : IFeature
{
public void Install(IArchitectureScope scope)
{
scope.Register<BossFightModel>().PerScope();
}
}
private sealed class BossFightModel { }
}
}
- Step 2: 实现 CreateFeature
在 ArchitectureScope 中:
public IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new()
{
return CreateFeature<TFeature>(typeof(TFeature).Name);
}
public IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new()
{
ThrowIfDisposed();
var featureScope = (ArchitectureScope)CreateScope(name);
try
{
var feature = new TFeature();
feature.Install(featureScope);
return featureScope;
}
catch
{
featureScope.Dispose();
throw;
}
}
- Step 3: 写 Install 失败清理测试
[Test]
public void CreateFeature_WhenInstallFailsDisposesCreatedScope()
{
using var root = FlowScope.FlowScope.Create();
Assert.Throws<InvalidOperationException>(() => root.CreateFeature<BrokenFeature>());
using var feature = root.CreateFeature<BossFightFeature>(nameof(BrokenFeature));
Assert.That(feature.Name, Is.EqualTo(nameof(BrokenFeature)));
}
private sealed class BrokenFeature : IFeature
{
public void Install(IArchitectureScope scope)
{
throw new InvalidOperationException("install failed");
}
}
这个测试证明失败 Scope 已从父级移除,否则同名创建会失败。
- Step 4: 运行测试确认通过
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: PASS build,CreateFeature 默认名、显式名、Install、失败清理全部通过。
建议提交点: feat: 实现 CreateFeature 自动安装
Task 7: 迁移有价值的旧 Composition 用例
Files:
-
Modify:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs -
Modify:
My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs -
Delete or quarantine:
My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs -
Step 1: 迁移 Root 服务可被 Feature Resolve
新增测试:
[Test]
public void FeatureScope_CanResolveRootServices()
{
using var root = FlowScope.FlowScope.Create();
var clock = new RootClock(10);
root.RegisterInstance<IRootClock>(clock);
using var feature = root.CreateFeature<ClockFeature>();
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
Assert.That(feature.Resolve<ClockModel>().Value, Is.EqualTo(10));
}
private interface IRootClock { int Value { get; } }
private sealed class RootClock : IRootClock
{
public RootClock(int value) => Value = value;
public int Value { get; }
}
private sealed class ClockFeature : IFeature
{
public void Install(IArchitectureScope scope)
{
scope.Register<ClockModel>().PerScope();
}
}
private sealed class ClockModel
{
public ClockModel(IRootClock clock) => Value = clock.Value;
public int Value { get; }
}
- Step 2: 迁移 Feature 级服务隔离
新增测试:
[Test]
public void TwoFeatureScopes_DoNotSharePerScopeFeatureServices()
{
using var root = FlowScope.FlowScope.Create();
root.Register<IFeatureService, FeatureService>().PerScope();
using var first = root.CreateFeature<EmptyFeature>("FirstActivity");
using var second = root.CreateFeature<EmptyFeature>("SecondActivity");
Assert.That(first.Resolve<IFeatureService>(), Is.SameAs(first.Resolve<IFeatureService>()));
Assert.That(first.Resolve<IFeatureService>(), Is.Not.SameAs(second.Resolve<IFeatureService>()));
}
private interface IFeatureService { }
private sealed class FeatureService : IFeatureService { }
private sealed class EmptyFeature : IFeature
{
public void Install(IArchitectureScope scope) { }
}
- Step 3: 迁移命名注册
新增测试:
[Test]
public void NamedRegistration_ResolvesByTypeAndNameFromFeature()
{
using var root = FlowScope.FlowScope.Create();
root.Register<IRewardService, BossRewardService>("boss").PerScope();
root.Register<IRewardService, ScratchRewardService>("scratch").PerScope();
using var boss = root.CreateFeature<EmptyFeature>("BossFight");
using var scratch = root.CreateFeature<EmptyFeature>("ScratchTicket");
Assert.That(boss.Resolve<IRewardService>("boss"), Is.TypeOf<BossRewardService>());
Assert.That(scratch.Resolve<IRewardService>("scratch"), Is.TypeOf<ScratchRewardService>());
Assert.That(boss.Resolve<IRewardService>("boss"), Is.SameAs(boss.Resolve<IRewardService>("boss")));
Assert.That(boss.Resolve<IRewardService>("boss"), Is.Not.SameAs(scratch.Resolve<IRewardService>("boss")));
}
private interface IRewardService { }
private sealed class BossRewardService : IRewardService { }
private sealed class ScratchRewardService : IRewardService { }
- Step 4: 不迁移 Command / Query 测试
旧测试 FeatureScope_CommandAndQueryOperateOnScopedArchitecture 不迁移。原因:spec 明确第一阶段不进入 Command / Query / Event。
- Step 5: 运行测试
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: PASS build。
建议提交点: test: 迁移 Composition 核心用例到 ArchitectureScope
Task 8: 删除或隔离旧 Composition 试验线
Files:
-
Delete or quarantine:
My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs -
Delete or quarantine:
My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs -
Delete or quarantine:
My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs -
Step 1: 查找旧 Composition 引用
rg -n "FlowScope\.Composition|IRootContext|IFeatureScope|ICommandContext|IQueryContext|CreateFeatureScope|FlowScopeApp" "My project/Assets/FlowScope"
Expected: 只剩 Runtime/Composition 与 Tests/EditMode/Composition 自身引用。
- Step 2: 删除旧 Runtime/Composition 文件
如果没有外部引用,执行:
git rm "My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs" "My project/Assets/FlowScope/Runtime/Composition/CompositionContracts.cs.meta" "My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs" "My project/Assets/FlowScope/Runtime/Composition/FlowScopeApp.cs.meta"
如果 Unity .meta 尚未生成或未跟踪,按实际文件列表只删除存在的路径。
- Step 3: 删除旧 Composition 测试
git rm "My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs" "My project/Assets/FlowScope/Tests/EditMode/Composition/CompositionTests.cs.meta"
- Step 4: 删除空目录 meta
如果目录为空,删除对应目录和 .meta:
git rm "My project/Assets/FlowScope/Runtime/Composition.meta" "My project/Assets/FlowScope/Tests/EditMode/Composition.meta"
如果目录里仍有迁移期 adapter,不删除目录。
- Step 5: 再次确认旧入口不存在
rg -n "FlowScope\.Composition|IRootContext|IFeatureScope|ICommandContext|IQueryContext|CreateFeatureScope|FlowScopeApp" "My project/Assets/FlowScope"
Expected: exit code 1,表示没有匹配;这里 exit code 1 是通过条件。
建议提交点: refactor: 移除旧 Composition 试验入口
Task 9: 总体验证与验收
Files:
-
Verify:
My project/FlowScope.Runtime.csproj -
Verify:
My project/FlowScope.Tests.EditMode.csproj -
Verify:
docs/specs/architecture-scope-container-implementation-spec.md -
Step 1: 编译 Runtime
dotnet build "My project/FlowScope.Runtime.csproj" --no-restore
Expected: Build succeeded。
- Step 2: 编译 EditMode Tests
dotnet build "My project/FlowScope.Tests.EditMode.csproj" --no-restore
Expected: Build succeeded。
- Step 3: 验证负向边界
rg -n "RootContext|FeatureScope|IRootContext|IFeatureScope|ICommandContext|IQueryContext|CreateFeatureScope|FlowScopeApp" "My project/Assets/FlowScope/Runtime"
Expected: exit code 1,表示 Runtime 中不再存在旧试验入口。
- Step 4: 验证第一阶段没有引入后续模块
rg -n "CommandContext|QueryContext|EventContext|UseMvvm|UseMvc|UIPanelLifecycle" "My project/Assets/FlowScope/Runtime/Core"
Expected: exit code 1,表示 Core 没有提前引入 Command / Query / Event / MVC / MVVM / UI Panel 生命周期接管。
- Step 5: 格式与空白检查
git diff --check
Expected: 无输出。
- Step 6: 检查 staged 范围
如果用户要求提交,执行者只应 stage 本计划相关文件:
git add "My project/Assets/FlowScope/Runtime/Core" "My project/Assets/FlowScope/Tests/EditMode/Core" "My project/Assets/FlowScope/Runtime/Container/Container.cs" "My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs"
如果旧 Composition 文件被删除,再额外 stage 精确删除路径。不要 broad add 整个项目。
建议最终提交信息: feat: 实现统一 ArchitectureScope 容器树
自检清单
FlowScope.Create()返回 RootIArchitectureScope。- Root / Feature / Child 都是同一种
IArchitectureScope。 CreateFeature<TFeature>()默认名为typeof(TFeature).Name。CreateFeature<TFeature>(name)自动创建 Scope 并调用Install。- 任意 Scope 可 Register,但只写自己。
- Resolve 从当前 Scope 开始,找不到向 Parent 查找。
- 子 Scope 覆盖父 Scope 时不修改父 Scope。
- Singleton 由注册所在 Scope 持有。
- PerScope 由 Resolve 发起 Scope 持有。
- Transient 每次创建新实例,并按最终规则释放。
- RegisterInstance 默认不释放外部实例。
- 子 Scope Dispose 不释放父 Scope 对象。
- 父 Scope Dispose 释放所有子 Scope。
- Dispose 幂等。
- 旧
RootContext + FeatureScope + Command/Query不再是新核心入口。 - 第一阶段没有实现 Command / Query / Event、完整 MVC/MVVM、UI Panel 生命周期统一接管。