8

依赖注入很新,我试图弄清楚这是否是一种反模式。

假设我有 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.Payment.SomeType从 Foo.Users 内部实例化一个新对象,而无需直接引用:

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在这种情况下类的确切依赖关系是什么,这让我认为这不是一个好习惯。

我还能如何做到这一点?

有什么想法吗?

4

3 回答 3

7

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

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

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

不要走那条路。

从给出的信息中,不可能确切地告诉您应该如何解决循环依赖的特定问题,但我建议您重新考虑您的设计。在许多情况下,循环引用是Leaky Abstractions的症状,所以如果你稍微改造一下你的 API,它就会消失 - 需要多少小改动常常令人惊讶。

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

  • 在许多情况下,发布/订阅模型运行良好。
  • 如果通信必须双向进行,中介者模式可能会提供一种替代方案。
  • 您还可以引入一个抽象工厂来检索您需要的实例,而不是要求它立即连接起来。
于 2009-12-12T08:21:16.250 回答
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,但它确实支持属性注入 -这是文档

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

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

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

于 2009-12-12T03:25:21.133 回答
1

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

于 2009-12-12T02:26:06.847 回答