依赖倒置原则核心是高层模块不依赖低层模块,二者都应依赖抽象;抽象不依赖细节,细节依赖抽象。关键在于依赖关系由外部容器(如.net的iservicecollection)控制,而非业务代码中硬编码new实现类,否则仅声明接口仍属伪dip。

依赖倒置不是加个 interface 就完事
很多人在 OrderService 里写个 INotificationService 参数,再让 EmailSender 实现它,就以为 DIP 成了。其实这只是契约声明——真正倒置的是“谁决定用哪个实现”。如果 Program.cs 里没注册、或业务代码里还藏着 new EmailSender(),那依赖仍在内部,高层模块依然被细节绑架。
关键判断点:打开你的构造函数,再看整个调用链路——有没有任何地方硬编码了具体类型?有没有在业务逻辑里 new 了实现类?只要有一处,DIP 就没成立。
构造函数注入必须配合 DI 容器注册
只改构造函数是半截子工程。.NET 的 IServiceCollection 才是把抽象和实现“解绑”的枢纽。不注册,运行时根本没法解析依赖,会直接抛出 InvalidOperationException: No service for type 'INotificationService' has been registered.
-
services.AddSingleton<inotificationservice emailsender>()</inotificationservice>:全局单例,适合无状态、线程安全的实现 -
services.AddScoped<inotificationservice smssender>()</inotificationservice>:每次 HTTP 请求新建一次,适合带 HttpContext 或数据库上下文的场景 -
services.AddTransient<inotificationservice dingtalksender>()</inotificationservice>:每次解析都新建,适合轻量、无共享状态的服务
别图省事全用 AddSingleton——比如把 DbContext 注册成单例,会导致并发写入异常或数据污染;也别滥用 AddTransient,频繁 new 对象可能触发 GC 压力。
工厂模式是动态决策时的合法出口
当实现类不能在启动时确定(比如根据用户配置选邮件还是钉钉),强行塞进 DI 容器注册表反而僵化。这时该用工厂,而不是把 IServiceProvider 塞进业务类里手动 Resolve——后者是服务定位反模式,会让测试变难、生命周期失控。
正确做法:
- 定义
INotificationFactory接口,含CreateFor(User user)方法 - 工厂实现类内部可以
new具体类型,这是允许的——因为工厂本就是低层角色 - 业务类仍只依赖
INotificationFactory,不碰任何具体类名
注意:工厂接口本身也要注册进 DI,比如 services.AddSingleton<inotificationfactory configbasednotificationfactory>()</inotificationfactory>,否则构造函数注入照样失败。
抽象设计失败比注入失败更难排查
常见坑是接口越写越大:把 SendEmail、SendSms、RetryWhenFailed 全塞进一个 INotificationService。结果换短信服务商时,还得改接口、改所有实现、改所有调用方——这已经违背了“抽象比细节稳定”的前提。
判断接口是否健康,就看它能不能一句话说清职责:
- ✅
SendAsync(Order order):聚焦“发通知”这个业务动作 - ❌
SendEmailBySmtp(string host, int port, string from):暴露技术路径,绑定协议细节 - ❌
ValidateAndSend(object data):职责爆炸,违反单一职责
真正的难点从来不在怎么注入,而在于抽象是否经得起变化。接口一旦发布,修改成本远高于调整 DI 注册代码——所以花时间推敲契约,比急着写 new 和 Register 重要得多。











