依赖倒置原则的核心是依赖抽象而非具体实现:高层模块与低层模块均应依赖接口或抽象类,抽象不依赖细节,细节依赖抽象,从而实现解耦与灵活替换。

接口编程思想的核心,就是让代码依赖契约而非具体实现。依赖倒置原则(DIP)不是抽象概念,而是可落地的设计实践——它通过接口把高层逻辑和底层细节隔开,让系统真正具备“换而不改”的能力。
定义稳定抽象层:先写接口,再写实现
接口不是对已有实现的总结,而是由业务需求反推出来的契约。比如订单服务需要“发通知”,就该先定义 NotifierInterface,明确 notify(string $recipient, string $content) 这个行为,而不是等写完邮件类再抽接口。
- 接口名体现意图(如
PaymentGateway),不暴露技术细节(避免叫PayPalAdapter) - 方法签名聚焦“做什么”,不规定“怎么做”(不包含 SMTP 配置、API key 等)
- 一个接口只表达一类职责(发送、存储、验证),避免大而全的“万能接口”
高层模块只依赖接口类型
业务类(如 OrderService)的构造函数参数、属性声明、方法参数,类型必须是接口,不能是具体类。
- 错误写法:
private SmtpMailer $mailer; - 正确写法:
private MailerInterface $mailer; - IDE 和静态分析工具能立刻发现违规,这是最直接的约束手段
用依赖注入把实现“塞进来”
谁来决定用哪个实现?不是业务类自己 new,而是外部容器或工厂在运行时注入。
- 构造器注入最常用:对象一创建就持有明确依赖,状态不可变
- 方法注入适合临时、可选依赖(如日志上下文)
- 避免在类内部调用
new SmtpMailer()或静态工厂方法,那等于绕过 DIP
低层模块实现接口,不反向引用高层
具体实现(如 SendGridMailer)只关注如何完成接口承诺,不感知谁在用它,也不引入业务逻辑相关代码。
- 实现类里不能出现
OrderService、UserService等高层类名 - 配置、第三方 SDK 调用都封装在实现内部,对外透明
- 测试时可轻松替换为
FakeMailer或InMemoryMailer,无需网络或真实服务
不复杂但容易忽略:DIP 的价值不在写接口那一刻,而在第一次需要替换实现时——比如从本地邮件切到云服务,或从 MySQL 换成 Redis 缓存,你只需新增一个实现类,改一行配置,其余代码完全不动。











