依赖注入实战:写好可测试、可替换的代码

举报
yd_237615889 发表于 2026/10/02 15:40:35 2026/10/02
【摘要】 博客 · 开发实践 / 代码设计 · 2026-10-02依赖注入实战:写好可测试、可替换的代码依赖注入不是框架特性,而是一个习惯:把依赖从「内部创建」改成「外部传入」。这一个改动,决定了你的代码能不能被独立测试、能不能在不改源码的前提下替换实现。工程实践手记 ·2026-10-02 ·约 12 分钟阅读一、从一个坏习惯说起:依赖在内部创建看一段很常见的代码:函数内部直接创建数据库连接和邮件...
博客 · 开发实践 / 代码设计 · 2026-10-02

依赖注入实战:写好可测试、可替换的代码

依赖注入不是框架特性,而是一个习惯:把依赖从「内部创建」改成「外部传入」。这一个改动,决定了你的代码能不能被独立测试、能不能在不改源码的前提下替换实现。


工程实践手记 ·2026-10-02 ·约 12 分钟阅读

一、从一个坏习惯说起:依赖在内部创建

看一段很常见的代码:函数内部直接创建数据库连接和邮件客户端。它的三个问题也正是依赖注入要解决的三个问题:测试要连真数据库(慢且不稳)、想换邮件服务必须改源码(实现与使用耦合)、依赖是隐形的(看签名完全不知道它依赖什么)。

// ✗ 依赖在内部 new:无法替换、无法隔离测试
function processOrder(order) {
  const db = new MySQLConnection();   // 硬编码
  const mailer = new SmtpMailer();    // 换服务要改这里
  ...
}

// ✓ 依赖从外部传入:显式、可替换、可测试
function processOrder(order, deps) {
  deps.db.save(order);
  deps.mailer.send(order.email, "已下单");
}

改动其实很小:把「创建」从函数里挪出去,只留「使用」。创建放到哪里?放到离程序入口最近的地方(后面第四节的"组合根")。这就是依赖注入的全部核心——框架只是帮你把组装做得更省事,不是必需品。

依赖注入的本质:谁使用,谁声明;谁组装,谁创建——使用与创建分离。
· · ·

二、三种注入形式:按场景选

形式 做法 适用
构造器注入 依赖在对象创建时一次性传入 首选:对象创建后状态完整、依赖关系清晰
方法参数注入 需要时才作为参数传入 一次性依赖,如日志器、请求上下文
属性 / 容器注入 框架在运行时赋值 框架场景,尽量少用:依赖关系不再显式

选择标准一句话:依赖是"这个对象活着的必需品"就用构造器注入;只影响某一次调用就用参数传入。运行时注入能不用就不用——它会让"这个类到底依赖什么"重新变得不可见。

三、什么时候需要 DI:答案比想象的少

  • 需要测试替身时:数据库、HTTP 客户端、消息队列——凡是要在测试里替换的(即"接缝"),都值得注入
  • 需要在多实现间切换时:多个支付渠道、多家短信服务——依赖指向接口,实现外部决定
  • 不需要时:无副作用的小工具(uuid、格式化函数)直接调用即可——把它们也注入一遍,是常见的过度设计

一条简单判据:只注入"接缝"处的依赖——IO、时间、随机、第三方。纯函数和值对象不是接缝。

四、手写组合根:不引框架也能优雅装配

依赖在哪里创建?答案:在程序入口集中组装一次,然后一路传下去——这个位置叫组合根(composition root)。它让"用哪些实现"成为一个可一眼看懂的地方:

// composition root(示意):程序入口集中装配
const deps = {
  db: new MySQLConnection(env.databaseUrl),
  mailer: env.mailer === "ses" ? new SesMailer() : new SmtpMailer(),
  clock: () => new Date(),
};
server.start((req) => processOrder(req.order, deps));

什么时候才需要依赖注入容器?当模块数量多、装配样板重复到影响效率时——而即便如此,很多团队用几行工厂函数就足够了。容器是手段不是目标:如果引入容器后"谁依赖谁"只能运行时才知道,那是理解成本的负收益。

五、DI 与测试的化学反应

  • 用内存实现替换数据库:测试跑在毫秒级,不再依赖外部服务(呼应单元测试一篇的 Fake 策略)
  • 用假时钟控制时间:注入一个可控的"现在",时间边界(跨天、过期)全部可测、可重复
  • 按需构造依赖:每个用例只装配它关心的两个依赖,而不是启动整个世界

六、可替换性:面向"我需要的形状"

注入的时候,类型写在什么上决定了可替换性:依赖指向"我需要的形状"(接口/协议),而不是某个具体实现。两条纪律:接口最小化——只声明用到的能力(比如只要 Send,就不要把整个邮件服务的十个方法都搬进接口);第三方 SDK 挡在适配器后面——核心代码不出现厂商类型,换供应商只写一个新适配器(呼应设计模式一篇的适配器思想)。

七、五个常见坑

  • 服务定位器反模式:到处 container.get("db")——依赖重新变隐形,测试仍难写;"从全局拿"与"从内部 new"的问题是同一类
  • 容器滥用:注册上百个服务,运行时才拼出依赖图——静态不可读,出错才知道
  • 接口膨胀:每个类都造一个接口,却没有第二个实现——多出来的间接层只增加维护面
  • 构造函数依赖爆炸:一个类注入十个依赖——不是 DI 的问题,是这个类职责过重(呼应单元测试篇"mock 五个依赖 = 设计信号")
  • 把 DI 当目的:连格式化函数都注入——为"架构好看"牺牲了代码直觉

速查卡

场景 做法
函数内部 new 了外部资源 改为参数传入——使用与创建分离
依赖活一整轮 构造器注入
依赖只用一次 方法参数传入(如日志器)
在哪里创建实现 组合根:入口集中装配,一路传下去
测试要隔离外部 注入内存实现 / 假时钟,不 mock 业务对象
第三方 SDK 侵入核心 包一层适配器,接口只留需要的能力

写在最后

依赖注入值得记住的只有两句话:使用与创建分离、组装集中在入口。做到这两点,你的代码就能在没有数据库、没有网络、没有真实时钟的环境里被完整测试,也能在不碰业务逻辑的前提下替换任何外部实现——这正是一套代码"活得久"的底子。

下一步 · 找一处「函数内部 new 外部资源」改成参数传入
挑一个你最近写过的函数:把数据库/HTTP 客户端的创建挪到调用处试试——顺手为它写一个不连任何外部服务的测试。系列下一篇候选:事件驱动架构入门、性能预算实践、结构化日志规范。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。