在.NET项目规模变大之后,把系统拆成多个模块是常见做法。订单、权限、通知等功能各自成块,本来是为了让团队并行开发,但很多代码里模块之间还是互相直接实例化,结果主工程一改底层类,所有引用方都编译不过。依赖注入容器把对象的创建和绑定从业务代码里抽出来,模块只要声明自己需要什么接口,由宿主程序在启动时把实现填进去,这样新增或替换模块都不会波及他人。

为什么模块化开发需要依赖注入
模块化开发的核心目标是高内聚、低耦合。如果模块A为了发消息直接写了new EmailSender(),那它就和具体的邮件实现绑死了。哪天要换成短信或者消息队列,只能去改A的源码。依赖注入要求A只依赖IMessageSender这样的接口,实现类由外部注册到容器,A通过构造函数拿到实例,自身完全不关心来源。
在.NET中,这种机制还能配合程序集扫描做自动注册。每个模块可以暴露一个IModule接口,宿主在启动时反射加载所有dll,调用模块的注册方法,把服务塞进同一个IServiceCollection。这样主工程不需要引用模块的具体类型,编译期解耦就做到了,运行时又能正常解析。
定义模块与抽象接口
先给出一个最基础的抽象。模块之间通信靠接口,不靠具体类。下面代码展示了一个消息发送接口和对应的模块注册契约。
// 消息发送抽象,供各模块依赖
public interface IMessageSender
{
void Send(string to, string content);
}
// 模块统一注册契约
public interface IModule
{
void RegisterServices(IServiceCollection services);
}
订单模块可以实现自己的发送器,并在注册方法里把自己加进容器。注意这里没有引用宿主的任何具体类型,只用了.NET自带的IServiceCollection,因此订单模块可以独立编译成类库。
// 订单模块内的实现
public class OrderModule : IModule
{
public void RegisterServices(IServiceCollection services)
{
services.AddScoped<IMessageSender, OrderEmailSender>();
services.AddScoped<IOrderService, OrderService>();
}
}
public class OrderEmailSender : IMessageSender
{
public void Send(string to, string content)
{
// 实际发送逻辑
}
}
宿主程序如何装载模块
宿主在启动时要扫描指定目录下的程序集,找到所有实现了IModule的类型并调用注册。下面代码演示了基于反射的自动装载,实际项目可改为从配置文件读取模块清单。
public static void LoadModules(IServiceCollection services, string pluginPath)
{
foreach (var dll in Directory.GetFiles(pluginPath, "*.dll"))
{
var asm = Assembly.LoadFrom(dll);
foreach (var type in asm.GetTypes())
{
if (typeof(IModule).IsAssignableFrom(type) && !type.IsAbstract)
{
var module = (IModule)Activator.CreateInstance(type);
module.RegisterServices(services);
}
}
}
}
这种写法让新增模块变成单纯放一个dll到目录,宿主无需重新编译。生命周期上推荐模块内部服务用Scoped,在一次请求内复用;而像配置读取器可用Singleton减少重复开销。
如果模块之间出现循环依赖,比如订单模块要用用户模块的服务,用户模块又反调订单,容器在构建时会抛异常。此时应抽出二者共用的第三个接口放到公共契约层,打破直接引用。
作用域与生命周期控制
依赖注入容器管理三种基本生命周期:Transient每次解析都新建,Scoped在同一个作用域内复用,Singleton全局唯一。模块化系统常跑在Web框架里,一个HTTP请求对应一个Scope,因此跨模块共享的数据库上下文应注册为Scoped,避免线程错乱。
// 在模块注册时合理选择生命周期 services.AddScoped<IOrderService, OrderService>(); services.AddSingleton<IConfigProvider, ConfigProvider>();
如果某模块误把有状态的服务注册成Singleton,高并发下会出现数据串号。建议在模块文档中标明每个服务的并发安全性,宿主也可以在单元测试里启动容器做解析验证。
优缺点与实践建议
使用依赖注入做模块化,最大优点是替换实现零成本,测试时塞个假实现就能跑通流程。缺点是容器黑盒化,新人不易理清对象从哪来,需要配合良好的接口命名和模块清单。
| 方式 | 耦合度 | 维护成本 |
|---|---|---|
| 直接new | 高 | 改一处全盘重编 |
| 依赖注入+接口 | 低 | 仅改实现类 |
建议把接口定义在独立的契约类库,各模块和宿主都引用它,具体实现留在模块内。配合自动扫描,就能在.NET里搭出易扩展的插件化系统。
dependency_injectionmodular_developmentNET修改时间:2026-08-11 21:51:40