繁体   English   中英

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

[英]Avoiding a nested service locator antipattern with DI / 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>();
    }
}

为清楚起见,该游戏包含一棵由组件派生的类的树:

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所指出的那样, 服务定位器是一种反模式

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

目标

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

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

我想写直观的:

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

并能够在组成根中指定

  • IAudioService由Audio类实现,您应该创建/获取一个单例(我可以做到!)
  • IShootingService由ShootingComponent实现,并且每个Screen都应该创建/获取其中之一
  • 根据IShootingService的INavigationService

我必须承认,我对后两者完全迷失了。 我不会在这里列出基于无止境的基于autofac的大量尝试,因为我已经进行了数十次长时间尝试,而且都没有远程功能。 我已经详细阅读了文档-我知道生命周期作用域和Owned<>都在我要关注的范围内,但是我看不到如何透明地注入作用域依赖项-但是我觉得DI总的来说,似乎应该为我要做的事情提供便利!

如果这是理智的,我该如何实现? 还是这只是恶魔般的? 如果是这样,当那些对象的生存期根据使用对象的上下文而变化时,您将如何构造这种应用程序以充分利用DI来避免递归地传递对象?

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;
    }
}

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

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

  • 首先开始使用模块。 它们是管理依赖项和配置的绝佳方法。
  • 定义具有适当生存期的依赖项:IAudioService作为单例,IShootingService根据生存期范围。
  • 确保您的生命周期作用域接口也实现了IDisposable,以确保正确清理。
  • 在一个简单的嵌入式框架中创建一个薄包装器来管理您的整个生命周期:将游戏级别夹在Begin()和End()方法之间。 (这就是我的方法。我敢肯定,您可以在自己的结构中找到一种更好的方法)
  • (可选)创建一个“核心”模块来保留您的通用依赖项(即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调用之间。

正如我所说,我相信您将能够在您的开发结构中提出一种更好的方法,我希望这能给您关于如何实现它的基本想法,前提是要考虑我的经验一种“好的”方式。

祝好运。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM