依赖注入容器 - 如何保持可用性

5
当使用依赖注入创建应用程序并且它利用诸如Unity(或Ninject)之类的依赖注入框架时,如何在开始时一起初始化将接口注册到容器中,并使它们在整个应用程序的运行生命周期内保持可用?您是否需要将DI容器传递给每个可能使用依赖注入的方法,还是有一些方法可以使容器全局可访问,以便您可以在开始时一起注册它们,并在运行应用程序时无需不断传递它们,并能够在需要时使用它们?
环境:Visual Studio 2015,C#,Microsoft Unity(用于DI容器)
示例代码
    static void Main(string[] args)
    {

        // Make Unity resolve the interface, providing an instance
        // of TrivialPursuit class
        var diContainer = new UnityContainer();
        diContainer.RegisterType<IGame, TrivialPursuit>();

        var gameInstance = diContainer.Resolve<IGame>();


        var xotherClass = new AnotherClass();
        xotherClass.TestOtherClassOtherMethod();

    }

------ 没有依赖注入类上下文的另一个类 ------

    public void TestOtherClassOtherMethod()
    {
        IGame gameInstance = -- -Container is Not available to resolve from in this class ---
    }

原因:我不想在加载每个类时都需要传递可能需要的每种类型,我只想在需要它们时使用实例。随着应用程序变得更加复杂,我深入类层次结构的程度越深,我不想将每种类型的实例从Main()方法向下传递到每个类中。


2
你正在开发什么类型的应用程序?是Web应用还是桌面应用? - Darin Dimitrov
先使用桌面(控制台应用程序)进行测试,然后在掌握概念后使用Web(MVC)。 - CodingRiot
1个回答

23

一个依赖注入(DI)容器就是这样的。它是一个框架用于促进DI。您不需要传递容器来解决对象的实例,只需在类的构造函数中请求所需的类型,DI框架将注入适当的依赖项。

Mark Seemann写了一本关于依赖注入的好书,我会推荐它:《.NET中的依赖注入》

您需要在组合根中注册需要解决的所有内容。也就是说,当您的程序启动时,应该注册所有内容。

假设我们有以下代码:

public class MyClass
{
    public Run()
    {
        var dependency = new Dependency1();
        dependency.DoSomething();
    }
}

public class Dependency1
{
    public void DoSomething()
    {
        var dependency = new Dependency2();
        dependeny.DoSomethingElse();
    }
}

public class Dependency2
{
    public void DoSomethingElse()
    {
    }
}

这给我们带来了上述的依赖链: MyClass -> Dependency1 -> Dependency2。

我们首先需要重构类,通过它们的构造函数获取它们的依赖关系,并依赖于接口而不是具体实现。除非有注入点(构造函数、属性等),否则无法注入依赖项。

以下是重构后的代码:

public interface IMyClass
{
    void Run();
}

public interface IDependency1
{
    void DoSomething();
}

public interface IDependency2
{
    void DoSomethingElse();
}

public class MyClass : IMyClass
{
    public readonly IDependency1 dep;

    public MyClass(IDependency1 dep)
    {
        this.dep = dep;
    }

    public void Run()
    {
        this.dep.DoSomething();
    }
}

public class Dependency1 : IDependency1
{
    public readonly IDependency2 dep;

    public MyClass(IDependency2 dep)
    {
        this.dep = dep;
    }

    public void DoSomething()
    {
        this.dep.DoSomethingElse();
    }
}

public class Dependency2 : IDependency2
{
    public void DoSomethingElse()
    {
    }
}

你会注意到现在所有的类都通过它们的构造函数来获取它们的依赖项,而不需要新创建任何东西。类只应该接受它们实际需要的依赖项。例如,MyClass 不需要一个 Dependency2,因此它不会请求一个。它只请求一个 Dependency1,因为那是它所需的全部。Dependency1 需要 Dependency2,而不是 MyClass。

现在,为了将它们全部连接起来,我们可以在组合根中直接创建它们:

void Main()
{
    var myClass = new MyClass(new Dependency1(new Dependency2()));
}

如果我们有很多类和依赖关系,你可以看到会变得非常复杂。这就是为什么我们使用容器的原因。它为我们处理了所有依赖图。使用容器后,我们将重写如下:

void Main()
{
    // the order of our registration does not matter.
    var container = new Container();
    container.Register<IDependency1>.For<Dependency1>();
    container.Register<IDependency2>.For<Dependency2>();
    container.Register<IMyClass>.For<MyClass>();

    // then we request our first object like in the first example (MyClass);
    var myClass = container.Resolve<IMyClass>();

    myClass.Run();
}
在第二个示例中,容器将处理所有依赖项的连接。因此,我们不需要将Depedency2传递给MyClass,然后再传递给Depedency1。我们只需要在Dependency1中请求它,容器会像第一个示例中那样为我们连接它。
因此,在您的示例中,我们应该这样重写:
static void Main(string[] args)
{
    var game = new UnityContainer();
    game.RegisterType<IGame, TrivialPursuit>();
    game.RegisterType<IAnotherClass, AnotherClass>();
    game.RegisterType<IYetAnotherClass, YetAnotherClass>();

    var gameInstance = game.Resolve<IGame>();
    // you'll need to perform some action on gameInstance now, like gameInstance.RunGame() or whatever.
}

public class Game : IGame
{
    public Game(IAnotherClass anotherClass)
    {
    }
}    

public class AnotherClass : IAnotherClass
{
    public AnotherClass(IYetAnotherClass yetAnotherClass)
    {
    }
}

public class YetAnotherClass : IYetAnotherClass {}
在这些情况下,无需在容器之间传递。你可以将依赖项注册到容器中,然后在类构造函数中请求它们。如果你想在类中使用容器而不是通过构造函数请求它,则你并没有进行依赖注入,你只是将容器用作单例服务定位器。这通常应该避免。
容器作为服务定位器 这通常应该避免,但如果你想将容器用作服务定位器,你有两个选择:
1)通过构造函数将容器传递到需要它的类中。 您可以使用上面的示例来连接DI所需的类。但是,在构造函数中不是像IDependency一样请求一个依赖项,而是传递容器。
public class Game : IGame
{
    public Game(IContainer container)
    {
        var blah = container.Resolve<IBlah>();
    }
}

2)通过一个静态类请求您的容器:

public static class ServiceLocator
{
    private static IContainer container;
    public static IContainer Container
    {
        get 
        {
            if (container == null)
            {
                container = new Container();
            }

            return container;
        }
    }
}

在您的组合根中使用ServiceLocator类将所有内容注册为正常内容。然后要使用:

public class MyClass
{
    public void DoSomething()
    {
        var blah = ServiceLocator.Container.Resolve<IBlah>();
    }
}

1
我认为你没有理解问题的重点。问题不是“什么是DI?”,而是“如何保持容器可用于未来操作?”例如,以MVC应用程序的上下文为例,人们很可能需要了解MVC依赖项解析器。这就是为什么Darin上面的评论很重要的原因。 - DavidG
1
除了Main()方法之外,您如何使用该实例,因为它的范围仅限于仅在Main()方法中使用,难道您不需要传递该容器或某些引用以便能够在其他地方使用它吗?(抱歉,我还在学习依赖注入,并希望在尝试充分利用它之前确保我理解它)。非常感谢大家的帮助。 - CodingRiot
1
我的观点是您不需要在应用程序的其他部分了解容器。只有组合根,其中依赖项解析器将是其中一部分。您不会在其他地方请求对容器的引用,而只会要求实际需要的依赖项。如果它们被注册,容器将提供给您。 - Rabid Penguin
1
我可能在错误地使用它,你能帮我理解一下在注册到组合根后如何在单独的类中使用它吗?例如以下代码:` static void Main(string[] args) { var game = new UnityContainer(); game.RegisterType<IGame, TrivialPursuit>(); var gameInstance = game.Resolve(); var xotherClass = new AnotherClass();}public AnotherClass() { public void TestOtherClassOtherMethod() { IGame gameInAnotherClass = new ---[容器不可用于解析]--; } } ` - CodingRiot
1
你为什么想要全局引用它?DI 的重点在于不要全局引用它。你需要显式地声明所有的依赖关系。你可以将容器用作全局服务定位器,但是为什么要这样做呢? - Rabid Penguin
显示剩余9条评论

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