使用DI/autofac避免嵌套服务定位器反模式

El *_*rko 8 c# dependency-injection circular-dependency autofac

在之前的游戏项目中,我有一些方便的服务定位器反模式.我想用依赖注入替换它.autofac看起来像我最容易的DI容器,因为它似乎有相关的功能 - 但我无法弄清楚如何实现我正在寻找的东西.

现有方法

我有一个服务定位器而不是单个服务定位器,它可以委托给它的父节点(实际上提供"范围"服务):

class ServiceLocator {
    ServiceLocator _parent;
    Dictionary<Type, object> _registered = new Dictionary<Type, object>();

    public ServiceLocator(ServiceLocator parent = null) {
        _parent = parent;
    }

    public void Register<T>(T service) {
        _registered.Add(typeof(T), service);
    }

    public T Get<T>() {
        object service;
        if (_registered.TryGetValue(typeof(T), out service)) {
            return (T)service;
        }
        return _parent.Get<T>();
    }
}
Run Code Online (Sandbox Code Playgroud)

为简洁起见,游戏由一组Component派生类组成:

abstract class Component {
    protected ServiceLocator _ownServices;
    protected List<Component> _components = new List<Component>();
    ...

    public Component(ServiceLocator parentServices) {
        _ownServices = new ServiceLocator(parentServices);
    }

    ...
}
Run Code Online (Sandbox Code Playgroud)

所以我可以(并且确实)构建树结构,如:

Game
 -  Audio : IAudioService
 -  TitleScreen : Screen
 -  GameplayScreen : Screen
      -  ShootingComponent : IShootingService
      -  NavigationComponent : INavigationService
     |-  AIComponent (uses IAudioService and IShootingService and INavigationService)
Run Code Online (Sandbox Code Playgroud)

每个组件都可以简单地调用构造它的ServiceLocator来查找它需要的所有服务.

优点:

  • 组件无需关心谁实现他们使用的服务或这些服务所在的位置; 只要这些服务的生命周期等于或大于他们自己的生命周期.

  • 多个组件可以共享相同的服务,但只有在需要时才能存在该服务.特别是,当玩家退出关卡时,我们可以Dispose()层次结构的整个部分,这比使组件重建复杂数据结构以适应他们现在处于一个全新关卡的想法要容易得多.

缺点:

  • 正如Mark Seeman所指出的,Service Locator是一种反模式.

  • 一些组件将实例化服务提供者纯粹是因为我(程序员)知道嵌套组件需要该服务,或者我(程序员)知道游戏必须具有例如在游戏世界中运行的AI,而不是因为实例化器需要每个服务SE.

目标

根据DI的精神,我想从组件中删除所有关于"服务定位器"和"范围"的知识.因此,他们将为他们使用的每个服务接收(通过DI)构造函数参数.要将这些知识保留在组件之外,组合根必须为每个组件指定:

  • 是否实例化特定类型的组件会创建新范围
  • 在该范围内,哪些服务可用.

我想写直观的:

class AIComponent
{
    public AIComponent(IAudioService audio, IShootingService shooting, INavigationService navigation)
    {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

并且能够在组合中指定根

  • IAudioService由Audio类实现,你应该创建/获取一个单例(我可以这样做!)
  • IShootingService由ShootingComponent实现,每个屏幕应该有一个创建/获得的
  • 根据IShootingService的INavigationService

我必须承认,在谈到后两者时,我完全迷失了.我不会在这里列出我的无数次基于autofac的尝试,因为我已经在很长一段时间里做了几打,而且没有一个是远程功能的.我已经详细阅读了文档 - 我知道生命范围并且Owned<>在我正在看的领域,但是我看不到如何透明地注入范围内的依赖关系 - 但我觉得DI一般似乎应该准确地促进我的目标!

如果这是理智的,我怎么能实现这一目标?或者这只是恶魔般的?如果是这样,当这些对象的生命周期根据使用对象的上下文而变化时,如何构建这样一个充分利用DI的应用程序以避免递归传递对象?

Jim*_*lla 1

LifetimeScopes 听起来就是答案。我认为您基本上所做的是将生命周期范围与屏幕联系起来。因此 ShootingComponent 和朋友将注册到.InstancePerMatchingLifetimeScope("Screen"). 诀窍是在标记为“Screen”的新 LifetimeScope 中创建每个屏幕。我的第一个想法是制作一个像这样的屏幕工厂:

public class ScreenFactory
{
    private readonly ILifetimeScope _parent;

    public ScreenFactory(ILifetimeScope parent) { _parent = parent; }

    public TScreen CreateScreen<TScreen>() where TScreen : Screen
    {
        var screenScope = _parent.BeginLifetimeScope("Screen");
        var screen = screenScope.Resolve<TScreen>();
        screen.Closed += () => screenScope.Dispose();
        return screen;
    }
}
Run Code Online (Sandbox Code Playgroud)

这完全未经测试,但我认为这个概念是有道理的。