使用依赖注入注入依赖注入器

时间:2009-12-12 00:50:38

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

依赖注入相当新,我试图弄清楚这是否是反模式。

假设我有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类在这个例子中的确切依赖性是什么,这让我认为这不是一个好习惯。

我还能怎样做到这一点?

有什么想法吗?

3 个答案:

答案 0 :(得分:7)

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

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

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

不要走那条路。

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

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

  • 在许多情况下,发布/订阅模型效果很好。
  • 中介模式可以提供替代,如果通信必须双向进行。
  • 您还可以引入抽象工厂来根据需要检索所需的实例,而不是要求立即连接。

答案 1 :(得分: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容器首先调用构造函数,然后在构造的对象上设置属性。通常,这用于可选的依赖项,例如记录器。但是你也可以用它来打破两个需要彼此的类之间的循环依赖。

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

答案 2 :(得分:1)

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