依赖倒置的核心是高层与低层模块共同依赖抽象,且抽象不依赖细节,落地需通过DI容器或集中化工厂实现,而非仅用接口声明;生命周期错配、职责过载和测试滥用容器是常见误区。
依赖倒置不是“用接口代替类”就完事了
很多人以为只要把 new concreteservice() 换成 iservice service,再让类实现 iservice,就算完成了依赖倒置。其实这只是表面——真正关键在于谁来决定“哪个实现被注入”,以及这个决定是否发生在高层模块之外。
常见错误现象:ServiceA 自己在构造函数里 new 了 RepositoryB,哪怕它依赖的是 IRepository 接口;或者所有 new 都挪到了 Main() 里,但逻辑散落在各处、无法复用或测试。
- 依赖倒置的核心是:高层模块(比如业务服务)不依赖低层模块(比如数据库访问),二者都依赖抽象;而抽象不应依赖细节,细节应依赖抽象
- 真正的落地点通常是构造函数注入 + DI 容器(如
Microsoft.Extensions.DependencyInjection),而不是手工 new + 接口声明 - 如果不用容器,至少要把对象创建逻辑集中到一个地方(比如工厂类),且该工厂本身不能被业务逻辑直接引用
用 Microsoft.Extensions.DependencyInjection 做对注册和解析
注册不对,依赖倒置就只是个空壳。最常踩的坑是生命周期配错,导致状态混乱或内存泄漏。
使用场景:ASP.NET Core 默认自带该容器,但控制台或 WinForms 项目需手动初始化 ServiceCollection 并构建 ServiceProvider。
-
AddTransient<iservice serviceimpl>()</iservice>:每次请求都新建实例,适合无状态、轻量对象 -
AddScoped<iservice serviceimpl>()</iservice>:每个作用域(如一次 HTTP 请求)内单例,Web 项目常用,控制台需手动BeginScope() -
AddSingleton<iservice serviceimpl>()</iservice>:整个应用生命周期内唯一,注意线程安全和状态共享风险 - 别在
Singleton里注入Transient实例——会导致后者“升格”为单例,可能引发并发问题
示例注册:
var services = new ServiceCollection(); services.AddSingleton<ILogger>(_ => new ConsoleLogger()); services.AddScoped<IOrderService, OrderService>(); services.AddTransient<IPaymentGateway, StripeGateway>();
构造函数参数太多?说明职责还没拆干净
当一个类的构造函数要塞进 5 个 Ixxx 参数时,大概率不是 DI 写得不好,而是这个类本身违反了单一职责——它正在试图协调太多事情,成了“上帝类”。
性能影响不大,但可维护性会断崖式下跌:改一个功能要动一堆注入关系,单元测试要 mock 十几个依赖,出错时根本分不清是哪个环节掉链子。
- 先检查这个类是否同时承担了“执行”“编排”“转换”“校验”多种角色
- 把编排逻辑提到上层(比如
OrderWorkflow),当前类只做一件事(比如InventoryChecker) - 如果确实需要组合多个能力,考虑用策略模式或命令总线(
ICommandHandler<t></t>)解耦,而不是全塞进构造函数 - VS 里按 Ctrl+. 看有没有 “Extract Interface” 或 “Extract Class” 提示,别硬扛
测试时绕过 DI 容器反而更简单
写单元测试时,硬套 ServiceProvider 不仅慢,还容易掩盖设计问题。真正该测的是类的行为,不是容器能不能 resolve 出对象。
容易踩的坑:为图省事,在测试里也用 new ServiceCollection().Add...BuildServiceProvider(),结果测试跑得慢、失败难定位、还把 DI 配置逻辑复制了一份。
- 直接 new 被测类,把它的依赖用
Mock<iwhatever>().Object</iwhatever>或手写 stub 传进去 - 确保被测类构造函数参数全是抽象(接口/基类),否则说明它和具体实现耦合了
- 如果发现某个依赖很难 mock(比如操作文件系统或时间),说明它不该出现在构造函数里——应该用
Func<datetime></datetime>或IFileSystem这类更窄的契约替代
示例测试片段:
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(x => x.GetById(123)).Returns(new User("Alice"));
var service = new UserService(mockRepo.Object); // 直接构造,不碰 DI
依赖倒置最难的部分,从来不是“怎么写接口”或“怎么注册服务”,而是判断哪些东西真的该抽象、哪些只是暂时不想动所以假装抽象了。一旦开始为抽象而抽象,代码就会比原来更难懂、更难改。


















