依赖注入

2 min read
Zekari
软件工程系统思维架构

问题的起点

代码需要其他代码。一个类需要另一个类,一个模块需要另一个模块。这是软件开发的基本事实。

传统的做法是:当你需要什么,就在内部创建它。需要一个数据库连接?在构造函数里 new Database()。需要一个日志记录器?直接实例化 Logger

这看起来很自然。但它带来了一个根本性的问题——你的代码决定了它依赖什么

这个"决定权"看似无害,实际上锁死了你的选择。

💡 Click the maximize icon to view in fullscreen

控制的转移

依赖注入的核心不是"注入",而是"控制反转"(Inversion of Control)。

你的类不再决定使用什么依赖。它只声明需要什么,然后由外部提供。

这个转移改变了权力结构。

原来是:类说"我要用 MySQL 数据库"。现在是:类说"我需要一个符合数据库接口的东西"。

区别在哪?前者是命令,后者是契约。

命令是刚性的,契约是柔性的。命令绑定了具体实现,契约只约定了行为。

解耦的真正含义

很多人说依赖注入能"解耦"。但解耦解的是什么?

不是让代码之间没有关系。代码必须有关系,否则它们无法协作。

真正解耦的是具体实现和使用者之间的绑定

你的代码不需要知道数据库是 MySQL 还是 PostgreSQL。它只需要知道有一个对象能执行查询,能返回结果。

这个"不需要知道"不是无知,而是抽象。

抽象让你能在不改变使用者的情况下,替换具体实现。这就是灵活性的来源。

接口(Interface)在依赖注入中扮演关键角色。它定义了契约,规定了依赖对象必须提供什么行为。

使用者依赖接口,而不是具体实现。这样,任何实现了该接口的对象都可以被注入。

这不是技术细节,而是设计原则:依赖于抽象,而不是具体

测试性的自然结果

依赖注入常被说成"让代码更容易测试"。但这不是它的目的,而是它的结果。

当你的类接受外部提供的依赖时,你自然可以在测试时提供一个模拟(Mock)对象。

不需要连接真实的数据库,不需要发送真实的邮件。你可以提供一个假的数据库,一个假的邮件服务。

这不是因为依赖注入"支持测试",而是因为它从根本上改变了代码的结构。

好的设计自然带来好的测试性。 不是为了测试而设计,而是好的设计天然容易测试。

手动注入与框架注入

依赖注入有两种实现方式。

一种是手动注入。你在代码中显式地创建依赖,然后传递给需要它的对象。

const database = new PostgresDatabase(config);
const logger = new FileLogger('/var/log/app.log');
const userService = new UserService(database, logger);

另一种是框架注入。你声明依赖关系,框架自动解析和注入。

@Injectable()
class UserService {
  constructor(
    private database: Database,
    private logger: Logger
  ) {}
}

手动注入简单直接,但在大型应用中会变得繁琐。框架注入自动化了这个过程,但增加了学习成本和抽象层级。

选择哪种?取决于你的项目复杂度。

但无论选择哪种,核心原则不变——控制权在外部,不在内部

生命周期管理

依赖注入不只是传递对象,还涉及对象的生命周期。

一个依赖应该每次都创建新实例(Transient),还是整个应用共享一个实例(Singleton),还是在某个范围内共享(Scoped)?

这个决策很重要。

数据库连接通常是单例,因为创建连接昂贵。HTTP 请求处理器通常是瞬时的,因为每个请求应该独立。

依赖注入框架通常提供生命周期管理能力。但即使手动注入,你也需要思考这个问题。

对象应该活多久?谁负责创建和销毁它? 这些问题的答案塑造了你的应用架构。

依赖注入是强大的工具,但不是万能的。

有些情况下,直接创建依赖更简单、更清晰。比如简单的值对象、工具函数、不需要替换的内部实现。

过度使用依赖注入会导致:

  • 代码变得难以追踪(到处都是接口和注入点)
  • 增加不必要的抽象层级
  • 配置变得复杂而脆弱

原则是:当你需要灵活性和可测试性时使用依赖注入,当你需要简单性时直接创建

工具要服务于目的,不要为了使用工具而使用工具。

框架只是工具

很多人学习依赖注入,是通过 Spring、Angular、Nest.js 这些框架。

这些框架提供了强大的依赖注入容器,自动解析依赖关系,管理对象生命周期。

但框架不是依赖注入本身。

依赖注入是一个设计原则。框架只是实现这个原则的工具。

理解原则比学习框架重要。因为原则是本质,框架是形式。形式会变,本质不会。

当你理解了为什么要转移控制权,为什么要依赖抽象,为什么要从外部提供依赖,你就能在任何语言、任何框架中应用依赖注入。

甚至不需要框架。

设计的本质

依赖注入揭示了软件设计的一个核心问题:谁控制什么

传统的设计让每个组件控制自己的依赖。这看似自治,实则封闭。每个组件都是一个孤岛,难以改变,难以组合。

依赖注入反转了这个控制。组件不再控制依赖,而是声明需求。外部负责满足这些需求。

这个反转创造了灵活性。你可以组合不同的依赖,创造不同的配置,测试不同的场景。

软件的灵活性不是来自代码做了什么,而是来自代码不做什么。 依赖注入的价值,正是它让代码"不做"依赖的创建和管理。

最后

依赖注入不是一个技术技巧。它是一种思维方式。

它要求你思考边界。什么应该在内部,什么应该在外部。什么应该被控制,什么应该被开放。

这种思考超越了代码层面。它关乎系统的组织方式,关乎复杂性的管理方式。

当你习惯于从外部提供依赖,你会发现,很多设计问题变得更清晰。因为你在强迫自己思考:这个组件的真正职责是什么?它需要什么才能完成这个职责?

这个思考的过程,就是设计的过程。

Related Posts

Articles you might also find interesting

从意图到架构

3 min read

技术方案不是设计出来的,而是从问题中涌现的。理解这个过程,就理解了软件设计的本质。

软件工程架构
Read More

编码前的思考

1 min read

软件设计不是从代码开始的。在动手之前,有一套思维框架值得遵循:理解边界、定义数据、设计函数、建立抽象。

软件工程思维框架
Read More
Featured

代码的边界:从项目到包的重构实践

3 min read

将博客系统打包为 npm 包的完整实践,聚焦边界定义、抽象层建立和务实的取舍决策

npm重构
Read More

适配器模式:对现实的妥协

4 min read

当 PayPro 要求 IP 白名单而 Stripe 不需要,当一个按秒计费另一个按请求计费,架构设计不是消除约束——而是管理约束。适配器模式不是优雅设计,而是对现实混乱的务实投降。

架构支付集成
Read More

API 测试各种边界情况

2 min read

边界情况是系统最脆弱的地方,也是最容易被忽略的地方。测试边界情况不是为了追求完美,而是为了理解系统的真实边界。

API测试
Read More

文档标准是成本计算的前提

3 min read

API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。

API文档
Read More

调用链路追踪法:从断点到根因

2 min read

功能失效的背后,是一条完整的调用链路。追踪这条链路,定位断点,才能从根本上解决问题。

架构调试
Read More

离屏渲染:照片捕获为什么需要独立的 canvas

2 min read

实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。

架构前端开发
Read More

集中式配置:让 Reddit 组件脱离重复泥潭

2 min read

当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。

架构React 组件
Read More

约束驱动设计:为何选择内存追踪

2 min read

在 Cloudflare Workers 环境中实现追踪系统,持久化和内存存储之间的权衡不是技术偏好,而是约束驱动的必然选择。

架构Cloudflare Workers
Read More

CRUD 操作

2 min read

四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑

系统设计软件工程
Read More

让文档跟着代码走

2 min read

文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。

文档软件工程
Read More

双重导出管道的架构选择

2 min read

在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。

架构图像导出
Read More

端到端 Postback 模拟测试

2 min read

真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。

测试API
Read More

错误隔离

3 min read

失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。

系统设计可靠性工程
Read More

Purikura的页面系统

3 min read

通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。

架构React
Read More

重复数据的迁移实践:从 N 个文件到 1 个真相源

3 min read

当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。

架构配置管理
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

实现幂等性处理,忽略已处理的任务

3 min read

在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解

系统设计并发控制
Read More

引入懒加载模式

1 min read

懒加载不是优化技巧,而是关于时机的选择。何时创建,决定了系统的效率和复杂度。

软件工程性能优化
Read More

多厂商 AI 调度:统一混乱的供应商生态

3 min read

当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。

Purikura 项目架构
Read More

分布式 Workers 的解耦设计

3 min read

通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。

Purikura 项目架构
Read More

队列、可靠性与系统边界

1 min read

探讨消息队列系统如何通过时间换空间,用异步换解耦,以及可靠性背后的权衡

架构可靠性
Read More

Studio 系统架构:从状态机到端到端流程

3 min read

深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑

Purikura 项目架构
Read More

Context 驱动的认证状态管理

3 min read

认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。

软件工程认证系统
Read More