依赖注入和IDisposable

31

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

假设我对象有一定的层次结构:

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

这导致以下对象图:

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

我需要让我的管理器和仓储实现IDisposable接口,并在控制器中调用Manager.Dispose()、在Manager中调用Repository.Dispose()等,还是Autofac会自动知道在我的调用堆栈中哪些对象需要正确地进行清理?控制器对象已经实现了IDisposable接口,因为它派生自基本的ASP.NET Web API控制器。

1个回答

57
资源的一般规则是:
拥有资源的人负责处理它。
这意味着如果一个类拥有一个资源,它应该在创建该资源的同一方法中处理掉它(在这种情况下,可处理的称为 短暂可处理),或者如果不可能,在通常情况下,拥有资源的类必须实现 IDisposable,以便可以在其 Dispose 方法中处理掉资源。
但值得注意的是,通常情况下,只有当一个类负责创建资源时,它才应该拥有资源。然而,当注入资源时,这意味着此资源已经存在于使用者之前。使用者没有创建资源,因此不应该处理掉它。虽然您可以将资源的所有权转移给使用者(并在类的文档中传达所有权已转移),但通常不应该转移所有权,因为这会使您的代码变得复杂,并使应用程序变得脆弱。
尽管在某些情况下,将对象所有权转移的策略可能是有道理的,例如对于可重用API中的类型(如System.IO.StreamReader),但在处理对象图中的组件时,这不是一个好主意。我将在下面解释原因。
因此,即使您的Controller依赖需要处理的服务,您的控制器也不应该处理它们的销毁:
因为消费者没有创建这种依赖关系,所以它不知道其依赖项的预期寿命是多少。很可能依赖项的寿命应该超过消费者。在这种情况下,让消费者处理该依赖项将导致应用程序中的错误,因为下一个控制器将获取到已经被处置的依赖项,这将导致抛出一个{{ObjectDisposedException}}。
即使依赖项的生命周期与消费者相等,处理它仍然是有问题的,因为这会阻止您轻松地替换那个组件为未来可能具有更长寿命的组件。一旦您将该组件替换为更长寿命的组件,您将不得不遍历所有的使用者,可能会导致应用程序中的大规模更改。换句话说,这会违反开闭原则——应该可以添加或替换功能而不进行大规模更改。
如果您的消费者能够处理其依赖项,则意味着您必须在它们的抽象上实现{{IDisposable}}。这意味着这样的抽象正在泄漏实现细节——这是依赖倒置原则的一种违反。当在抽象上实现{{IDisposable}}时,您正在泄漏实现细节,因为很少有{{每个实现}}需要确定性处置,因此您定义了一个具有特定实现的抽象。消费者不应该知道任何关于实现的信息,无论它是否需要确定性处置。
让该抽象实现{{IDisposable}}也会导致您违反接口隔离原则,因为抽象现在包含一个额外的方法(即{{Dispose}}),并非所有消费者都需要调用它。他们可能不需要调用它,因为——如我所提到的——资源可能会超过消费者的寿命。在这种情况下,让它实现{{IDisposable}}是危险的,因为任何人都可以调用{{Dispose}},导致应用程序崩溃。如果您对测试更严格,这还意味着您将不得不测试消费者{{不调用}}{{Dispose}}方法。这将导致额外的测试代码。这是需要编写和维护的代码。
相反,你应该仅在实现中放置IDisposable。这样可以使抽象的任何使用者免于疑虑,无论是否应该调用Dispose(因为在抽象上没有可调用的Dispose方法)。
由于只有组件实现了IDisposable,并且只有您的Composition Root创建组件,因此是Composition Root负责其处理。如果您的DI容器创建此资源,则也应将其处理掉。像Autofac这样的DI容器实际上会为您执行此操作。您可以轻松验证这一点。如果您不使用DI容器(即Pure DI)连接对象图,则意味着您必须自己在Composition Root中处理这些对象。
考虑到您问题中给出的对象图,一个简单的代码示例,演示解析(即组合)和释放(即处理)可能如下:
// 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,并且这会向对象图级联。这为您的系统中的每个类添加了大量的代码和不必要的复杂性。


我同意这将是一个糟糕的设计。我不知道Autofac是否有糟糕的设计。我应该让这更清楚。 - John Saunders
1
在这种情况下,我的回答价值不大,所以我已经删除了它。你可能想编辑你的回答来适应这一点。 - John Saunders
1
可重用的库或API是由第三方使用的,而您作为编写者无法控制的东西。微软作为.NET框架的一部分发布的所有内容都是可重用的库。每个曾经创建的NuGet包都是可重用的库。这与单个解决方案中的库形成对比。那些在您的控制下,可以随意重构。可重用的库需要非常特殊的设计才能尽可能地可用。这与自己创建完全不同。这就是为什么与应用程序组件相比可能适用其他规则的原因。 - Steven
1
当我写下我的回答时,似乎我在赞成StreamReader的设计,但事实并非如此。我自己曾多次因StreamReader的隐式行为而被绊倒,希望微软不要使用那种设计。但是,当设计可重用API的API时,您正在尝试使“简单用例易于使用,复杂用例可能”。有了这个指导,当手头的API是经常使用的部分并用于“简单用例”时,您可能希望使初学者轻松掉入“成功的坑中”。这可能会证明这种设计是合理的。 - Steven
1
但是,作为API设计师,您还必须考虑更大的画面,如果您提供了两种管理所有权的方法,这在长期内可能会更加混乱,并且使初学者难以知道何时隐式转移所有权和何时不转移。我个人更喜欢使所有权的转移非常明确,而不是StreamReader使用的隐式模型,但在某些情况下,隐式所有权可能是有意义的。但是,我很难为您提供一个对我来说有意义的例子。 - Steven
显示剩余5条评论

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