创建单例以访问Unity容器还是通过应用程序传递它更好?

49

我正在尝试使用IoC框架,我选择使用Unity。其中一个我仍然不完全理解的问题是如何在应用程序中更深层次地解析对象。我猜想我还没有领悟清楚的灵光一现的时刻。

因此,我正在尝试使用类似于伪代码的以下内容:

void Workflow(IUnityContatiner contatiner, XPathNavigator someXml)
{
   testSuiteParser = container.Resolve<ITestSuiteParser>
   TestSuite testSuite = testSuiteParser.Parse(SomeXml) 
   // Do some mind blowing stuff here
}

所以 testSuiteParser.Parse 的作用是

TestSuite Parse(XPathNavigator someXml)
{
    TestStuite testSuite = ??? // I want to get this from my Unity Container
    List<XPathNavigator> aListOfNodes = DoSomeThingToGetNodes(someXml)

    foreach (XPathNavigator blah in aListOfNodes)
    {
        //EDIT I want to get this from my Unity Container
        TestCase testCase = new TestCase() 
        testSuite.TestCase.Add(testCase);
    } 
}

我能看到三种选择:

  1. 创建一个Singleton来存储我可以在任何地方访问的unity容器。我真的不喜欢这种方法。添加依赖项以使用依赖注入框架似乎有点奇怪。
  2. 将IUnityContainer传递给我的TestSuiteParser类和它的所有子类(假设它深度为n级,实际上大约为3级)。在各个地方传递IUnityContainer看起来很奇怪。也许我只是需要克服这个。
  3. 想出正确使用Unity的方法。希望有人可以帮我打开这个开关。

[编辑] 其中一个我不清楚的事情是,我希望为foreach语句的每次迭代创建一个新的测试用例实例。上面的示例需要解析测试套件配置并填充测试用例对象集合。


6
这就像是问,是死在火灾中还是淹死更好。 - Krzysztof Kozmic
4个回答

51
DI的正确方法是使用构造函数注入或另一种DI模式(但构造函数注入最常见),将依赖项注入到消费者中,而与DI容器无关。
在您的示例中,看起来您需要依赖项TestSuite和TestCase,因此您的TestSuiteParser类应该通过其(唯一的)构造函数静态声明需要这些依赖项,并要求它们:
public class TestSuiteParser
{
    private readonly TestSuite testSuite;
    private readonly TestCase testCase;

    public TestSuiteParser(TestSuite testSuite, TestCase testCase)
    {
        if(testSuite == null)
        {
            throw new ArgumentNullException(testSuite);
        }
        if(testCase == null)
        {
            throw new ArgumentNullException(testCase);
        }

        this.testSuite = testSuite;
        this.testCase = testCase;
    }

    // ...
}

注意如何使用readonly关键字和Guard Clause来保护类的不变量,确保依赖项将可用于任何成功创建的TestSuiteParser实例。
现在,您可以像这样实现Parse方法:
public TestSuite Parse(XPathNavigator someXml) 
{ 
    List<XPathNavigator> aListOfNodes = DoSomeThingToGetNodes(someXml) 

    foreach (XPathNavigator blah in aListOfNodes) 
    { 
        this.testSuite.TestCase.Add(this.testCase); 
    }  
} 

然而,我怀疑可能涉及多个测试用例,如果是这样,您可能需要使用抽象工厂来注入,而不是单个测试用例。

从您的组合根中,您可以配置Unity(或任何其他容器):

container.RegisterType<TestSuite, ConcreteTestSuite>();
container.RegisterType<TestCase, ConcreteTestCase>();
container.RegisterType<TestSuiteParser>();

var parser = container.Resolve<TestSuiteParser>();

当容器解析TestSuiteParser时,它理解构造函数注入模式,因此会自动连接所需的所有依赖项与实例。创建单例容器或传递容器只是服务定位器反模式的两种变体,因此我不建议这样做。

2
所以你的意思是说你不应该传递容器,也不应该将其设置为静态。那我就没有其他选择了。在许多情况下,我不能简单地注入依赖项,因为在用户输入之后,我不知道需要做什么。例如:我有一些按钮可以打开不同类型的窗口。当用户按下按钮A时,我希望能够说Container.Resolve<BaseWindows>("StringTiedToButtonA")。那么我应该将其作为某种服务进行注入吗? - Ingó Vals
3
我的意思是,你既不应该传递容器,也不应该使其静态化。当您需要根据运行时值解析依赖项时,可以使用抽象工厂。请参见例如https://dev59.com/iHI-5IYBdhLWcg3wSGUB#1927167。 - Mark Seemann
2
你能指出一些更好的地方来解释这个问题吗?比如展示容器之间的交互。在你的例子中,你仍然需要访问实例化工厂的容器。那么容器是怎么到达那里的呢? - Ingó Vals
1
@Ingó:具体工厂可以尽可能地简单,其意义在于提供一个后期创建的实例,该实例需要最近才知道的信息才能实例化。 - Johann Gerell
3
我认为这个回答仍然不能回答原来的问题。在 Web 应用程序或请求模型中,不需要传递容器,因为你可以将容器的一个实例(你将使用的唯一实例)塞入应用程序级别的字典,如 HttpApplicationState 中。但是,你的桌面应用程序中最靠近 UI 需要组合的顶级类(top-level classes)如何获取对容器的引用? - Water Cooler v2
显示剩余4条评论

13

我刚开始接触依赖注入,也有同样的问题。我一直在思考将DI应用于正在开发中的类,并尝试在构造函数中添加依赖项后,立即寻找方法将Unity容器带到需要实例化该类的地方,以便我可以在该类上调用 Resolve 方法。结果是,我一直在考虑将Unity容器作为全局可用的静态变量或将其包装在单例类中。

我阅读了这里的答案,但并没有真正理解所解释的内容。最终,对我有所帮助的是这篇文章:

http://www.devtrends.co.uk/blog/how-not-to-do-dependency-injection-the-static-or-singleton-container

特别是这段话是我“恍然大悟”的时刻:

"99%的代码库不应该了解你的IoC容器,只有根类或启动程序使用容器,即使在这种情况下,通常只需要一个resolve调用来构建您的依赖关系图并启动应用程序或请求。"

这篇文章帮助我理解,实际上我不能在整个应用程序中都访问Unity容器,而只能在应用程序的根处访问它。因此,我必须将DI原则重复应用到应用程序的根类。

希望这可以帮助其他跟我一样困惑的人! :)


4

在您的应用程序中,您实际上不需要直接在很多地方使用容器。您应该在构造函数中获取所有依赖项,并且不要从方法中访问它们。您的示例可能如下所示:

public class TestSuiteParser : ITestSuiteParser {
    private TestSuite testSuite;

    public TestSuitParser(TestSuit testSuite) {
        this.testSuite = testSuite;
    }

    TestSuite Parse(XPathNavigator someXml)
    {
        List<XPathNavigator> aListOfNodes = DoSomeThingToGetNodes(someXml)

        foreach (XPathNavigator blah in aListOfNodes)
        {
            //I don't understand what you are trying to do here?
            TestCase testCase = ??? // I want to get this from my Unity Container
            testSuite.TestCase.Add(testCase);
        } 
    }
}

然后您需要在整个应用程序中以相同的方式执行此操作。当然,在某个时候,您必须解决一些问题。例如,在 asp.net mvc 中,这个地方就是控制器工厂。这是创建控制器的工厂。在这个工厂中,您将使用容器来解决控制器的参数。但这只是整个应用程序中的一个地方(可能有更多高级功能时会有更多地方)。
还有一个很好的项目叫做CommonServiceLocator。这是一个为所有流行的 ioc 容器提供共享接口的项目,这样您就不会依赖于特定的容器。

谢谢回复。我已经更新了我的问题。这样对你清楚明白了吗? - btlog
1
@btlog Mark Seemann已经在他的回答中包含了这个。一个常见的解决方案是在TestSuiteParser类的构造函数中注入一个测试用例工厂。 - Mattias Jakobsson

0
如果有一个“服务定位器”,可以在服务构造函数之间传递,但又能够“声明”注入类的预期依赖关系(即不隐藏依赖关系)……这样,所有对服务定位器模式的反对意见都可以得到解决。
public class MyBusinessClass
{
    public MyBusinessClass(IServiceResolver<Dependency1, Dependency2, Dependency3> locator)
    {
        //keep the resolver for later use
    }
}

不幸的是,上述内容显然只会存在于我的梦中,因为C#禁止使用变量泛型参数(仍然如此),因此每次需要额外的泛型参数时手动添加新的泛型接口将会很笨拙。

另一方面,如果可以通过以下方式实现上述内容,尽管C#有限制...

public class MyBusinessClass
{
    public MyBusinessClass(IServiceResolver<TArg<Dependency1, TArg<Dependency2, TArg<Dependency3>>> locator)
    {
        //keep the resolver for later use
    }
}

这样,只需要多打一些字就能实现同样的事情。 我还不确定的是,如果TArg类的设计得当(我假设巧妙地使用继承来允许无限嵌套的TArg泛型参数),DI容器是否能够正确解析IServiceResolver。最终的想法是,无论注入到哪个类的构造函数中找到的泛型声明是什么,都可以简单地传递相同的IServiceResolver实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接