构造函数注入——通过构造函数参数传入依赖,是最推荐的方式。优点是依赖不可变、对象创建时即保证所有依赖就绪,缺点是参数过多时构造函数会变长。
属性注入——通过公开属性设置依赖,灵活度较高。优点是无需修改构造函数,缺点是依赖可能被遗漏设置,无法保证对象创建后的完整性。
方法注入——通过特定方法传入依赖,较少使用。适用于依赖仅在特定操作中需要的场景。本演示工具使用构造函数注入方式,在 UserController 的构造函数中注入 ILogger、IUserRepository 和 IEmailService,这是业界公认的最佳实践。
Transient(瞬态):每次请求都创建新实例,各实例之间相互独立。适合有状态或需要独立数据的服务,如数据库上下文、工作单元等。
选择原则:如果服务是无状态的且创建开销较大,优先使用 Singleton;如果服务持有可变状态或需要保证线程安全隔离,使用 Transient。在本工具中,您可以通过多次解析 UserController 并对比实例检查器中的实例 ID,直观感受两种生命周期的差异。Singleton 模式下 ID 不变,Transient 模式下每次解析都产生新的 ID。
解耦:类只依赖接口而非具体实现,降低了模块间的耦合度,修改实现不影响使用方。
可测试性:方便替换为 Mock 对象进行单元测试,测试时可以注入模拟依赖。
集中管理:所有依赖关系在容器中统一配置,便于全局理解和调整。
生命周期控制:容器统一管理对象的创建和销毁,避免资源泄漏。
减少样板代码:无需手动编写工厂类和依赖组装逻辑。
可替换性:更换服务实现只需修改注册配置,无需修改业务代码。
本工具通过实际演示这些好处的运作方式,帮助您理解为什么现代框架(Spring、ASP.NET Core、Angular)都默认集成了 DI 容器。
.NET 生态:Microsoft.Extensions.DependencyInjection(官方框架)、Autofac(功能强大,支持高级特性)、Unity(微软早期框架)。
Java 生态:Spring IoC Container(最流行的 Java DI 框架,支持完整的 Bean 生命周期管理)、Google Guice(轻量级,由 Google 开发)、Dagger(编译时 DI,由 Google 开发,适用于 Android)。
PHP 生态:Laravel Service Container(Laravel 框架内置)、Symfony DI(Symfony 框架组件)。
JavaScript/TypeScript 生态:InversifyJS(基于 TypeScript 装饰器)、Awilix(支持自动解析)、NestJS DI(NestJS 框架内置)。
Python 生态:dependency-injector、inject 等。不同框架的原理相似,都实现了服务注册、依赖解析、生命周期管理等核心功能。
检测方式:优质的 DI 容器框架(如 Spring、Autofac)会在解析时自动检测循环依赖,并抛出明确的错误信息,指出循环链路中的具体服务名称。
解决方案:
① 重构设计:提取公共接口或服务打破循环,重新划分职责边界;
② 属性注入:将其中一个依赖从构造函数注入改为属性注入,延迟依赖设置;
③ Lazy 加载:使用 Lazy<T> 包装依赖,延迟到实际使用时才解析;
④ 引入中间层:使用 Mediator 模式将直接依赖转为间接依赖。
循环依赖通常是设计不良的信号,解决循环依赖的过程往往能推动更好的架构设计。
① 类型查找:根据请求的服务类型在注册表中查找对应的实现类型和生命周期配置。
② 依赖分析:分析实现类型的构造函数参数,识别出所有依赖的服务类型。
③ 递归解析:对每个依赖的服务类型重复步骤①和②,直到所有依赖都被解析。
④ 实例创建:按照正确的顺序(从最深层的依赖开始)创建所有实例。
⑤ 依赖注入:将创建好的依赖实例注入到目标对象的构造函数中。
⑥ 生命周期管理:根据配置的生命周期(Singleton/Transient/Scoped)决定实例的缓存策略。
⑦ 日志记录:记录整个解析过程供调试和审计使用。
本工具的日志系统完整记录了上述步骤,帮助您追踪容器的每一步操作。
依赖关系图以图形化方式展示了服务之间的依赖网络。图中用不同颜色区分接口(蓝色)、具体实现(绿色)和目标类(橙色),用箭头标注依赖方向,用虚线表示尚未注册的服务。依赖关系图是理解容器如何自动构建复杂对象网络的有力可视化工具,它直观呈现了"一个服务需要哪些依赖、这些依赖又依赖什么"的完整依赖链。
不可变性:通过构造函数传入的依赖通常赋值给 readonly 字段,确保对象创建后依赖不可被修改,保证对象状态的一致性。
完整性保证:对象创建时所有依赖就绪,不会出现使用时才发现某个依赖未设置的运行时错误。
显式依赖:构造函数参数列表清晰展示了该类的所有依赖,便于理解代码的职责和复杂度。
可测试性:单元测试时可以直接传入 Mock 对象,无需额外的设置步骤。
框架支持:几乎所有主流 DI 容器都对构造函数注入提供最好的支持。
当构造函数参数过多(通常超过 4-5 个)时,可能是类的职责过重的信号,应该考虑拆分职责而不是改用属性注入。
① 定义接口:为需要注入的服务定义清晰的接口契约,确保接口职责单一。
② 实现接口:编写具体的实现类,实现类只关注自身的业务逻辑。
③ 选择 DI 框架:根据技术栈选择合适的 DI 容器框架(如 .NET 选 Autofac、Java 选 Spring、JS 选 InversifyJS)。
④ 配置注册:在应用启动时将所有服务注册到容器中,指定接口到实现的映射和生命周期。
⑤ 构造函数注入:修改现有代码,将 new 关键字创建依赖的方式改为通过构造函数接收依赖。
⑥ 编写测试:利用 DI 的优势,为每个服务编写单元测试,测试时注入 Mock 依赖。
引入 DI 应循序渐进,可以从新功能开始使用,逐步重构现有代码。本工具的交互演示可以帮助您在动手写代码之前先建立对 DI 工作原理的清晰认知。
UD5工具箱