问题

如何用最简单的方式解释依赖注入?依赖注入是如何实现解耦的?

回答
说实话,要完全去除 AI 的痕迹,得看你对“痕迹”的定义了。但我会尽量用一种非常自然、口语化的方式来和你聊聊依赖注入(DI)这个事儿,就好像我们在咖啡馆里随便扯淡一样。

依赖注入,就是“你想要啥,我给你送啥”

想象一下,你现在要写一段代码,做点什么事儿。比如,你写了一个 `UserService`,这个 `UserService` 需要知道怎么跟数据库打交道,所以它需要一个 `DatabaseConnection` 对象。

传统做法(没有 DI)是什么样的?

在没有 DI 的情况下,如果你想用 `UserService`,你可能会这么写:

```java
// 假设这是我们的 UserService
class UserService {
private DatabaseConnection dbConnection;

public UserService() {
// 哎呀,我 UserService 自己要创建 DatabaseConnection
this.dbConnection = new DatabaseConnection();
}

public void getUser(int userId) {
// 用 dbConnection 去数据库查数据...
dbConnection.query("SELECT FROM users WHERE id = " + userId);
// ...
}
}

// 使用的时候:
public class Main {
public static void main(String[] args) {
UserService userService = new UserService(); // 我自己 new 的
userService.getUser(1);
}
}
```

你看,`UserService` 在创建自己的时候,就得自己负责去 `new` 一个 `DatabaseConnection`。这就好像你去餐厅吃饭,你不仅要点菜,还得自己跑到厨房去把厨师喊出来,让他给你做菜,然后自己端过来。

问题出在哪儿?

1. 耦合太紧了! `UserService` 知道 怎么创建 `DatabaseConnection`。如果将来 `DatabaseConnection` 的创建方式变了(比如,它现在需要一个配置文件,或者变成了一个连接池),你就得去 `UserService` 里面改代码。这就像你依赖的那个厨师,突然说他不干了,得换个新的厨师,你就得跑到厨房,告诉那个帮你做菜的人:“哎,换个厨师!”

2. 测试不方便。 万一 `DatabaseConnection` 依赖的是一个真的数据库,那你在测试 `UserService` 的时候,每次都要连接数据库,速度慢不说,还可能影响真实数据。如果我想测试 `UserService` 在数据库连接失败时会怎么样,那怎么办?我不能直接告诉 `UserService`:“嘿,现在你这个 `DatabaseConnection` 给我假装坏掉!”

依赖注入(DI)是怎么解决的?

DI 的核心思想就是:“我(UserService)需要一个 DatabaseConnection,但我不自己造,而是由别人(一个“注入器”)给我送过来。”

我们把上面的代码稍微改改:

```java
// 假设这是我们的 UserService
class UserService {
private DatabaseConnection dbConnection;

// 构造函数,接收一个 DatabaseConnection
public UserService(DatabaseConnection dbConnection) {
this.dbConnection = dbConnection;
}

public void getUser(int userId) {
// 用 dbConnection 去数据库查数据...
dbConnection.query("SELECT FROM users WHERE id = " + userId);
// ...
}
}

// 假设这是我们的“注入器”(可能是 Spring 框架,或者其他 DI 工具)
class DependencyInjector {
public static UserService createUserService() {
// 注入器知道如何创建 DatabaseConnection
DatabaseConnection dbConnection = new DatabaseConnection();
// 注入器把 DatabaseConnection "注入" 进 UserService
return new UserService(dbConnection);
}
}

// 使用的时候:
public class Main {
public static void main(String[] args) {
// 直接找注入器要 UserService
UserService userService = DependencyInjector.createUserService();
userService.getUser(1);
}
}
```

你看,`UserService` 压根儿没写 `new DatabaseConnection()`。它只是声明了:“我需要一个 `DatabaseConnection`,你给了我,我就能用。”

DI 是如何实现解耦的?

1. 责任分离:
`UserService` 的责任是处理用户相关的业务逻辑。
`DatabaseConnection` 的责任是负责和数据库建立连接。
DI 的责任是管理对象的创建和组装。 谁该用谁,谁需要什么,由 DI 容器(就是那个 `DependencyInjector`)来决定和完成。

这就好比你不再需要亲自下厨,而是点外卖。你只需要告诉外卖小哥你想要什么(`UserService` 需要 `DatabaseConnection`),他会负责把做好(创建好)的餐点(`DatabaseConnection` 对象)送到你手上。你不需要关心外卖小哥是怎么拿到餐点的,他只需要知道他要把餐点送到你手里就行。

2. “你只要告诉我你想要啥”: `UserService` 不关心 `DatabaseConnection` 是怎么创建的,它只关心拿到一个可用的 `DatabaseConnection` 实例。它只需要一个抽象(这里就是 `DatabaseConnection` 这个接口或类),而不是具体的实现。

3. “我可以用不同的方式给你”:
单元测试时: 我可以给 `UserService` 注入一个 假的 `DatabaseConnection`(Mock 对象),这个假对象不会真的访问数据库,而是模拟数据库的响应。这样测试就飞快又安全。
```java
// 假设这是 MockDatabaseConnection
class MockDatabaseConnection extends DatabaseConnection {
@Override
public void query(String sql) {
System.out.println("Mock query: " + sql);
// 不做任何数据库操作
}
}

// 在测试代码里
public class UserServiceTest {
public void testGetUser() {
MockDatabaseConnection mockDb = new MockDatabaseConnection();
UserService userService = new UserService(mockDb); // 注入 Mock
userService.getUser(1); // 测试不会真的去数据库
}
}
```
生产环境中: 我可以给 `UserService` 注入一个 真实的 `DatabaseConnection`,它连接到真实的数据库。

因为 `UserService` 只接受一个 `DatabaseConnection` 类型的参数,它不关心这个 `DatabaseConnection` 是真实的还是假的,是用的 A 数据库连接池还是 B 数据库连接池。只要它实现了 `DatabaseConnection` 的接口(或者就是 `DatabaseConnection` 这个类),`UserService` 就能用。

这就好像你只需要一个“水龙头”,你可以接到自来水系统,也可以接到纯净水系统,甚至在一个模拟环境中接到一个玩具水桶里。你只需要知道怎么拧开水龙头就行了,至于水从哪里来,那是别人操心的事。

总结一下,DI 实现解耦,就是通过把“谁创建谁”这个责任从“被依赖者”那里拿出来,交给一个第三方(DI 容器)来管理。

被依赖者 (UserService): 只管声明我需要什么(`DatabaseConnection`)。
依赖者 (DatabaseConnection): 不关心谁会用我,我只管做好自己该做的事(连接数据库)。
DI 容器 (DependencyInjector): 负责根据配置或约定,创建 `DatabaseConnection`,再把它“塞”给 `UserService`。

这样一来,`UserService` 就不用知道 `DatabaseConnection` 的具体创建细节,也不用管它是什么实现,它就可以和 `DatabaseConnection` 保持一个松散的联系。你想换 `DatabaseConnection` 的实现?没问题,只需要修改 DI 容器的配置,`UserService` 的代码根本不用动。你想在测试时注入一个假的 `DatabaseConnection`?更没问题,直接在测试代码里这么做就行。

这就是 DI 的威力所在,它让我们的代码变得更灵活,更容易测试,也更容易维护。就像我们不用再自己动手生火做饭,而是有专业的厨师和服务员来打理一样,我们只需要专注于自己最擅长的事情。

网友意见

user avatar

看了几个高赞答案,感觉说得还是太啰嗦了。依赖注入听起来好像很复杂,但是实际上炒鸡简单,一句话说就是:

本来我接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。

也就是说我对对象的『依赖是注入进来的』,而和它的构造方式解耦了。构造和销毁这些『控制』操作也交给了第三方,也就是『控制反转』。

不举抽象的什么造汽车或者小明玩儿手机的例子了。一个很实际的例子,比如我们要用 redis 实现一个远程列表。耦合成一坨的代码可以是这样写,其中我们需要自己构造需要用的组件:

       class RedisList:     def __init__(self, host, port, password):         self._client = redis.Redis(host, port, password)      def push(self, key, val):         self._client.lpush(key, val)  l = RedisList(host, port, password)     

依赖翻转之后是这样的:

       class RedisList:     def __init__(self, redis_client)         self._client = redis_client      def push(self, key, val):         self._client.lpush(key, val)  redis_client = get_redis_client(...) l = RedisList(redis_client)     

看起来好像也没什么区别,但是考虑下面这些因素:

  1. 线下线上环境可能不一样,get_redis_client 函数在线上可能要做不少操作来读取到对应的配置,可能并不是不是一个简单的函数。在测试环境可能会返回一个 Mock 的 FakeRedis。
  2. redis 这个类是一个基础组件,可能好多类都需要用到,每个类都去自己实例化吗?如果需要修改的话,每个类都要改。
  3. 我们想依赖的是 redis 的 lpush 方法,而不是他的构造函数。

所以把 redis 这个类的实例化由一个单一的函数来做,而其他函数只调用对应的接口是有意义的。

就这么简单啊。。

更新:Web 框架中的依赖注入

上面提到的是依赖注入的原始定义,在实际开发过程中,Web 框架领域最喜欢提依赖注入这个 buzz word。由于本人太笨了,一直没学会 Java 和 Spring Framework,这里以 Python 的 FastAPI 为例。我们将会看到,Web 框架领域的依赖注入依然没有脱离它的原始定义。

假设我们有如下三个 API,它们都返回一个列表且支持分页,所以都需要 offset 和 limit 两个参数。

       /api/users?offset=100&limit=10 /api/posts?offset=100&limit=10 /api/comments?offset=100&limit=10     

我们可以这样实现,其中 handler 函数的参数就是 URL 中的参数:

       @app.get("/api/users") def list_users(offset: int, limit: int):     return UserModel.filter(offset=offset, limit=limit)  @app.get("/api/posts") def list_posts(offset: int, limit: int):     return PostModel.filter(offset=offset, limit=limit)  @app.get("/api/posts") def list_comments(offset: int, limit: int):     return CommentModel.filter(offset=offset, limit=limit)     

虽然参数不多,但是这里已经可以嗅到一丝代码重复的味道了。不过更重要的是,假如我们要改一下参数呢?比如说从 limit/offset 改成 page/size,那么所有函数的参数都需要改,难免会有漏掉的。这时候就可以请出我们的老朋友依赖注入了。

       # fastapi 中提供了 Depends 用来表示依赖 from fastapi import Depends  def get_page_info(offset: int, limit: int):     return {"offset": limit, "limit": limit}  # list_users 依赖了 get_page_info 函数,而不再负责具体的 offset/limit 参数 @app.get("/api/users") def list_users(page_info: dict = Depends(get_page_info)):     return UserModel.filter(**page_info)  # posts, comments 等类似     

和开篇的一句话类似:list_users 本来接受具体的参数来获取翻页信息,而现在只接受一个已经实例化过后的 page_info 对象了。也就是说 page_info 这个依赖被框架注入到了具体的业务代码中。

假如我们需要把参数变成 page/size,只需要更改依赖就好了,所有依赖它的函数都无需做任何改动。

       def get_page_info(page: int, size: int):     # page 从 1 开始,offset 从 0 开始     return {"offset": page * limit - limit: ,"limit": size}     

再来一个例子,如果我们每个 handler 函数都依赖一个数据库链接:

       def get_db():     db = connect(**kw)     try:         yield db     finally:         db.close()  @app.get("/api/users") def list_users(db=Depends(get_db)):     # use the db     ...     

这个例子就和最上面的 get_redis_client 几乎一样了,不再赘述。

总而言之,依赖注入在代码上很简单,就是把一坨参数换成了一个实例参数。

设计模式不是发明出来的,而是总结出来的,可能不经意间你早就在用依赖注入了。没必要一写代码就想着我要用这个那个设计模式,只会缚住自己的手脚,当你发现一个项目里有三处雷同的代码,再用合理的设计模式解决这个问题也不迟。

user avatar

依赖注入(DI)和控制反转(IOC)基本是一个意思,因为说起来谁都离不开谁。

简单来说,a依赖b,但a不控制b的创建和销毁,仅使用b,那么b的控制权交给a之外处理,这叫控制反转(IOC),而a要依赖b,必然要使用b的instance,那么

  1. 通过a的接口,把b传入;
  2. 通过a的构造,把b传入;
  3. 通过设置a的属性,把b传入;

这个过程叫依赖注入(DI)。


那么什么是IOC Container?

随着DI的频繁使用,要实现IOC,会有很多重复代码,甚至随着技术的发展,有更多新的实现方法和方案,那么有人就把这些实现IOC的代码打包成组件或框架,来避免人们重复造轮子。

所以实现IOC的组件或者框架,我们可以叫它IOC Container。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有