接口是模块间约定行为的“语言协议”,只声明能力不绑定实现,通过纯粹契约实现解耦、多实现切换、Mock测试及结构型模式支撑。

接口不是代码模板,是模块之间约定行为的“语言协议”。它不规定怎么干,只明确必须能做什么——这种约束力,才是解耦真正的起点。
接口即契约:只声明能力,不绑定实现
一个接口只包含方法签名和常量,比如 PaymentProcessor 里只有 process(double amount) 和 refund(String txId)。调用方只依赖这个契约,完全不知道背后是支付宝、银联还是测试模拟器。只要返回值符合约定(成功/失败),业务流程就能继续推进。
这种“能力可见、实现隐藏”的设计带来三个直接效果:
- 上层逻辑无需修改,就能切换底层支付渠道
- 新增一种支付方式,只需写新类实现该接口,不碰原有代码
- 单元测试时可快速构造 Mock 实现,绕过真实网络或加密逻辑
为什么抽象类不能替代接口做契约
抽象类容易悄悄引入状态(如 protected $logLevel)或默认行为(如 formatMessage()),导致子类被隐式绑定。而接口强制只声明方法,连参数名都不能含糊——log(string $message) 就是唯一要求。
这种纯粹性支撑了关键工程能力:
- Java/C# 中一个类可实现多个接口,但只能继承一个父类;多角色支持靠的是接口,不是抽象类
- 依赖注入容器(如 Spring)能安全装配任意
ILogger实现,不会因父类字段冲突失败 - 测试时直接 new 一个空实现类即可,不用处理抽象类构造函数里的初始化逻辑
命名与组织要暴露业务意图,而非技术细节
接口名不该泄露实现方式。像 FileBasedUserRepository 这种名字,一旦要迁移到 Redis,所有引用都得改;换成 UserRepository,调用方只认 save(User $user) 和 findById(int $id): ?User 这两个语义,背后换数据库、换序列化格式都不影响。
好接口的命名和位置有明确信号:
- 用动名词表达能力,如 OrderValidator、InventoryLocker,不加 Interface 后缀
- 包路径体现契约属性,例如
com.example.order.contract,不混进impl或service - 接口内只放真正全局不变的常量,如
MAX_RETRY = 3,不放配置项或运行时状态标记
结构型模式靠统一接口实现动态替换
装饰器、适配器、代理这些模式能生效,前提是被包装对象和包装对象实现了同一个接口。比如 CompressionDecorator 必须实现 TextProcessor,否则调用方的 $processor->process($text) 就会报错。
这里没有魔法:客户端代码只认接口类型,不认具体类名。所以加日志、加重试、加缓存,不是去改原始类,而是提供一个新类,也实现那个接口,再通过 DI 容器或工厂注入进去。
当发现某个实现类需要加新方法才能工作,先问一句:“这是所有实现都必须提供的能力吗?” 如果只是某一个实现的特需逻辑,说明它不该属于这个契约,该拆新接口或用策略模式隔离。

















