尝试应用良好的依赖注入实践时遇到的问题

时间:2011-09-26 13:17:54

标签: c# .net design-patterns dependency-injection

我已经在.NET中使用IoC(主要是Unity)和依赖注入一段时间了,我真的很喜欢这种模式,以鼓励创建具有松散耦合的软件类,并且应该更容易隔离以进行测试

我通常试图坚持的方法是"Nikola's Five Laws of IoC" - 特别是不注入容器本身并且只使用构造函数注入,这样您就可以从构造函数签名中清楚地看到类的所有依赖关系。 Nikola确实有一个帐户,但我不确定他是否仍然活跃。

无论如何,当我最终违反其他法律之一或者通常最终得到一些感觉不好或看起来不对的东西时,我不得不质疑我是否遗漏了某些东西,能否做得更好,或者干脆不应该在某些情况下不使用IoC。考虑到这一点,这里有几个例子,我将不胜感激任何指针或进一步讨论这些:

  1. 具有过多依赖项的类。 (“任何具有3个以上依赖关系的类都应该被质疑SRP违规”)。我知道这个问题在依赖注入问题中出现了很多,但在阅读完之后我仍然没有任何解决我问题的Eureka时刻:

    • a)在一个大型应用程序中,我总是发现我需要3个依赖项才能访问基础架构(示例 - 日志记录,配置,持久性),然后才能获得该类所需的特定依赖项(希望单一的责任) ) 任务完成。我知道将重构和包装这些依赖关系的方法整合到一个方法中,但我经常发现这只是几个其他服务的外观,而不是自己的任何真正的责任。是否可以在此规则的上下文中忽略某些基础结构依赖性,前提是该类仍被视为仍有一个责任?

    • b)重构可能会增加此问题。考虑拆分已经变得有点大的类的相当常见的任务 - 将一个功能区域移动到一个新类中,并且第一个类依赖于它。假设第一个类仍然需要它之前的所有依赖项,它现在有一个额外的依赖项。在这种情况下,我可能不介意这种依赖关系更加紧密,但它仍然更容易让容器提供它(与使用新的...()相反),即使没有新的依赖关系也可以做到这一点。它自己的界面。

    • c)在一个具体的例子中,我有一个班,负责每隔几分钟通过系统运行各种不同的功能。由于所有函数都属于不同的区域,因此该类最终会产生许多依赖关系,以便能够执行每个函数。我猜测在这种情况下应该考虑其他方法,可能涉及事件,但到目前为止我还没有尝试过,因为我想协调任务运行的顺序,在某些情况下应用逻辑涉及结果方式。

  2. 一旦我在一个应用程序中使用IoC,似乎我创建的几乎所有由另一个类使用的类最终都被容器注册和/或注入。这是预期的结果还是某些类与IoC无关?在代码中添加新东西的替代方案看起来就像代码气味,因为它紧密耦合。这也与上面的1b有关。

  3. 我在应用程序启动时完成了所有容器初始化,为系统中的每个接口注册了类型。有些是故意单实例生命周期,其他人可以在每次解析时成为新实例。但是,由于后者是前者的依赖关系,实际上它们也成为单个实例,因为它们仅在单个实例的构造时间被解析一次。在许多情况下这并不重要,但在某些情况下,每次我进行操作时我真的想要一个不同的实例,所以我不得不使用内置的容器功能,而是强迫我i)拥有相反,工厂依赖,所以我可以强制这种行为或ii)传递容器,这样我每次都可以解决。在Nikola的指导下,这两种方法都不受欢迎,但我认为i)是两种罪恶中较小的一种,在某些情况下我会使用它。

2 个答案:

答案 0 :(得分:3)

  

在一个大型应用程序中,我总是发现我需要3个依赖项才能访问基础架构(示例 - 日志记录,配置,持久性)

imho基础设施不依赖。使用servicelocator获取记录器(private ILogger _logger = LogManager.GetLogger())没有问题。

但是,在我看来,持久性不是基础设施。这是一种依赖。将你的班级分成更小的部分。

  

重构可能会增加此问题。

当然。在成功重构所有类之前,您将获得更多依赖项。只需挂在那里继续重构。

在单独的项目中创建接口(分离的接口模式),而不是向类添加依赖项。

  

在一个具体的例子中,我有一个班,负责每隔几分钟通过系统运行各种不同的功能。由于所有函数都属于不同的区域,因此该类最终会有许多依赖项,以便能够执行每个函数。

然后你采取了错误的方法。任务运行器不应该依赖于应该运行的所有任务,它应该是相反的方式。所有任务都应该在跑步者中注册。

  

一旦我在一个应用程序中使用IoC,似乎我创建的几乎所有由另一个类使用的类最终都被容器注册和/或注入。*

我在容器中注册了除业务对象,DTO等之外的所有内容。

  

我在应用程序启动时完成了所有容器初始化,为系统中的每个接口注册了类型。有些是故意单实例生命周期,其他人可以在每次解析时成为新实例。但是,由于后者是前者的依赖关系,实际上它们也成为单个实例,因为它们仅在单个实例的构造时间被解析一次。

如果可以避免,请不要混合使用寿命。或者不要接受短暂的依赖关系。在这种情况下,您可以使用简单的消息传递解决方案来更新单个实例。

您可能想要阅读我的guidelines

答案 1 :(得分:1)

让我回答问题3.让单身人士依赖瞬态是容器剖析器试图检测和警告的问题。 服务应仅依赖于其生命周期大于或等于其自身生命周期的其他服务。注入工厂接口或委托解决此问题通常是一个很好的解决方案,并且传入容器本身是一个糟糕的解决方案,因为你最终得到了Service Locator anti-pattern

您可以通过实施代理来解决此问题,而不是注入工厂。这是一个例子:

public interface ITransientDependency
{
    void SomeAction();
}

public class Implementation : ITransientDependency
{
    public SomeAction() { ... }
}

使用此定义,您可以根据ITransientDependencyComposition Root中定义代理类:

public class TransientDependencyProxy<T> : ITransientDependency
    where T : ITransientDependency
{
    private readonly UnityContainer container;

    public TransientDependencyProxy(UnityContainer container)
    {
        this.container = container;
    }

    public SomeAction()
    {
        this.container.Resolve<T>().SomeAction();
    }
}

现在您可以将此TransientDependencyProxy<T>注册为单身人士:

container.RegisterType<ITransientDependency,
    TransientDependencyProxy<Implementation>>(
        new ContainerControlledLifetimeManager());

虽然它被注册为单身,但它仍将充当瞬态,因为它会将其调用转发给瞬态实现。

通过这种方式,您可以完全隐藏ITransientDependency需要与应用程序的其他部分成为瞬态。

如果您需要针对许多不同服务类型的此行为,则为每个服务类型定义代理将变得很麻烦。在这种情况下,您可以尝试Unity的拦截功能。您可以定义一个拦截器,允许您为各种服务类型执行此操作。