Files
FlowScope/docs/superpowers/plans/2026-06-17-architecture-scope-container-implementation-plan.md
2026-06-18 12:20:20 +08:00

36 KiB
Raw Blame History

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() 创建 RootRoot / 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: FAILCreateScope / 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: FAILCreateScope 未实现或未检查重名。

  • 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 buildScope 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 buildCreateFeature 默认名、显式名、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() 返回 Root IArchitectureScope
  • 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 生命周期统一接管。