依赖注入(Dependency Injection, DI)的核心概念和解决的核心问题

核心概念:

依赖注入是一种设计模式,也是实现控制反转(Inversion of Control, IoC) 原则的一种具体技术。其核心思想是:

  1. 解耦: 将一个类(客户端)所依赖的其他类或服务(依赖项)的创建和管理职责,从该类内部移除
  2. 反转控制: 将依赖项的创建和提供(注入)的控制权,反转给外部(通常是框架、容器或调用者)。
  3. 注入方式: 依赖项通过构造函数属性/Setter方法接口方法等方式传递(注入)给需要它的类。

简单来说: 不是让对象自己去找它所依赖的东西(比如自己 new 一个),而是由外部(“注入器”)把依赖的东西“喂”给它。

解决的核心问题:

依赖注入主要解决软件开发中常见的几个痛点:

  1. 紧耦合(Tight Coupling): 当类 A 在其内部直接实例化它所依赖的类 B(例如 B b = new B();)时,A 和 B 就紧密耦合在一起。这意味着:

    • 修改困难: 如果想替换 B 的实现(比如换成更高效的 BImpl2),必须修改 A 的源代码。
    • 难以测试: 测试 A 时,无法轻松地将 B 替换为一个模拟对象(Mock)或桩对象(Stub)来进行隔离测试(因为 A 内部硬编码了 new B())。测试 A 会不可避免地触发真实的 B,这可能导致测试速度慢、依赖外部环境(数据库、网络)、或产生副作用。
    • 缺乏灵活性: 难以在运行时根据配置或条件动态切换依赖项的实现。
    • 违反单一职责原则: 类 A 不仅要完成自己的核心逻辑,还要负责创建和管理 B 的生命周期。
  2. 可测试性差: 如上所述,紧耦合使得单元测试(孤立地测试一个单元)变得非常困难。

  3. 代码重复和难以维护: 如果多个类都需要同一个依赖项(比如一个数据库连接池或日志服务),并且各自负责创建它,会导致创建逻辑重复,难以统一管理和配置。

  4. 生命周期管理复杂: 当依赖关系变得复杂(如依赖依赖的依赖)时,手动管理对象的创建顺序、作用域(单例、请求作用域等)和销毁变得异常繁琐且容易出错。

依赖注入如何解决这些问题?

  • 解耦: 类 A 不再关心如何创建 B。它只声明“我需要一个实现了某接口(或符合某基类)的东西”。
  • 可测试性: 在测试 A 时,你可以轻松地“注入”一个模拟的 B(MockB),这个 MockB 完全在你的控制之下,用于验证 A 是否正确地调用了 B 的方法,而无需启动真实的 B(如数据库、网络服务)。
  • 灵活性: 依赖项的具体实现可以在外部配置(如配置文件、代码配置)。更换实现只需要修改注入器的配置,无需修改使用它的类(A)。
  • 可维护性: 创建逻辑集中在注入器(如 DI 容器)中,避免重复。依赖关系清晰声明(通常在构造函数或属性上),代码更易理解。
  • 生命周期管理: DI 容器通常提供强大的生命周期管理功能(单例、瞬态、作用域),自动处理依赖项的创建和销毁。

举例说明(传统方式 vs. 依赖注入方式):

场景: 一个用户登录服务 (LoginService) 需要在登录成功后发送通知。通知方式可能是邮件 (EmailNotifier) 或短信 (SmsNotifier)。

1. 传统方式(紧耦合 - 自己创建依赖):

// 邮件通知实现
public class EmailNotifier {public void sendNotification(String message) {// 实际发送邮件的复杂逻辑System.out.println("Sending email: " + message);}
}// 登录服务 - 内部直接创建 EmailNotifier
public class LoginService {private EmailNotifier notifier; // 直接依赖具体实现类public LoginService() {this.notifier = new EmailNotifier(); // 紧耦合:在构造函数内部创建依赖}public void login(String username, String password) {// ... 验证逻辑 ...// 登录成功后发送通知notifier.sendNotification("User " + username + " logged in successfully.");}
}// 使用登录服务
public class Main {public static void main(String[] args) {LoginService loginService = new LoginService(); // LoginService内部已经绑定了EmailNotifierloginService.login("alice", "password123");}
}

传统方式的问题:

  • 紧耦合: LoginService 直接依赖具体的 EmailNotifier,并在其构造函数中硬编码了 new EmailNotifier()
  • 难以切换通知方式: 如果想改用 SmsNotifier,必须修改 LoginService 的源代码(把 new EmailNotifier() 改成 new SmsNotifier()),违反了开闭原则(对扩展开放,对修改关闭)。
  • 难以测试: 测试 login 方法时,它会真的尝试发送一封邮件!这很慢,可能失败(如果没有邮件服务器配置),并且测试关注点应该是登录逻辑是否正确,而不是邮件发送。你无法轻松地用模拟对象替换 EmailNotifier

2. 依赖注入方式(解耦 - 依赖由外部提供):

// 1. 定义通知接口 (抽象)
public interface Notifier {void sendNotification(String message);
}// 2. 邮件通知实现 (具体实现1)
public class EmailNotifier implements Notifier {@Overridepublic void sendNotification(String message) {System.out.println("Sending email: " + message);}
}// 3. 短信通知实现 (具体实现2) - 新增很容易
public class SmsNotifier implements Notifier {@Overridepublic void sendNotification(String message) {System.out.println("Sending SMS: " + message);}
}// 4. 登录服务 - 依赖抽象(接口),通过构造函数注入
public class LoginService {private Notifier notifier; // 依赖抽象接口,而不是具体类// 构造函数注入:依赖项通过参数传入public LoginService(Notifier notifier) {this.notifier = notifier; // 接收外部传入的Notifier实现}public void login(String username, String password) {// ... 验证逻辑 ...// 登录成功后发送通知 (通过接口调用)notifier.sendNotification("User " + username + " logged in successfully.");}
}// 5. 使用登录服务 (手动注入 - 模拟"注入器"的角色)
public class Main {public static void main(String[] args) {// 决定使用哪种通知方式 (配置点)Notifier emailNotifier = new EmailNotifier();// Notifier smsNotifier = new SmsNotifier(); // 切换通知方式只需改这一行!// 创建LoginService,并将依赖项(Notifier)注入给它LoginService loginService = new LoginService(emailNotifier); // 注入Email实现// LoginService loginService = new LoginService(smsNotifier); // 注入SMS实现loginService.login("bob", "securePass");}
}// 6. 测试登录服务 (使用Mock框架如Mockito)
public class LoginServiceTest {@Testpublic void testLoginSuccessSendsNotification() {// 1. 创建Notifier的模拟对象(Mock)Notifier mockNotifier = Mockito.mock(Notifier.class);// 2. 创建LoginService,注入模拟的NotifierLoginService loginService = new LoginService(mockNotifier);// 3. 执行登录操作loginService.login("testUser", "testPass");// 4. 验证:mockNotifier的sendNotification方法是否被正确调用了一次Mockito.verify(mockNotifier, Mockito.times(1)).sendNotification(Mockito.contains("testUser")); // 验证消息包含用户名}
}

依赖注入方式的优点:

  • 解耦: LoginService 只依赖于 Notifier 接口,完全不知道也不关心具体是 EmailNotifier 还是 SmsNotifier。它只关心接口契约。
  • 易于切换实现: 在程序入口(Main 或配置中),只需改变注入给 LoginService 的具体 Notifier 实例(如 new EmailNotifier()new SmsNotifier()),无需修改 LoginService 本身的代码。符合开闭原则。
  • 易于测试:
    • 在单元测试 LoginServiceTest 中,我们可以轻松地创建一个 Notifier 的模拟对象 (mockNotifier)。
    • 将这个模拟对象注入到 LoginService 中。
    • 执行 login 方法。
    • 验证 login 方法是否正确地调用了 mockNotifier.sendNotification(...) 方法,并检查了传递的参数。整个过程完全隔离,没有真实的邮件或短信发送! 测试快速、可靠、无副作用。
  • 可扩展性强: 添加新的通知方式(如 PushNotifier),只需实现 Notifier 接口并在注入点使用它即可。LoginService 完全不需要改动。
  • 职责清晰: LoginService 只负责登录逻辑,Notifier 负责发送通知,创建 Notifier 实例的职责由外部(如 Main 或 DI 容器)承担。符合单一职责原则。

依赖注入的常见方式:

  1. 构造函数注入(最推荐): 依赖项通过类的构造函数传入。优点:强制要求依赖,保证对象在构造完成后就是完整的、可用的状态;依赖关系明确;方便不可变(immutable)对象的创建。
  2. Setter方法注入(属性注入): 依赖项通过类的公共Setter方法设置。优点:比较灵活,可以在对象创建后改变依赖(但通常不推荐频繁改变)。缺点:对象可能在一段时间内处于依赖不完整的状态。
  3. 接口注入: 定义一个包含注入方法的接口,需要依赖的类实现这个接口,注入器通过该接口方法注入依赖。这种方式相对少见。

依赖注入容器(DI Container/IoC Container):

在实际的大型项目中,手动管理所有的依赖注入(像上面 Main 里那样)会变得非常繁琐。这时通常会使用依赖注入容器(如 Spring Framework for Java, .NET Core DI, Guice, Dagger 等)。容器的职责是:

  • 注册(Register): 告诉容器有哪些类型(接口和它们的实现类)需要管理,以及它们的生命周期(单例、每次请求新实例等)。
  • 解析(Resolve): 当需要一个对象(如 LoginService)时,容器会自动查找它的依赖(Notifier),创建依赖(或使用已存在的实例,如单例),并将依赖注入到目标对象中,最后返回组装好的、完全可用的目标对象实例。

使用容器后,创建对象的复杂性(对象图的构建)就完全交给了容器管理。

总结:

特性传统方式 (紧耦合)依赖注入方式 (松耦合)
依赖创建类内部创建 (new)外部创建并注入
耦合度 (依赖具体类) (依赖抽象接口/基类)
可测试性 (难以隔离测试) (易于注入Mock进行单元测试)
灵活性 (修改依赖需改代码) (通过配置/注入点轻松切换实现)
可维护性 (职责混杂,依赖关系隐式) (职责清晰,依赖关系显式声明)
扩展性 (添加新实现需修改客户端) (添加新实现只需注册并注入)
核心原则违反IoC、开闭原则、单一职责遵循IoC、开闭原则、单一职责、依赖倒置

依赖注入通过将对象的依赖关系与其创建逻辑分离,极大地提高了代码的松耦合性、可测试性、可维护性和灵活性,是现代软件开发中一项至关重要的设计模式和技术。 它通常与面向接口编程和单元测试实践紧密结合。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/88988.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/88988.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Reactor Schedulers

Reactor 是一个基于响应式编程的库,它提供了丰富的调度器(Schedulers)机制,用于管理异步操作的执行环境。Schedulers 是 Reactor 中的核心组件之一,它们允许开发者灵活地控制操作符和订阅操作在哪个线程上执行&#xf…

设备树引入

一、设备树的基本知识 1、什么是设备树?为什么会有设备树? 2011年,Linux之父Linus Torvalds发现这个问题后,就通过邮件向ARM-Linux开发社区发了一封邮件,不禁的发出了一句“This whole ARM thing is a f*cking pain i…

【数据标注师】3D标注

目录 一、 **3D标注知识体系框架**二、 **五阶能力培养体系**▶ **阶段1:空间认知筑基(2-3周)**▶ **阶段2:核心标注技能深化**▶ **阶段3:复杂场景解决方案**▶ **阶段4:领域深度专精▶ **阶段5&#xff1…

华为HN8145V光猫改华为蓝色公版界面,三网通用,xgpon公版光猫

咸鱼只卖20多元一个,还是xgpon的万兆猫,性价比不错哦 除了没有2.5G网口,其他还行。 改成公版光猫后,运营商是无法纳管光猫,无法后台修改光猫数据及超密。 华为 HN8145V 光猫具有以下特点: 性能方面 高速接…

【LeetCode 热题 100】438. 找到字符串中所有字母异位词——(解法二)定长滑动窗口+数组

Problem: 438. 找到字符串中所有字母异位词 题目:给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 【LeetCode 热题 100】438. 找到字符串中所有字母异位词——(解法一&…

PAC 学习框架:机器学习的可靠性工程

PAC(Probably Approximately Correct) 是机器学习理论的核心框架,用于量化学习算法的可靠性。它回答了一个关键问题: “需要多少训练样本,才能以较高概率学到一个近似正确的模型?” 一、PAC 名称拆解 术语…

嵌入式C语言数组:数组/字符数组

1. 数组 1.1 一维数组 数组是一串连续的地址; 数组名是地址常量,代表数组的起始地址; sizeof(数组名) 可得出数组的总内存空间; C 语言对数组不做越界检查,使用时应注意; 数组不…

变长字节的数字表示法vb224

开始 数字有大有小,用多少字节表示呢? 本文描述的方案,采用变化的长度。vb是varying bytes的意思,224是表示它特征的一个数。 第一版: 每个字节8比特,最高的1比特用来表示“是否连续”,0表示…

ByteMD+CozeAPI+Coze平台Agent+Next搭建AI辅助博客撰写平台(逻辑清楚,推荐!)

背景: 现在主流的博客平台AI接入不够完善,如CSDN接入的AI助手不支持多模态数据的交互、稀土掘金的编辑器AI功能似乎还没能很好接入(哈哈哈,似乎在考虑布局什么?) 痛点分析: 用户常常以截图的形式…

【数据标注师】关键词标注

目录 一、 **理解关键词标注的核心逻辑**1. **三大标注原则**2. **关键词类型体系** 二、 **四阶训练体系**▶ **阶段1:基础规则内化**▶ **阶段2:语义浓缩训练**▶ **阶段3:场景化标注策略**▶ **阶段4:工具效率提升** 三、 **五…

for each循环语句

for each循环语句 for each.....nextFor Each 的案例 for each…next 1、循环对象合集 worksheets workbooks range range("区域")selection (选中的区域)usedrange或者currentregion 返回的单元格区域格式: for each 变量名 in 对象集合(范围)循环内容…

基于LQR控制器的六自由度四旋翼无人机模型simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序 4.系统原理简介 5.参考文献 6.完整工程文件 1.课题概述 四旋翼无人机因其结构简单、机动性强和成本低廉等特点,在航拍测绘、物流运输、灾害救援等领域得到广泛应用。六自由度(3维平移3维旋转&#xff0…

vftp centos 离线部署

install_ftp_offline.sh vsftpd-3.0.2-28.el7.x86_64.rpm #!/bin/bash# 一键安装配置vsftpd脚本(开放根目录,禁用chroot)# 安装vsftpd RPM包 echo "正在安装vsftpd..." rpm -ivh vsftpd-3.0.2-28.el7.x86_64.rpm if [ $? -ne 0 …

【数据标注】事件标注1

目录 **一、 深入理解事件标注的核心概念****二、 系统学习:从理论到实践****1. 吃透标注指南****2. 语言学基础补充****3. 事件结构解析训练** **三、 分阶段实践:从简单到复杂****阶段1:基础标注训练****阶段2:进阶挑战****阶段…

在 Ansys Electronics Desktop 中启用额外的 CPU 内核和 GPU

Ansys Electronics Desktop (AEDT) 可以通过利用多个 CPU 内核和 GPU 加速来显著缩短仿真时间。但是,启用其他计算资源除了基本求解器许可证外,还需要适当的高性能计算 (HPC) 许可证。 默认情况下,基本许可证最多允许使用 4 个内核,而无需任何其他 HPC 许可。借助 Ans…

R语言机器学习算法实战系列(二十六)基于tidymodels的XGBoost二分类器全流程实战

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据准备数据探索转换因子查看属性相关性配对图PCA 可视化缺失值、异常值处理 & 特征标准数据分割构建模型与调参模型评估模型可解释性(变量重要性、SHAP、DALEX)变量…

零基础langchain实战一:模型、提示词和解析器

一,使用python调取大模型api 1,获取api_key 获取api_key 在各个大模型的官网中获取。 2,设置api_key 方式一: 在系统环境中可直接执行python代码:这里以deepseek为例 import os os.environ["DEEPSEEK_API_…

Pytorch分布式通讯为什么要求Tensor连续(Contiguous)

参考资料: https://github.com/pytorch/pytorch/issues/73515 https://www.cnblogs.com/X1OO/articles/18171700 由于业务原因,需要在Pytorch代码中使用分布式通讯来把计算负载平均到多张显卡上。在无数次确认我的业务代码没问题之后,我开始把…

关于前端页面上传图片检测

依赖于前文,linux系统上部署yolo识别图片,远程宿主机访问docker全流程(https://blog.csdn.net/yanzhuang521967/article/details/148777650?spm1001.2014.3001.5501) fastapi把端口暴露出来 后端代码 from fastapi import FastAPI, UploadFile, File, HTTPExcep…

第十三章---软件工程过程管理

仅供参考 文章目录 一、Gantt图是做什么的。二、软件配置的概念 一、Gantt图是做什么的。 Gantt 图(甘特图)是软件项目管理中用于进度安排和可视化管理的重要工具,主要用于展示任务的时间安排、进度状态及任务之间的依赖关系 Gantt 图是一种…