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

时间:2013-12-26 16:47:40

标签: c# dependency-injection autofac circular-dependency

我在之前的游戏项目中有一些方便的服务定位器反模式。我想用依赖注入替换它。 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>();
    }
}

为了清晰起见,游戏由一组Component派生类组成:

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

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

    ...
}

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

Game
 -  Audio : IAudioService
 -  TitleScreen : Screen
 -  GameplayScreen : Screen
      -  ShootingComponent : IShootingService
      -  NavigationComponent : INavigationService
     |-  AIComponent (uses IAudioService and IShootingService and INavigationService)

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

优点:

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

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

缺点:

  • Mark Seeman指出,Service Locator is an Anti-Pattern

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

目标

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

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

我想写直观的:

class AIComponent
{
    public AIComponent(IAudioService audio, IShootingService shooting, INavigationService navigation)
    {
        ...
    }
}

并且能够在组合根中指定

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

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

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

2 个答案:

答案 0 :(得分:1)

LifetimeScope听起来像答案。我认为你基本上做的是将生命周期范围与屏幕联系起来。所以ShootingComponent和朋友将在.InstancePerMatchingLifetimeScope("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;
    }
}

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

答案 1 :(得分:1)

巧合的是,我目前正在处理类似的要求(使用Autofac),这是我到目前为止所提出的:

  • 首先开始使用模块。它们是管理依赖关系和配置的绝佳方式。
  • 使用适当的生命周期定义依赖关系:IAudioService as singleton,IShootingService按生命周期范围。
  • 确保您的生命周期范围界面也实现IDisposable以确保正确清理。
  • 创建一个瘦包装器来管理您在一个简单的嵌入式框架中的所有生命周期:将您的游戏关卡夹在Begin()和End()方法之间。 (这就是我做的方式。我确信你可以在自己的结构中找到更好的方法)
  • (可选)创建一个&#39;核心&#39;模块保留您的通用依赖项(即IAudioService)和其他依赖岛的单独模块(例如,可能取决于同一接口的不同实现)

这是我如何做到的一个例子:

public ScopedObjects(ILifetimeScope container, IModule module)
{
    _c = container;
    _m = module;
}

public void Begin()
{
    _scope = _c.BeginLifetimeScope(b => b.RegisterModule(_m));
}

public T Resolve<T>()
{
    return _scope.Resolve<T>();
}

public void End()
{
    _scope.Dispose();
}

在你给出的例子中,我会在进入关卡时将AIComponent的分辨率放在上述类的Begin和End调用之间。

正如我所说,我相信你能够在你的开发结构中找到更好的方法,我希望这能为你提供如何实现它的基本思路,假设我的经验被认为是一种“好”的经历。这样做的方式。

祝你好运。