依赖注入
问题的起点
代码需要其他代码。一个类需要另一个类,一个模块需要另一个模块。这是软件开发的基本事实。
传统的做法是:当你需要什么,就在内部创建它。需要一个数据库连接?在构造函数里 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
信息的归宿
关于持续输出的系统性思考:为什么观点要慎发,为什么节奏比速度重要,为什么生活质量决定创作质量
莫向外求:内在力量的回归
所有真正持久的幸福、力量和智慧,其根源不在于外部世界,而在于内在状态。这是一次关于收回主导权的深刻探索。
一次一次的代价
在表格的每一行里调用一次查询,看起来最直接。但一次一次累积起来,代价会变得巨大。
离屏渲染:照片捕获为什么需要独立的 canvas
实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。
集中式配置:让 Reddit 组件脱离重复泥潭
当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。
约束驱动设计:为何选择内存追踪
在 Cloudflare Workers 环境中实现追踪系统,持久化和内存存储之间的权衡不是技术偏好,而是约束驱动的必然选择。
双重导出管道的架构选择
在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。
Purikura的页面系统
通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。
从意图到架构
技术方案不是设计出来的,而是从问题中涌现的。理解这个过程,就理解了软件设计的本质。
重复数据的迁移实践:从 N 个文件到 1 个真相源
当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。
引入懒加载模式
懒加载不是优化技巧,而是关于时机的选择。何时创建,决定了系统的效率和复杂度。
Context 驱动的认证状态管理
认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。
编码前的思考
软件设计不是从代码开始的。在动手之前,有一套思维框架值得遵循:理解边界、定义数据、设计函数、建立抽象。
解析 Payload
Payload 不只是技术术语,它揭示了一个更深刻的模式:真正有价值的东西,往往需要层层包装才能到达目的地