依赖注入和 IDisposable
Dependency Injection and IDisposable
我对使用 Autofac 的 IDisposable
实现中的 Dispose()
方法有点困惑
说我对我的对象有一定的深度:
Controller
取决于 IManager
;
Manager
取决于 IRepository
;
Repository
取决于 ISession
;
ISession
是 IDisposable
.
这导致以下对象图:
new Controller(
new Manager(
new Repository(
new Session())));
我是否需要让我的 Manager 和 Repository 也实现 IDisposable 并在 Controller 中调用 Manager.Dispose(),在 Manager 中调用 Repository.Dispose() 等,或者 Autofac 会自动知道哪些对象在我的调用堆栈需要正确处理吗? Controller 对象已经是 IDisposable,因为它派生自 base ASP.NET Web API controller
资源的一般规则是:
He who owns the resource is responsible of disposing of it.
这意味着如果一个 class 拥有一个资源,它应该使用创建它的相同方法来处理它(在这种情况下,一次性被称为 ephemeral disposable),或者如果这不可能,这通常意味着拥有 class 必须实现 IDisposable
,因此它可以在其 Dispose
方法中处理资源。
但重要的是要注意,一般来说,class 只应 拥有 资源,前提是它负责 创建。但是当一个资源被注入时,这意味着这个资源在消费者之前就已经存在了。消费者没有创建资源,在这种情况下应该 而不是 处理它。虽然您可以将资源的所有权传递给消费者(并在 class 的文档中说明所有权已传递),但通常您不应该传递所有权,因为这会使您的代码复杂化并且使应用程序非常脆弱。
虽然转移对象所有权的策略在某些情况下可能有意义,例如对于属于可重用 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 creates this implementation, it is the Composition Root that is responsible of its disposal. In case your DI container creates this resource, it should also dispose it. DI Containers like Autofac will actually do this for you. You can easily verify this. In case you wire your object graphs without the use of a DI Container (a.k.a. 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
,并且这会向上级联对象图。这会为您系统中的每个 class 添加大量代码和不必要的复杂性。
我对使用 Autofac 的 IDisposable
实现中的 Dispose()
方法有点困惑
说我对我的对象有一定的深度:
Controller
取决于IManager
;Manager
取决于IRepository
;Repository
取决于ISession
;ISession
是IDisposable
.
这导致以下对象图:
new Controller(
new Manager(
new Repository(
new Session())));
我是否需要让我的 Manager 和 Repository 也实现 IDisposable 并在 Controller 中调用 Manager.Dispose(),在 Manager 中调用 Repository.Dispose() 等,或者 Autofac 会自动知道哪些对象在我的调用堆栈需要正确处理吗? Controller 对象已经是 IDisposable,因为它派生自 base ASP.NET Web API controller
资源的一般规则是:
He who owns the resource is responsible of disposing of it.
这意味着如果一个 class 拥有一个资源,它应该使用创建它的相同方法来处理它(在这种情况下,一次性被称为 ephemeral disposable),或者如果这不可能,这通常意味着拥有 class 必须实现 IDisposable
,因此它可以在其 Dispose
方法中处理资源。
但重要的是要注意,一般来说,class 只应 拥有 资源,前提是它负责 创建。但是当一个资源被注入时,这意味着这个资源在消费者之前就已经存在了。消费者没有创建资源,在这种情况下应该 而不是 处理它。虽然您可以将资源的所有权传递给消费者(并在 class 的文档中说明所有权已传递),但通常您不应该传递所有权,因为这会使您的代码复杂化并且使应用程序非常脆弱。
虽然转移对象所有权的策略在某些情况下可能有意义,例如对于属于可重用 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 creates this implementation, it is the Composition Root that is responsible of its disposal. In case your DI container creates this resource, it should also dispose it. DI Containers like Autofac will actually do this for you. You can easily verify this. In case you wire your object graphs without the use of a DI Container (a.k.a. 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
,并且这会向上级联对象图。这会为您系统中的每个 class 添加大量代码和不必要的复杂性。