为什么需要依赖注入

写代码时,一个类往往要用到另一个类:订单服务要调用仓储,仓储要连数据库。如果在类内部直接 new 出它依赖的对象,类与实现就死死绑在一起——想换一个仓储做测试,几乎无从下手。依赖注入(Dependency Injection,DI)的核心思路是:对象不自己创建依赖,而是由外部把依赖「递」进来。

依赖抽象,而不是依赖具体实现;这是 DI 最重要的一句话。

好处是显而易见的:换实现只要换一个注册,测试时传入假对象即可,业务代码一行不用改。.NET 从底层就内置了 DI 容器,不需要第三方库就能用。

DI 解决的三个问题
  • 解耦依赖面向接口,替换实现不影响使用方。
  • 可测单元测试时注入伪造依赖,脱离真实数据库。
  • 统一管理对象的创建与生命周期交给容器集中处理。
依赖注入架构配图:发光积木模块经中央半透明管道彼此连接,光点在模块间穿行
模块之间不直接 new,而是通过容器这条「管道」交换依赖。

DI 容器的注册与生命周期

使用 DI 前要先「注册」:告诉容器「当有人要 IOrderRepository 时,给它 OrderRepository」。.NET 内置容器提供三种生命周期,选错是最常见的 bug 来源。

services.AddScoped<IOrderRepository, OrderRepository>();
services.AddSingleton<IClock, SystemClock>();
services.AddTransient<INotification, SmsNotification>();
三种生命周期
  • Singleton整个应用一个实例,适合无状态的工具。
  • Scoped每一次请求一个实例,Web 里最常用。
  • Transient每次取都新建,适合轻量无状态对象。

最容易踩的坑是「把 Scoped 服务注入 Singleton」。单例对象在应用启动时就创建,它捕获的 Scoped 依赖会一直活着,造成数据错乱。记住一条规则:依赖的生命周期不能比被依赖的更长。

模块化:把功能切成独立单元

项目一大,所有服务都堆在一个容器里,很快就分不清谁是谁。模块化架构的思路是:按业务功能把应用切成独立模块,比如订单模块、用户模块、支付模块。每个模块自己负责注册自己的服务、定义自己的接口。

public class OrderModule {
    public void Load(IServiceCollection services) {
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<IOrderService, OrderService>();
    }
}

这样做的好处是:想删一个功能,只需删掉对应模块的调用;想抽成独立服务,模块边界天然就是拆分线。模块内部的实现对外不可见,只暴露少数接口。

模块之间如何通信

模块之间不能随便互相调用内部实现,否则又回到紧耦合。推荐两种方式:一是通过共享接口,订单模块依赖一个抽象的「支付接口」,具体支付模块去实现;二是用事件——订单创建后发布一个事件,其他模块各自订阅处理,彼此完全不认识。

mediator.Publish(new OrderCreatedEvent(orderId));
// 用户模块、库存模块各自订阅,互不干扰

这种方式让新增一个下游动作变得很轻松:比如以后要加「下单后积分加一」,只要新增一个订阅者,订单模块一行不用改。

从注册到组装的完整示例

把上面的思路拼起来:程序启动时,先创建容器,再逐个调用各模块的 Load 方法完成注册,最后由容器在需要时自动把依赖注入进来。

var services = new ServiceCollection();
new OrderModule().Load(services);
new UserModule().Load(services);
var provider = services.BuildServiceProvider();

using (var scope = provider.CreateScope()) {
    var orderService = scope.ServiceProvider
        .GetRequiredService<IOrderService>();
    await orderService.PlaceAsync(productId, qty);
}

注意要从「Scope 范围」里取服务,而不是直接从根容器取——这样 Scoped 服务的生命周期才正确。业务代码拿到的 orderService 已经装配好了它需要的仓储、时钟、通知等全部依赖。