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

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.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个回答

7
尽管有争议,但是这是一种反模式。它被称为“服务定位器”,尽管有些人认为这是一种合适的设计模式,但我认为它是一种反模式。
问题在于,例如您的UserAccounts类的使用变得隐含而不是显式。虽然构造函数说明它需要一个IDependencyResolver,但它并没有说明应该放入什么内容。如果您传递一个无法解决ISomeType的IDependencyResolver,它将会抛出异常。
更糟糕的是,在后续迭代中,您可能会想从UserAccounts中解析出其他类型。虽然编译时不会出现错误,但在运行时,如果不能解析该类型,很可能会抛出异常。
不要走这条路。
根据给定的信息,无法准确告诉您如何解决循环依赖问题,但建议您重新考虑设计。在许多情况下,循环引用是“漏洞充满的抽象”的症状,因此,如果您稍微调整API,它就会消失——通常情况下,只需要进行少量的更改。
通常,解决任何问题的方法都是添加另一层间接性。如果确实需要两个库中的对象紧密协作,则通常可以引入中间代理。
- 在许多情况下,发布/订阅模型运作良好。 - 如果通讯必须双向进行,中介者模式可能提供一种替代方案。 - 您还可以引入抽象工厂,按需检索所需的实例,而不需要立即将其连接起来。

这就是我认为依赖关系不清晰,所以它必须是反模式的原因。 :) 抽象工厂是我的第一选择,但它使代码过于复杂化。在实际应用中,我需要创建许多不同的类型,而不仅仅是一个。我可以硬编码每个不同的工厂方法,或者使用泛型将接口与具体类(也是硬编码)相关联。但是这样会失去依赖注入框架的强大功能,并且配置绑定将变得非常混乱,甚至需要手动进行依赖注入/类型解析器代码,使用反射。 - andreialecu
总是有代价的 :) 我不同意配置容器会变得混乱 - 它可能会变得很冗长和啰嗦,但它将由许多几乎声明性的代码组成,这是一个很好的权衡。如果您最终需要配置许多类似的抽象工厂,则可以通过基于约定的配置解决大部分冗长问题 - 特别是如果所有抽象工厂都必须以相同的方式进行配置。Castle Windsor具有使您能够通过几个简单语句按约定配置的功能 - 我不知道NInject是否也可以做到这一点... - Mark Seemann

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,Foo.Users.UserAccounts 实现了该接口。
假设没有其他依赖项。
这里存在一个程序集循环依赖关系,它们将在应用程序运行时相互依赖(虽然在编译时它们通过共享DLL进行依赖)。但是,在编译时和运行时都不存在类之间的循环依赖。
在这种情况下,您仍应该能够正常使用IoC容器,而无需添加额外的间接层。在您的MyModule中,只需将每个接口绑定到相应的具体类型。让每个类将其依赖项作为构造函数的参数接受。当您的顶级应用程序代码需要一个类的实例时,让它向IoC容器请求该类。让IoC容器担心找到该类所依赖的所有内容。
如果您最终在类之间出现循环依赖关系,则可能需要在其中一个类上使用属性注入(也称为setter注入),而不是构造函数注入。我不使用Ninject,但它支持属性注入- 这里是文档
通常,IoC容器使用构造函数注入-它们将依赖项传递给依赖于它们的类的构造函数。但是,当存在循环依赖关系时,这种方法行不通。如果类A和B彼此依赖,则需要将类A的实例传递给类B的构造函数。但是,为了创建A,您需要将类B的实例传递到其构造函数中。这是一个鸡生蛋的问题。
使用属性注入时,您告诉IoC容器首先调用构造函数,然后在构造的对象上设置属性。通常这用于可选依赖项,例如日志记录器。但是,您也可以使用它来打破两个相互需要的类之间的循环依赖关系。
但这并不美观,我强烈建议重构您的类以消除循环依赖关系。

1

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


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