【问题标题】:Dependency Injection and IDisposable依赖注入和 IDisposable
【发布时间】:2015-05-17 05:05:32
【问题描述】:

我对使用 Autofac 的 IDisposable 实现中的 Dispose() 方法有点困惑

假设我对我的对象有一定的深度:

  • Controller 依赖于IManager
  • Manager 依赖于IRepository
  • Repository 依赖于ISession
  • ISessionIDisposable

这导致以下对象图:

new Controller(
    new Manager(
        new Repository(
            new Session())));

我是否需要让我的 Manager 和 Repository 也实现 IDisposable 并在 Controller 中调用 Manager.Dispose()、在 Manager 中调用 Repository.Dispose() 等,或者 Autofac 会自动知道我的调用堆栈中的哪些对象需要正确执行被处置?控制器对象已经是 IDisposable,因为它派生自基本 ASP.NET Web API 控制器

【问题讨论】:

    标签: .net dependency-injection inversion-of-control autofac idisposable


    【解决方案1】:

    资源的一般规则是:

    拥有资源的人有责任处置它。

    这意味着如果一个类拥有一个资源,它应该以创建它的相同方法处理它(在这种情况下,一次性被称为ephemeral disposable),或者如果这是不可能的,这通常是意味着拥有类必须实现IDisposable,因此它可以在其Dispose 方法中处理资源。

    但重要的是要注意,一般来说,一个类应该拥有一个资源,如果它负责它的创建。但是当一个资源被注入时,这意味着这个资源在消费者之前就已经存在了。消费者没有创建资源,在这种情况下应该处理它。尽管您可以将资源的所有权传递给消费者(并在类的文档中传达所有权已传递),但通常您不应传递所有权,因为这会使您的代码复杂化并使应用程序非常脆弱。

    虽然转移对象所有权的策略在某些情况下可能有意义,例如对于属于可重用 API 一部分的类型(如 System.IO.StreamReader),但在处理属于对象的组件时总是不好图(我们所谓的injectables)。我将在下面解释原因。

    因此,即使您的 Controller 依赖于需要处理的依赖项,您的控制器也不应该处理它们:

    • 因为消费者没有创建这样的依赖,它不知道它的依赖的预期生命周期是什么。依赖关系应该比消费者更长寿。在这种情况下,让消费者处理该依赖项将导致您的应用程序出现错误,因为下一个控制器将获得已处理的依赖项,这将导致ObjectDisposedException 被抛出。
    • 即使依赖项的生活方式与消费者的生活方式相同,处置仍然不是一个好主意,因为这会阻止您轻松地将该组件替换为将来可能具有更长寿命的组件。一旦您将该组件替换为一个寿命更长的组件,您将不得不遍历它的所有消费者,这可能会导致整个应用程序发生彻底的变化。换句话说,您这样做将违反Open/closed principle——应该可以添加或替换功能而无需进行彻底的更改。
    • 如果您的消费者能够处理其依赖关系,这意味着您在该抽象上实现了IDisposable。这意味着这个抽象泄露了实现细节——这违反了Dependency Inversion Principle。在抽象上实现IDisposable 时,您正在泄漏实现细节,因为该抽象的每个实现 都不太可能需要确定性处置,因此您在定义抽象时考虑了某个实现。无论是否需要确定性处置,消费者都不必了解任何有关实施的信息。
    • 让该抽象实现IDisposable 也会导致您违反Interface Segregation Principle,因为抽象现在包含一个额外的方法(即Dispose),并非所有消费者都需要调用。他们可能不需要调用它,因为——正如我已经提到的——资源可能比消费者更长寿。在这种情况下让它实现IDisposable 是危险的,因为任何人都可以调用Dispose 导致应用程序崩溃。如果您对测试更严格,这也意味着您必须测试消费者是否不调用 Dispose 方法。这将导致额外的测试代码。这是需要编写和维护的代码。

    因此,您应该实现实现IDisposable。这让抽象的任何消费者都不必担心是否应该调用Dispose(因为没有Dispose 方法可以调用抽象)。

    因为只有实现实现了IDisposable,并且只有您的Composition Root 创建了这个实现,所以它是组合根负责处理它。如果你的 DI 容器创建了这个资源,它也应该释放它。 Autofac 之类的 DI 容器实际上会为您执行此操作。您可以轻松地验证这一点。如果您在不使用 DI 容器(又名Pure DI)的情况下连接对象图,则意味着您必须自己在合成根中处理这些对象。

    考虑到您的问题中给出的对象图,一个演示解析(即组合)和释放(即处置)的简单代码示例如下所示:

    // Create disposable component and hold reference to it
    var session = new Session();
    
    // create the complete object graph including the disposable
    var controller =
        new Controller(
            new Manager(
                new Repository(
                    session)));
    
    // use the object graph
    controller.TellYoMamaJoke();
    
    // Clean up resources
    session.Dispose();
    

    当然,这个例子忽略了一些复杂的因素,例如实现确定性清理、与应用程序框架的集成以及 DI 容器的使用,但希望这段代码有助于绘制一个心智模型。

    请注意,此设计更改使您的代码更简单。在抽象上实现IDisposable 并让消费者处理他们的依赖项将导致IDisposable 像病毒一样在您的系统中传播并污染您的代码库。它散布开来,因为对于任何抽象,您总是可以想到需要清理其资源的实现,因此您必须在每个抽象上实现IDisposable。这意味着每个需要一个或多个依赖项的实现也必须实现IDisposable,这会级联对象图。这会为系统中的每个类添加大量代码和不必要的复杂性。

    【讨论】:

    • 我应该澄清一下,我的意思是在注入时所有权转移的情况。我不知道 Autofac 是否如此。
    • 我同意这将是一个糟糕的设计。我不知道 Autofac 的设计是否不好。我应该说得更清楚。
    • 那样的话,我的回答没有什么价值,所以我把它删了。您可能需要编辑您的答案以适应这一点。
    • @Steven 感谢您的精彩解释!可以帮忙解答后续问题@stackoverflow.com/questions/30294166
    • @Igorek:很抱歉,您的新问题涉及 Autofac 的细节太深了。我帮不了你。我对 Autofac 不太熟悉。
    猜你喜欢
    • 2021-03-02
    • 1970-01-01
    • 2021-11-19
    • 1970-01-01
    • 1970-01-01
    • 2017-04-02
    • 2012-02-15
    • 2011-05-11
    • 2011-04-04
    相关资源
    最近更新 更多