8.3 生命方式目录
现在我们已经介绍了生命周期管理背后的原则,我们将花一些时间来看看常见的生命方式模式。正如我们在介绍中所描述的那样,Lifestyle是一种正式的方式,用来描述依赖关系的预期寿命。这给了我们一个共同的词汇,就像设计模式一样。它使我们更容易推理出一个依赖关系何时以及如何超出范围–以及它是否会被重用。
本节讨论了表8.1中描述的三种最常见的生命方式。 因为你已经遇到了Singleton和Transient,我们将从它们开始。
表 8.1 本节涉及的生命方式模式
名称 | 描述 |
---|---|
单例 singleton | 单个实例是永久重复使用的。 |
暂时的 Transient | 新的实例总是被送达。 |
范围Scoped | 每个隐式或显式定义的范围最多只能提供一种类型的实例。 |
注意: 我们在本节中使用了可比较的例子。但为了使我们能够专注于本质,我们将组成浅层的层次结构,而且我们有时会忽略可释放依赖关系的问题,以避免这种额外的复杂性。
范围型生名方式的使用很普遍;大多数外来的生名方式都是它的变种。与高级生名方式相比,Singleton生活方式可能看起来很平凡,但它仍然是一种常见的、合适的生命周期策略。
8.3.1 单例生命方式
在本书中,我们时常隐含地使用Singleton Lifestyle。这个名字既清晰又有点让人困惑。然而,它是有意义的,因为所产生的行为与Singleton设计模式相似,但结构不同。
注意: 在一个Composer的范围内,只有一个具有Singleton Lifestyle的组件的实例。每次消费者请求该组件时,都会有同一个实例。
在Singleton Lifestyle和Singleton设计模式中,依赖只有一个实例,但相似性到此为止。Singleton设计模式提供了一个访问其实例的全局点,这与我们在第5.3节讨论的环境上下文反模式相似。 然而,消费者不能通过静态成员来访问一个Singleton范围内的依赖关系。如果你要求两个不同的Composers为一个实例服务,你会得到两个不同的实例。因此,重要的是,你不要把Singleton Lifestyle和Singleton设计模式混淆。
因为只有一个实例在使用,Singleton Lifestyle通常消耗最少的内存,而且效率很高。唯一不是这样的情况是,当实例很少被使用但却消耗了大量的内存。在这种情况下,该实例可以被包裹在一个虚拟代理中,我们将在第8.4.2节中讨论。
什么时候使用Singleton生命方式
尽可能地使用Singleton生活方式。有两个主要的问题可能会阻止你使用Singleton,如下。
- 当一个组件不是线程安全的时候。因为Singleton实例有可能被许多消费者共享,它必须能够处理并发访问。
- 当组件的一个依赖的寿命预计会更短,可能是因为它不是线程安全的。给予组件一个Singleton Lifestyle将使它的依赖存活时间过长。在这种情况下,这样的依赖关系就成了一个俘虏性依赖关系。 我们将在第8.4.1节中更详细地介绍俘获性依赖。
根据定义,所有的无状态服务都是线程安全的,不可变类型也是如此,显然,专门设计为线程安全的类也是如此。在这些情况下,没有理由不把它们配置成Singletons。
除了对效率的争论外,有些依赖关系只有在共享的情况下才会按预期工作。例如,我们将在第9章讨论的Circuit Breaker设计模式的实现,以及内存缓存就是这种情况。在这些情况下,实现必须是线程安全的。
让我们仔细看看内存中的存储库。接下来我们将探讨一个例子。
示例:使用一个线程安全的内存仓库
让我们再一次把注意力转向实现CommerceControllerActivator,就像7.3.1和8.1.2节中的那些。不要使用基于SQL Server的IProductRepository,你可以使用一个线程安全的内存实现。为了使内存数据存储有意义,它必须在所有请求之间共享,所以它必须是线程安全的。这在图8.7中得到了说明。

你应该使用一个具体的类,并使用Singleton Lifestyle对其进行适当的范围划分,而不是使用Singleton 模式明确地实现这样一个仓库。 下一个列表显示了Composer如何在每次被要求解决HomeController时返回新的实例,而IProductRepository则在所有实例中共享。
Listing 8.10 管理一个单例生命方式
public class CommerceControllerActivator: IControllerActivator {
//在composer的生命周期内保持单例的依赖
private readonly IUserContext userContext;
private readonly IProductRepository repository;
public CommerceControllerActivator() {
//创建单例
this.userContext = new FakeUserContext();
this.repository = new InMemoryProductRepository();
}...
//每当composer要求解析一个HomeController实例时
//它就会创建一个transient ProductService,并将这两个sngletons注入其中。
private HomeController CreateHomeController() {
return new HomeController(new ProductService(this.repository, this.userContext));
}
}
注意,在这个例子中,repository和userContext都包含了Singleton Lifestyles。不过,如果你愿意,你可以混合使用 Lifestyles。图8.8显示了CommerceControllerActivator在运行时发生的情况

Singleton生命方式是最容易实现的生活方式之一。它所要求的是你保持一个对象的引用,并在每次被请求时提供相同的对象。实例不会超出范围,直到Composer超出范围。当这种情况发生时,如果该对象是一个可释放的类型,Composer应该将其释放。
另一种实施起来很简单的生活方式是 "暂时 Transient "生活方式。 让我们接下来看看这个。
8.3.2 暂时性(Transient)的生命方式
Transient Lifestyle涉及到每次被请求时返回一个新的实例。除非返回的实例实现了IDisposable,否则就没有什么可追踪的了。 相反,当实例实现了IDisposable时,Composer必须记住它,并在要求释放适用的对象图时明确地处置它。本书中大多数构造对象图的例子都隐含着使用Transient Lifestyle。
警告: 当涉及到Transient Lifestyle时,要注意DI容器的行为会有所不同。 虽然有些DI容器会跟踪Transient组件,并在它们的消费者超出范围时倾向于处置它们,但其他的DI容器不会,因此,Transient不会被处置。
值得注意的是,在桌面和类似的应用程序中,我们倾向于只解决整个对象的层次结构一次:在应用程序启动时。 这意味着,即使是Transient组件,也只能创建几个实例,而且它们可以存在很长时间。在每个依赖只有一个消费者的退化情况下,解析纯Transient组件图的最终结果等同于解析纯Singletons的图,或其任何混合。这是因为图只被解析一次,所以行为上的差异从未被意识到。
什么时候使用暂时性(Transient)的生命方式
暂时性的生命方式是最安全的选择,但也是效率最低的选择之一。它可以导致无数的实例被创建和垃圾回收,即使是一个实例就足够了。
然而,如果你对一个组件的线程安全有疑问,暂时性(Transient)的生命方式是安全的,因为每个消费者都有自己的依赖实例。在许多情况下,你可以安全地将暂时性(Transient)的生命方式换成范围内的生活方式,对依赖关系的访问也被保证是连续的
示例:解决多样性依赖
你在本章前面看到了几个使用 Transient Lifestyle 的例子。在列表 8.3 中,Repository 是在解析方法中当场创建和注入的,而且 Composer 没有保留对它的引用。在列表 8.8 和 8.9 中,你随后看到了如何处理 Transient 可释放组件的方法。
在这些例子中,你可能已经注意到,userContext始终是一个Singleton。这是一个纯粹的无状态服务,所以没有理由为每个创建的ProductService创建一个新的实例。值得注意的一点是,你可以将依赖关系与不同的生命方式混合起来。
警告: 尽管你可以混合使用不同寿命的依赖关系,但你应该确保消费者只拥有寿命等于或超过其自身寿命的依赖关系,因为消费者将通过把依赖关系存储在其私有字段中来保持其寿命。如果不这样做,就会导致俘虏性依赖,我们将在第 8.4.1 节中讨论这个问题。
当多个组件需要相同的依赖时,每个组件都被赋予一个单独的实例。 下面的列表显示了一个解决ASP.NET Core MVC控制器的方法。
Listing 8.11 解决 TransientAspNetUserContextAdapter 实例
private HomeController CreateHomeController() {
return new HomeController(
new ProductService(
new SqlProductRepository(this.connStr),
new AspNetUserContextAdapter(), //各自或得了一个实例
new SqlUserRepository(
this.connStr,
new AspNetUserContextAdapter())));//各自或得了一个实例
}
Transient Lifestyle意味着每个消费者都会收到依赖的私有实例,即使在同一对象图中的多个消费者拥有相同的依赖时也是如此(就像前面的列表中的情况)。如果许多消费者共享同一个依赖,这种方法可能是低效的;但是如果实现不是线程安全的,更高效的Singleton Lifestyle就不合适了。在这种情况下,Scoped Lifestyle可能更适合。