依赖注入和IDisposable

6 浏览
0 Comments

依赖注入和IDisposable

我对在使用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会自动知道在我的调用堆栈中哪些对象需要被正确地释放?Controller对象已经是IDisposable,因为它派生自基本的ASP.NET Web API控制器。

0
0 Comments

依赖注入和IDisposable存在的原因是为了解决资源的所有权和释放问题。一般情况下,拥有资源的类负责释放资源。如果一个类拥有资源,它应该在创建资源的同一个方法中释放资源,或者如果不可能的话,通常意味着拥有资源的类必须实现IDisposable接口,以便在Dispose方法中释放资源。

然而,需要注意的是,一般情况下,只有当一个类负责创建资源时,它才应该拥有该资源。当一个资源被注入时,意味着这个资源在消费者之前已经存在了。消费者没有创建该资源,因此不应该释放它。虽然你可以将资源的所有权转移给消费者(并在类的文档中说明已经转移了所有权),但通常不应该转移所有权,因为这会使你的代码变得复杂,并使应用程序变得脆弱。

虽然在某些情况下,传递对象的所有权可能是有意义的,例如用于可重用API的类型(如System.IO.StreamReader),但在处理对象图中的组件时,这不是一个好主意。下面我将解释为什么。

所以,即使你的Controller依赖于需要被释放的服务,你的Controller也不应该释放它们:

- 因为消费者没有创建这样的依赖关系,他对其依赖关系的预期生命周期一无所知。依赖关系的生命周期可能会比消费者更长。让消费者释放该依赖关系,在这种情况下会导致应用程序中的错误,因为下一个Controller将得到一个已经被释放的依赖关系,这将导致抛出ObjectDisposedException异常。

- 即使依赖关系的生命周期与消费者相同,释放它仍然存在问题,因为这会阻止你轻松地替换为将来可能具有更长生命周期的组件。一旦你用一个生命周期更长的组件替换了该组件,你就必须遍历所有的消费者,可能导致应用程序中的大量变化。换句话说,你将违反开闭原则——应该可以添加或替换功能而不进行大规模的更改。

- 如果消费者能够释放它们的依赖关系,这意味着你必须在它们的抽象上实现IDisposable接口。这意味着这种抽象泄漏了实现细节,这违反了依赖倒置原则。当在抽象上实现IDisposable时,你泄漏了实现细节,因为很少有抽象的每个实现都需要确定性的释放,所以你使用特定的实现定义了这个抽象。消费者不应该知道任何关于实现的东西,无论它是否需要确定性的释放。

- 让抽象实现IDisposable也会导致你违反接口隔离原则,因为抽象现在包含了一个额外的方法(即Dispose),并非所有消费者都需要调用该方法。他们可能不需要调用它,因为-如我之前提到的-资源可能会比消费者的生命周期长。在这种情况下,让它实现IDisposable是危险的,因为任何人都可以调用Dispose方法,导致应用程序崩溃。如果你对测试更严格,这也意味着你将不得不为不调用Dispose方法的消费者编写额外的测试代码。这将导致额外的测试代码,这些代码需要编写和维护。

相反,你应该只在实现上放置IDisposable。这样,抽象的任何消费者都不需要犹豫是否应该调用Dispose(因为抽象上没有Dispose方法可供调用)。

因为只有组件实现了IDisposable,而且只有你的组合根创建组件,所以组合根负责释放资源。如果你的DI容器创建这个资源,它也应该释放它。像Autofac这样的DI容器实际上会为你做到这一点。你可以很容易地验证这一点。如果你在没有使用DI容器的情况下连接对象图(即Pure DI),这意味着你必须在组合根自己中释放这些对象。

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

// 创建可释放的组件并持有它的引用
var session = new Session();
// 创建包括可释放组件在内的完整对象图
var controller =
    new Controller(
        new Manager(
            new Repository(
                session)));
// 使用对象图
controller.TellYoMamaJoke();
// 清理资源
session.Dispose();

当然,这个示例忽略了一些复杂因素,比如实现确定性清理、与应用程序框架集成以及使用DI容器。但希望这段代码能帮助你建立一个心理模型。

需要注意的是,这种设计变化使你的代码更简单。在抽象上实现IDisposable并让消费者释放它们的依赖关系会导致IDisposable像病毒一样在系统中扩散,并污染你的代码库。它扩散,因为对于任何抽象,你总是可以想到一个需要清理资源的实现,所以你必须在每个抽象上实现IDisposable。这意味着每个需要一个或多个依赖关系的实现也必须实现IDisposable,并且这种级联影响整个对象图。这给你的系统中的每个类增加了大量的代码和不必要的复杂性。

依赖注入和IDisposable的出现是为了解决资源所有权和释放的问题。通过将IDisposable仅放置在具体实现上,而不是抽象上,可以简化代码并避免违反设计原则。使用DI容器可以自动处理资源的释放,而不需要手动管理。这种设计变化可以使代码更清晰、更可维护,并提高系统的可扩展性。

0