什么是依赖注入
从本质上讲,依赖注入是一种强大的设计模式,它为现代软件开发(特别是在大型企业应用程序中)提供了几个关键优势。通过使用依赖注入,可以实现松散耦合、增强的可测试性和易于代码维护。
在传统开发中,使用类的构造函数实例化类非常简单。然而,随着代码库的增长以及该类在许多地方使用,多次调用构造函数会变得很麻烦。此外,当构造函数或类中需要更改或重构时,手动更新整个代码库中的每个实例可能非常耗时且容易出错。这些手动编辑可能会累积并影响关键的开发时间。
依赖注入消除了手动实例化类并将其传递给每个依赖组件的负担。相反它自动处理依赖项的实例化和注入。通过利用依赖项注入框架或技术,可以定义如何解析依赖项,并让框架在需要时管理和注入实例。
通过利用依赖项注入,可以集中依赖项的配置和管理,从而更轻松地引入更改或更新。当需要在构造函数或类中进行修改时,只需更新一个地方而不是搜索和修改整个代码库中的每个实例。这显着减少了出错的可能性并节省了宝贵的开发时间。
代码示例
1 | public class Dependency{ |
OtherClass类直接依赖于MyDependency类,原因如下:
- 用不同的实现替换 Dependency,必须修改 OtherClass 类
- 如果 Dependency 具有依赖项,则必须由 OtherClass 类对其进行配置。在具有多个依赖于 Dependency 的类的大型项目中,配置代码将分散在整个应用中
- 这种实现很难进行单元测试
依赖关系注入通过以下方式解决了这些问题:
- 使用接口或基类将依赖关系实现抽象化
- 在服务容器中注册依赖关系
- 将服务注入到使用它的类的构造函数中,框架负责创建依赖关系的实例,并在不再需要时将其释放
优化上述代码
1 | public interface IDependency{ |
- 不使用具体类型 MyDependency,仅使用它实现的 IMyDependency 接口。这样可以轻松地更改实现,而无需修改控制器或 Razor 页面
- 不创建 MyDependency 的实例,这由 DI 容器创建
服务类型
将依赖项注入到类中时有三种常用的服务类型,分别是:瞬态(Transient)、单例(Singleton)、作用域(Scoped),根据不同的情况为每个注册的服务选择适当的生存期。
瞬态服务(Transient Services)
瞬态服务广泛用于依赖注入,并且通常易于掌握。当请求瞬态服务时,将创建一个新对象并将其提供给消费类。这种根据每个请求实例化一个新对象的独特行为使得瞬态服务特别适合无状态和轻量级依赖。
由于每次注入都会生成一个新对象,因此不同服务使用之间不存在共享状态或数据。因此,瞬态服务非常适合服务行为应保持独立且不受先前调用影响的情况。
总结:瞬态服务适合轻量级、无状态的服务,在请求结束时会释放瞬态服务
单例服务(Singleton Services)
单例服务不是为每个请求创建一个新对象,而是实例化一个对象一次,然后在后续请求中重用它。
单例的概念围绕着确保在应用程序的整个生命周期中仅存在特定服务的单个实例。这意味着每当请求服务时都会返回相同的实例,从而促进不同组件之间的一致性和共享状态。
使用单例服务可以带来诸如提高性能和共享数据等好处。由于对象仅实例化一次,因此消除了创建多个实例的开销,从而优化了资源使用。此外,共享实例允许保存状态并能够在各个应用程序部分之间共享数据。
使用单例服务时必须注意确保共享状态不会导致意外的副作用或并发问题。此外,应仔细管理单例服务内的依赖关系,以避免测试或维护期间的紧密耦合和潜在挑战。
单一实例服务必须是线程安全的,并且通常在无状态服务中使用
总结:单例服务实例化一次,然后在后续请求中重用,可以提高性能,实现状态共享,整个应用程序生命周期
作用域服务(Scoped Services)
作用域服务提供了单例服务和瞬态服务之间的中间立场。它允许您在特定上下文或范围内维护状态并共享数据(例如 Web 请求或操作),而无需无限期地保留对象。这种方法在减少对象实例化数量(如单例服务)和实现高效垃圾收集之间取得了平衡。
作用域服务与瞬态服务相比,可以最大限度地减少频繁创建对象的需要。同时利用垃圾收集的优势,允许在范围内不再需要对象时将其丢弃。
当需要在特定上下文中进行有状态行为(例如处理 Web 应用程序中的用户会话或在操作期间管理资源)时,作用域服务非常有用,通过将对象的生命周期限制在其创建的范围内,可以确保维护正确状态并有效管理资源。
需要注意的是,作用域服务的确切定义和生命周期可能会有所不同,具体取决于所使用的依赖项注入框架或容器。
总结:作用域服务可以实现高效的内存使用并在定义的上下文或操作中管理资源
为什么应该使用依赖注入
在传统的紧密耦合设计中,类通常直接实例化它们的依赖关系,导致代码难以更改或更新。但是,通过依赖项注入,类的依赖项是从外部源提供的,从而使类较少依赖于特定的实现。这种松散耦合允许应用程序架构具有更大的灵活性和模块化性。
通过将依赖项注入到类中,可以在单元测试期间轻松地用模拟对象或测试替身替换实际依赖项。这种解耦允许对各个组件进行隔离测试,从而促进更全面、更可靠的测试实践。因此,可以自信地测试代码的行为,而不会受到复杂且相互关联的依赖关系的阻碍。
依赖注入促进了组件和依赖项的重用。可以通过注入依赖项轻松地在不同的上下文或场景中重用现有的实现。这鼓励代码重用,减少重复,并培育更加模块化和可扩展的架构。它还促进关注点分离以及创建更小、更集中、可以独立开发、测试和维护的组件。
依赖注入允许灵活且可互换的组件。通过抽象接口或抽象背后的依赖关系,可以轻松切换实现或引入新的实现,而无需修改依赖类。这种灵活性使您能够使应用程序适应不断变化的需求、集成新功能或利用依赖项的不同实现,例如使用单独的数据库或外部服务
使用哪一种服务
创建瞬态服务是最安全的,因为您始终会获得新实例。但是,由于依赖注入系统每次都会创建它们,它们将使用更多的内存和资源,如果它们太多,可能会对性能产生负面影响。
单例仅创建一次,直到应用程序结束才销毁。这些服务中的任何内存泄漏都会随着时间的推移而累积。因此要小心内存泄漏。单例也具有内存效率,因为它们一旦创建就可以在任何地方重用。
对状态很少或没有状态的轻量级服务使用瞬态生命周期。
当您想要维护请求中的状态时,作用域服务是更好的选择。
在需要维护应用程序范围状态的地方使用单例,例如:应用程序配置或参数、日志服务、数据缓存
示例代码
测试结果
由结果可见:瞬态服务每次请求都会创建新的实例;作用域服务每次请求创建新实例之后在此期间内都会保持不变;单例服务从一开始请求创建实例之后就保持不变一直到程序结束
将不同生命周期的服务注入另一个服务
将具有较低生命周期的服务注入到具有较高生命周期的服务中会将较低生命周期的服务更改为较高生命周期的服务。
考虑单例服务的示例,它依赖于另一个注册了短暂生命周期的服务。
当请求第一次到达时,会创建单例的新实例。它还创建瞬态对象的新实例并注入到 Singleton 服务中。
当第二个请求到达时,单例实例将被重用。单例已经包含临时服务的实例,因此不会再次创建它。这有效地将瞬态服务转换为单例服务。
因此,请记住以下规则
切勿将 Scoped & Transient 服务注入 Singleton 服务。
切勿将 Transient 服务注入作用域服务
参考链接
Dependency Injection Lifetime: Transient, Singleton & Scoped