【问题标题】:Is it OK to resolve from a DI container outside of the composition root when dealing with plugins?处理插件时是否可以从组合根之外的 DI 容器中解析?
【发布时间】:2014-09-20 13:59:22
【问题描述】:

我正在构建一个 WPF(桌面)应用程序,使用依赖注入、一个 DI 容器和 Register-Resolve-Release pattern。我的应用程序还在启动期间从单独的程序集中加载插件,并且这些插件已在 DI 容器中注册。我在启动时在组合根中解析了我的整个对象图,但是,我在解析我的插件时遇到了一些问题,我想知道是否可以将 DI 容器注入工厂以解决未知问题类型?

我的所有插件:

  • 实现一个通用接口,IPlugin
  • 需要是瞬态的(因为多个实例可以同时存在)
  • 在初始化时依赖运行时值来配置它们
  • 可能会或可能不会通过其构造函数注入其他依赖项

我已经开始实现PluginFactory 来在我需要插件时对其进行初始化,因为Mark Seemann says“任何需要运行时值来构造特定依赖项的地方,抽象工厂是解决方案。”

但是,我不能只 new() 我工厂中的插件,因为我不知道它们的类型和依赖关系。

我想到了几个解决方案:

  1. 我可以将 DI 容器注入到PluginFactory 的构造函数中,在运行时解析插件并收工。但是,这违反了 RRR 模式。

  2. 我可以通过PluginFactory的构造函数注入IEnumerable<IPlugin> plugins。但是,这将违反临时要求,因为每个实例只有一个副本[*错误]。我可以通过在我的所有插件上实现ICloneable 并在工厂中克隆它们来解决这个问题,但是,这既难以纠正依赖关系,又会使所有插件混乱。

[*Wrong] 编辑:注意,根据选择的答案,这是错误的!因此,选项 #2 是最佳选择,只要您将插件的生命周期注册为瞬态即可。

  1. 我可以将插件类型而不是实例注入PluginFactory 并使用Activator.CreateInstance<T>() 创建实例。我什至可以为插件和pass parameters to the Activator 定义一个通用的构造函数签名,以使用依赖项来初始化它们。但是,这违反了我对插件具有不同依赖项的要求,并且还导致了constrained construction anti-pattern

  2. 最后,在 3 的基础上进行构建。我可以注入插件类型并使用反射来整理插件的依赖关系。然而,这听起来像是很多工作,让我想知道我是否正在尝试从 DI 容器构建或复制解析方法。后者是我绝对不想做或不想看到的重点。

所以,在我的选择中,我最喜欢#1,即使这违背了 RRR 模式。

实例化我的插件的最佳方式是什么(我倾向于#1)。我是否完全错过了一些替代方案?还是我可能误判了我的一些其他选择?

【问题讨论】:

  • “但是,这将违反临时要求,因为每个实例只有一个副本。”。这完全取决于您使用的 DI 库。在 Simple Injector IEnumerable<T> 中,依赖项真正作为流提供,这意味着每次迭代可枚举时都会要求容器解析该实例。这样可以保留插件的生活方式。
  • 这很有趣。我这里假设太多了,容器显然比我聪明。您能否添加此评论作为答案?我觉得评论比您提供的其他答案更好地回答了我的真正问题:)
  • 这取决于您选择的容器。您目前使用的是哪个库?
  • SimpleInjector :) 如果您提供一个简短的 sn-p 说明如何在注册后(使用 RegisterAll)设置各个插件的生命周期,那就太好了。

标签: c# plugins dependency-injection simple-injector di-containers


【解决方案1】:

既然您已经引用了 Mark Seemann,请查看他的 Composition Root 文章和他的 Service Locator role vs mechanics 文章。长话短说,可以将容器引用/注入到构成您的组合根的基础架构组件中。

因此解决方案是定义一个IPluginFactory 抽象并将其放置在应用程序的核心库中。在您的组合根中(注意:将 CR 视为一个层,而不是一个类),您可以为此抽象创建一个实现,在这里可以注入容器。

【讨论】:

    【解决方案2】:

    在 Simple Injector 中,注入的 IEnumerable<T> 实例表现为流。这意味着每次迭代可枚举时,都会再次向容器询问实例。 Simple Injector documentation 声明:

    注意: Simple Injector 保留返回的实例的生活方式 来自注入的IEnumerable<T> 实例。实际上你不应该 将注入的IEnumerable<IValidator<T>> 视为一个集合 实现——您应该将其视为实例的。简单的 Injector 将始终注入对同一流的引用( IEnumerable<T> 本身是一个单例)并且每次迭代 IEnumerable<T>,对于每个单独的组件,都会询问容器 根据该组件的生活方式解析实例。 不管 [消费组件] 注册为 单例,它包装的验证器每个都有自己特定的 生活方式。

    容器将根据其生活方式返回实例。如果实例注册为瞬态,则意味着每次迭代集合时都会获得一个新实例。从某种意义上说,Simple Injector 注入的枚举就像工厂一样。这是一个显示这种行为的小测试:

    [TestMethod]
    public void EnumerablesBehaveAsStreams()
    {
        // Arrange
        var container = new Container();
    
        container.Collection.Register<ILogger>(typeof(SqlLogger), typeof(FileLogger));
    
        IEnumerable<ILogger> loggers = container.GetAllInstances<ILogger>();
    
        // Act
        ILogger sqlLogger1 = loggers.First();
        ILogger sqlLogger2 = loggers.First();
    
        // Assert
        Assert.AreNotSame(sqlLogger1, sqlLogger2);
    }
    

    一些开发人员对此感到困惑,但我认为这是唯一正确的行为,因为IEnumerable&lt;T&gt; 是流的定义,Simple Injector 尊重这一点,因此为您提供了流。这样做有一些有趣的优势。例如,它允许将枚举注入到单例中,同时保留元素的生活方式。

    如果你想注册一个插件集合,可以这样做:

    List<Type> pluginTypes = LoadPluginTypes();
    
    container.Collection.Register<IPlugin>(pluginTypes);
    

    Collection.Register 将已注册类型的解析转发回容器,因此尊重注册每种类型的生活方式。在上面的示例中,我们没有单独注册这些类型。这意味着默认情况下假定所有元素都是瞬态的。但是您可以按如下方式覆盖它:

    List<Type> pluginTypes = LoadPluginTypes();
    
    container.Register(pluginTypes.First(), Lifestyle.Singleton);
    
    container.Collection.Register<IPlugin>(pluginTypes);
    

    现在集合中的第一个插件注册为单例,而其他插件仍然是临时的。如果您想以相同的生活方式注册他们,您可以执行以下操作:

    List<Type> pluginTypes = LoadPluginTypes();
    
    pluginTypes.ForEach(type => container.Register(type, Lifestyle.Singleton));
    
    container.Collection.Register<IPlugin>(pluginTypes);
    

    另一种选择是将Registration 实例的集合传递给Collection.Register 方法:

    List<Type> pluginTypes = LoadPluginTypes();
    
    container.Collection.Register(typeof(IPlugin),
        from type in pluginTypes
        select Lifestyle.Singleton.CreateRegistration(
            typeof(IPlugin), type, container));
    

    在这种情况下,与之前的注册没有实际区别,但是当您想做更高级的事情(例如在多个抽象上共享同一个注册实例)时,注册Registration 实例变得很有趣。

    但是流行为在您的工厂中可能非常有用,因为您可以简单地将IEnumerable&lt;IPlugin&gt; 注入您的工厂,并且生活方式将得以保留:

    public class PluginFactory : IPluginFactory
    {
        private readonly IEnumerable<IPlugin> plugins;
    
        public PluginFactory(IEnumerable<IPlugin> plugins)
        {
            this.plugins = plugins;
        }
    
        public IPlugin GetPluginByName(string name)
        {
            return this.plugins.Single(plugin => plugin.Name == name);
        }
    }
    

    这里唯一要注意的是,如果您有兴趣通过过滤集合来一次返回一个插件,那么迭代集合将产生许多插件实例,即使您可能只对一个感兴趣。在上面的示例中,Single 将导致创建所有插件,而您只返回一个。更有效的是First,因为当插件匹配条件时停止,这会阻止所有以后的插件被创建。

    Simple Injector 非常快,因此在这通常会成为问题之前,集合可能会变得非常大,但需要注意这一点。如果您的意图是过滤列表并始终选择一个,则另一种方法可能会更好。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-06-22
      • 2012-09-21
      • 2018-01-21
      • 2018-10-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多