【问题标题】:Injecting the Dependency Injector using Dependency Injection使用依赖注入注入依赖注入器
【发布时间】:2009-12-12 00:50:37
【问题描述】:

对依赖注入还很陌生,我正试图弄清楚这是否是一种反模式。

假设我有 3 个程序集:

Foo.Shared - this has all the interfaces
Foo.Users - references Foo.Shared
Foo.Payment - references Foo.Shared

Foo.Users 需要一个在 Foo.Payment 中构建的对象,而 Foo.Payment 也需要来自 Foo.Users 的东西。这会产生某种循环依赖。

我在 Foo.Shared 中定义了一个接口,它代理我正在使用的依赖注入框架(在本例中为 NInject)。

public interface IDependencyResolver
{
    T Get<T>();
}

在容器应用中,我有这个接口的实现:

public class DependencyResolver:IDependencyResolver
{
    private readonly IKernel _kernel;

    public DependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }

    public T Get<T>()
    {
        return _kernel.Get<T>();
    }
}

配置如下:

public class MyModule:StandardModule
{
    public override void Load()
    {
        Bind<IDependencyResolver>().To<DependencyResolver>().WithArgument("kernel", Kernel);
        Bind<Foo.Shared.ISomeType>().To<Foo.Payment.SomeType>(); // <- binding to different assembly
        ...
    }
}

这允许我从 Foo.Users 内部实例化 Foo.Payment.SomeType 的新对象,而无需直接引用:

public class UserAccounts:IUserAccounts
{
    private ISomeType _someType;
    public UserAccounts(IDependencyResolver dependencyResolver)
    {
        _someType = dependencyResolver.Get<ISomeType>(); // <- this essentially creates a new instance of Foo.Payment.SomeType
    }
}

这使得在这种情况下UserAccounts 类的确切依赖关系不清楚,这让我认为这不是一个好习惯。

我还能如何做到这一点?

有什么想法吗?

【问题讨论】:

    标签: c# design-patterns dependency-injection ninject


    【解决方案1】:

    虽然有些争议:是的,这是一种反模式。它被称为服务定位器,虽然有些人认为它是一种合适的设计模式,但我认为它是一种反模式。

    这个问题是使用例如您的 UserAccounts 类变为 implicit 而不是 explicit。虽然构造函数声明它需要一个 IDependencyResolver,但它没有说明它应该包含什么。如果你向它传递一个无法解析 ISomeType 的 IDependencyResolver,它会抛出异常。

    更糟糕的是,在以后的迭代中,您可能会试图从 UserAccounts 中解析一些其他类型。它会编译得很好,但如果/当类型无法解析时,可能会在运行时抛出。

    不要走那条路。

    根据给出的信息,不可能准确地告诉您应该如何解决循环依赖的特定问题,但我建议您重新考虑您的设计。在许多情况下,循环引用是 Leaky Abstractions 的症状,因此,如果您稍微改造您的 API,它可能会消失 - 需要进行如此小的更改常常令人惊讶。

    一般来说,任何问题的解决方案都是添加另一层间接性。如果您确实需要让两个库中的对象紧密协作,您通常可以引入一个中间代理。

    • 在许多情况下,发布/订阅模型效果很好。
    • 如果通信必须双向进行,Mediator 模式可能会提供另一种选择。
    • 您还可以引入一个抽象工厂来检索您需要的实例,而不是要求它立即连接起来。

    【讨论】:

    • 这就是我认为的依赖关系不清楚,所以它必须是一个反模式。 :) 抽象工厂是我的第一选择,但它使代码过于复杂。在实际应用程序中,我需要创建许多单独的类型,而不仅仅是一种。我要么对每个不同的工厂方法进行硬编码,要么使用泛型将接口与具体类相关联(也是硬编码的)。但是这样我就失去了依赖注入框架的力量,配置绑定会变得非常混乱,甚至需要手动依赖注入/使用反射的类型解析器代码。
    • 总要付出代价 :) 我不同意配置容器会变得混乱 - 它可能会变得非常冗长和冗长,但它会包含很多几乎是声明性的代码,这是一个很好的权衡。您可以通过基于约定的配置来解决大部分冗长的问题 - 特别是如果您最终得到许多必须以相同方式配置的类似抽象工厂。 Castle Windsor 的功能可以让您通过一些简单的语句按照惯例进行配置——我不知道 NInject 是否也能做到这一点......
    【解决方案2】:

    我同意 ForeverDebugging - 消除循环依赖会很好。看看你能不能像这样分开类:

    • Foo.Payment.dll:只处理支付,不处理用户的类
    • Foo.Users.dll:只处理用户,不处理支付的类
    • Foo.UserPayment.dll:处理支付和用户的类

    然后你有一个程序集引用了另外两个程序集,但没有依赖循环。

    如果程序集之间确实存在循环依赖关系,并不一定意味着类之间存在循环依赖关系。例如,假设您有以下依赖项:

    • Foo.Users.UserAccounts 依赖于 Foo.Shared.IPaymentHistory,由 Foo.Payment.PaymentHistory 实现。
    • 另一个支付类 Foo.Payment.PaymentGateway 依赖于 Foo.Shared.IUserAccounts。 IUserAccounts 由 Foo.Users.UserAccounts 实现。

    假设没有其他依赖项。

    这里有一个程序集循环,它们在应用程序的运行时相互依赖(尽管它们在编译时不相互依赖,因为它们通过共享的 DLL)。但是在编译时或运行时没有相互依赖的类。

    在这种情况下,您应该仍然能够正常使用您的 IoC 容器,而无需添加额外的间接级别。在您的 MyModule 中,只需将每个接口绑定到适当的具体类型。使每个类接受其依赖项作为构造函数的参数。当您的顶级应用程序代码需要一个类的实例时,让它向 IoC 容器请求该类。让 IoC 容器担心找到类所依赖的所有内容。



    如果你最终在类之间产生了循环依赖,你可能需要在其中一个类上使用属性注入(又名 setter 注入),而不是构造函数注入。我不使用 Ninject,但它确实支持属性注入 - here is the documentation

    通常 IoC 容器使用构造函数注入 - 它们将依赖项传递给依赖它们的类的构造函数。但是当存在循环依赖时,这不起作用。如果 A 类和 B 类相互依赖,则需要将 A 类的实例传递给 B 类的构造函数。但是为了创建 A,您需要将 B 类的实例传递给它的构造函数。这是一个先有鸡还是先有蛋的问题。

    通过属性注入,您告诉您的 IoC 容器首先调用构造函数,然后在构造的对象上设置一个属性。通常这用于可选依赖项,例如记录器。但是你也可以用它来打破两个相互需要的类之间的循环依赖。

    但这并不漂亮,我绝对建议重构你的类以消除循环依赖。

    【讨论】:

      【解决方案3】:

      这对我来说确实有点奇怪。是否可以将需要两个引用的逻辑分离到第三个程序集中以打破依赖关系并避免风险?

      【讨论】:

        猜你喜欢
        • 2011-04-08
        • 2014-06-12
        • 1970-01-01
        • 2013-04-30
        • 1970-01-01
        • 1970-01-01
        • 2020-12-20
        相关资源
        最近更新 更多