【问题标题】:Problems faced when trying to apply good Dependency Injection practice尝试应用良好的依赖注入实践时面临的问题
【发布时间】:2011-09-26 13:17:53
【问题描述】:

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

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

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

  1. 依赖项太多的类。 (“任何具有超过 3 个依赖项的类都应因违反 SRP 而受到质疑”)。我知道这个问题在依赖注入问题中出现了很多,但是在阅读了这些之后,我仍然没有任何 Eureka 时刻可以解决我的问题:

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

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

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

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

  3. 我在应用程序启动时完成了所有容器初始化,为系统中的每个接口注册类型。有些是故意的单实例生命周期,而其他的每次解决时都可以是新实例。但是,由于后者是前者的依赖项,因此实际上它们也成为单个实例,因为它们只被解析一次 - 在单个实例的构建时。在许多情况下这无关紧要,但在某些情况下,我真的想要一个不同的实例,每次我做一个操作,所以我不能利用内置的容器功能,我不得不要么 i) 有而是使用工厂依赖项,以便我可以强制执行此行为,或者 ii)传入容器,以便每次都可以解决。这两种方法都在 Nikola 的指导下不受欢迎,但我认为 i) 是两害相权取其轻,我在某些情况下确实会使用它。

【问题讨论】:

  • 请尝试一次只问一个问题。
  • 拥有超过 3 个依赖项是很常见的。对我来说,超过 5 个依赖项就是代码异味。
  • 是的,我现在意识到我应该把它分开,但由于背景都是相关的,我在写作时有点被带走了。把它归结为缺乏经验。现在无法重构我的问题,因为我有一些答案......

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


【解决方案1】:

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

imho 基础设施不是依赖项。我使用服务定位器获取记录器没有问题 (private ILogger _logger = LogManager.GetLogger())。

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

重构会加重这个问题。

当然。在成功重构所有类之前,您将获得更多依赖项。坚持下去,继续重构。

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

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

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

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

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

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

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

您可能想阅读我的guidelines

【讨论】:

  • 感谢您的指导,它们很有用。仍在努力重构 - 如果我分解一个类,我经常发现所有新类仍然具有相同的依赖关系,所以我所做的只是向其他需要我正在分解的类中的函数的类添加更多依赖关系。我看不出在这种持续重构中的什么时候,我会开始看到每个类的依赖关系减少。至于任务运行器,我知道你是对的,但我需要弄清楚我如何保持对执行顺序的控制。也大概这意味着每个有任务要注册的模块都需要初始化?
  • 添加一个示例类,我会为您分解它,以便各部分使用较少的依赖项。
【解决方案2】:

让我回答问题 3。让单例依赖于瞬态是容器分析器试图检测和警告的问题。 服务应该只依赖于生命周期大于或等于它们自己生命周期的其他服务。注入工厂接口或委托来解决这个问题通常是一个很好的解决方案,并传入容器本身是一个糟糕的解决方案,因为您最终会得到Service Locator anti-pattern

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

public interface ITransientDependency
{
    void SomeAction();
}

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

使用这个定义,你可以在Composition Root中基于ITransientDependency定义一个代理类:

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&lt;T&gt; 注册为单例:

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

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

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

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

【讨论】:

  • 谢谢,我在其他地方看到过这个,但这是一个很好的解释它是如何工作的。
猜你喜欢
  • 2018-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-08
  • 1970-01-01
  • 2010-12-11
  • 1970-01-01
相关资源
最近更新 更多