资源的一般规则是:
拥有资源的人负责处理它。
这意味着如果一个类拥有一个资源,它应该在创建该资源的同一方法中处理掉它(在这种情况下,可处理的称为
短暂可处理),或者如果不可能,在通常情况下,拥有资源的类必须实现
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中处理这些对象。
考虑到您问题中给出的对象图,一个简单的代码示例,演示解析(即组合)和释放(即处理)可能如下:
var session = new Session();
var controller =
new Controller(
new Manager(
new Repository(
session)));
controller.TellYoMamaJoke();
session.Dispose();
当然,这个例子忽略了一些复杂因素,比如实现确定性清理、与应用程序框架集成以及使用 DI 容器,但希望这段代码可以帮助形成一个心理模型。
请注意,这种设计更改使您的代码更简单。在抽象上实现 IDisposable
并让消费者处理它们的依赖关系将会像病毒一样在系统中传播和污染您的代码库。它会扩散,因为对于任何抽象,您总是可以想到需要清理其资源的实现,因此您将不得不在每个抽象上实现 IDisposable
。这意味着每个需要一个或多个依赖项的实现也必须实现 IDisposable
,并且这会向对象图级联。这为您的系统中的每个类添加了大量的代码和不必要的复杂性。
StreamReader
的设计,但事实并非如此。我自己曾多次因StreamReader
的隐式行为而被绊倒,希望微软不要使用那种设计。但是,当设计可重用API的API时,您正在尝试使“简单用例易于使用,复杂用例可能”。有了这个指导,当手头的API是经常使用的部分并用于“简单用例”时,您可能希望使初学者轻松掉入“成功的坑中”。这可能会证明这种设计是合理的。 - StevenStreamReader
使用的隐式模型,但在某些情况下,隐式所有权可能是有意义的。但是,我很难为您提供一个对我来说有意义的例子。 - Steven