为什么依赖注入只在 Java 技术栈中流行,在 Go 和 C++ 没有大量使用?

2 阅读7分钟

Spring 项目里,写一个发送短信的功能。不考虑业务逻辑,光是"把这个类跑起来"需要什么?

@Service
public class SmsService {
    @Autowired
    private SmsChannelRouter router;
    @Autowired
    private SmsTemplateRepository templateRepo;
    @Autowired
    private SmsLogRepository logRepo;
    @Autowired
    private UserService userService;
    @Autowired
    private RateLimiter rateLimiter;
}

五个 @Autowired。SmsChannelRouter 内部又依赖 AliyunSmsClient 和 TencentSmsClient,两个 Client 各自需要配置类和 HTTP 客户端。SmsTemplateRepository 需要 DataSource。RateLimiter 需要 RedisTemplate,RedisTemplate 需要 RedisConnectionFactory……

拉完整棵依赖树,光是让这一个 SmsService 的单元测试跑起来,你就得 mock 五六个对象。

把同样的功能放到一个没有 DI 框架的语言里,比如 Go,长这样:

type SmsService struct {
    send func(phone, content string) error
    db   *sql.DB
}

一个发送函数,一个数据库连接。调用方创建的时候自己决定发送函数用阿里云还是腾讯云。没有容器,没有注解,没有自动扫描。

这个差异不是审美偏好,是语言的底层设计逼出来的。

Java 的 new 问题

Java 里所有东西都是类。发个短信要 SmsService 类,发短信之前要查模板是 SmsTemplateRepository 类,记录日志是 SmsLogRepository 类,选发送通道是 SmsChannelRouter 类,限流是 RateLimiter 类。每个类需要的依赖又是一堆类。

关键在 new 这个关键字。Java 里 new 一个对象必须写死具体类名:new AliyunSmsClient()。你不能写 new SmsClient()——SmsClient 是接口,接口不能 new。

SmsService 里直接 new AliyunSmsClient(),这个类就跟阿里云绑死了。想换腾讯云?改 SmsService 的代码。想测试的时候不真发短信?还是改 SmsService 的代码。

解决办法也直接:SmsService 不自己 new,让外面把 SmsClient 传进来:

public class SmsService {
    private final SmsClient client;

    public SmsService(SmsClient client) {
        this.client = client;
    }
}

构造方法收一个 SmsClient 接口,具体哪家的实现,SmsService 不管,谁创建它谁负责。

这就是依赖注入——一个简单到不需要任何框架的概念。

但 Java 就是需要框架

思路很简单,但在 Java 项目里手动做,规模一上来就撑不住。

一个中型 Spring Boot 项目启动时要创建多少个 Bean?算上框架自己的,几百个很正常。每个 Bean 有依赖,依赖又有依赖。创建顺序还有讲究——DataSource 得先于所有 Repository 创建,Repository 得先于 Service。

如果没有 Spring,你得自己写一个巨大的启动方法,按拓扑排序的顺序手动 new 几百个对象,一个个把依赖传进去。加一个新 Service,找到它依赖的所有对象的创建位置,确保顺序正确,插进去。改了某个类的构造方法参数,所有创建它的地方都得跟着改。

Spring 的 IoC 容器干的就是这个活。类上标个 @Service,构造方法声明需要什么类型,Spring 启动时扫描所有类,自动分析依赖关系,按正确顺序把 Bean 创建好、注入好。开发者不用操心"谁先创建谁后创建"的问题。

这不是偏好,是刚需。纯 OOP 导致了海量对象和复杂依赖图,手动管不过来,IoC 容器就成了标配。

Go 为什么没这个问题

Go 有函数。

听起来像废话,但这是关键区别。Java 里所有逻辑必须挂在类上,一个工具方法也得写成某个类的 static 方法。Go 的函数是一等公民,不需要包在对象里。

Java 项目里常见 StringUtilsDateUtilsValidationUtils——这些全是"为了装函数而存在的类"。类似的,很多 Service 本质上只是一堆方法的集合,没有自己的状态,但 Java 逼着你把它们装进类里,实例化成对象,再注入到别的对象里。

Go 直接写函数,import 包调用,不需要实例化。这一刀砍掉了 Java 里大量"只为存在而存在"的对象。

Go 的依赖关系也天然扁平。上面那个短信服务,在 Go 里可能就是一个结构体加两个字段。整个项目要管理的"有状态对象"数量级小得多——数据库连接、Redis 客户端、几个核心 Service,加起来十来个。main 函数里手动创建、手动传参,十几行代码搞定,每个依赖关系都写在明面上,IDE 里 Ctrl+点击就能跳转。

Go 社区不是不知道 DI 框架,Google 自己都出了个 Wire,Uber 出了个 Dig。但大部分 Go 项目根本不用。原因很现实——需要管理的对象就那么点,手动传参一点不痛苦,反而是引入框架之后 debug 变麻烦了,出了问题你得去理解框架的初始化逻辑而不是直接看代码。

C++ 就更不需要了

C++ 有模板,这是一种完全不同于 Java 接口+运行时多态的解耦机制。

Java 要实现"OrderService 不依赖具体的 Repository 实现",必须定义一个 Repository 接口,让具体实现类去 implement,然后运行时注入。C++ 可以用模板在编译期做这件事:

template<typename Repo>
class OrderService {
    Repo repo;
public:
    void createOrder() {
        auto user = repo.findById(1);
        // ...
    }
};

// 编译期就确定用哪个实现
OrderService<MySQLRepo> service;

模板参数在编译期展开,没有虚函数调用的开销,没有运行时类型信息,甚至编译器可以做内联优化。对于 C++ 追求极致性能的定位来说,运行时的 DI 容器引入的间接层是不可接受的。

C++ 也没有 Java 那种"所有东西都是对象"的包袱。自由函数、命名空间、值语义——C++ 有很多不需要通过对象管理来组织代码的手段。一个 C++ 项目里的"对象"数量远少于同规模的 Java 项目,压根不存在"对象图太复杂管不过来"的问题。

还有一个很现实的原因:C++ 没有反射。Java 的 Spring 通过反射扫描注解、发现类、创建实例、注入依赖——这套机制严重依赖运行时类型信息。C++ 的类型信息在编译完就扔了,运行时根本拿不到"这个类有哪些构造参数"这种信息。想在 C++ 里搞一个 Spring 那样的容器,技术上就不太可行。

Python、JS 也不怎么用

Python 有 DI 框架吗?有,dependency-injector 之类的。流行吗?不流行。Python 是动态类型语言,函数是一等公民,mock 一个依赖直接 monkeypatch 就行了,不需要靠接口+注入来实现可测试性。

JavaScript/TypeScript 呢?前端的 Angular 搞了一套 DI,但 React 和 Vue 完全不用。Node.js 后端的 NestJS 学了 Angular 的 DI 模式,但纯 Express 项目手动管理依赖照样跑得好好的。

规律很明显:DI 框架能流行起来,需要语言本身是纯 OOP、项目里有大量对象要管理、依赖关系足够复杂、语言还得有反射能力让框架自动干活。同时满足的主流语言,就 Java 和 C#。Java 有 Spring,C# 有 ASP.NET Core 内置的 DI——两家都把 IoC 容器当标配,不是巧合。

注入本身哪都有,容器才是 Java 特产

这个问题其实混淆了两件事。

Go 的 NewSmsService(db, sendFunc) 把依赖从外面传进去——这就是依赖注入。C++ 的模板参数 OrderService<MySQLRepo> 在编译期确定实现——这也是依赖注入。Python 函数参数里传一个 logger 进去——还是依赖注入。

所有语言都在用依赖注入,只不过手动传参就完事了,不需要单独拿出来当个概念讲。

Java 独特的地方在于它需要一个容器(Spring IoC、Guice)来自动化这个过程。几百个对象的依赖图太复杂,手动组装不现实,只能交给框架用反射帮你搞定。这个容器在 Go 和 C++ 里没有对应物——不是它们落后,是它们的语言设计压根没制造出这个规模的问题。