接口是类在设计阶段对能力的强制承诺,命名需体现能力而非角色,方法应无实现细节,粒度适中且依赖注入必须用接口类型。

接口不是为了多态,是为了让类主动“签合同”
面向接口编程的本质,不是为了后期能换实现类,而是让每个类在设计阶段就明确自己承诺提供什么能力——这个“承诺”就是接口。一旦类 implements 某个接口,它就必须实现所有方法,编译器会强制你履约,而不是靠文档或人肉约定。
常见错误现象:NullPointerException 频发、新增功能时反复修改已有类、同事改了 UserService 却没通知调用方,结果线上报 MethodNotFoundException。
- 接口名必须是能力描述,不是角色名:用
PaymentProcessor,别用PaymentService - 接口方法不能带具体实现细节:禁止在接口里写
default方法处理支付超时逻辑,那是实现类该管的事 - 一个接口只表达一种能力:把
sendEmail()和generateReport()塞进同一个NotificationService接口,等于让短信模块也得实现报表生成
依赖注入时,变量类型必须是接口,不是实现类
哪怕你当前只有一个实现,变量声明也得用接口。否则 Spring 的 @Autowired 或手动传参时看似能跑通,但后续加第二个实现(比如从 AlipayPayment 扩展出 WechatPayment)就会卡在硬编码上。
使用场景:Spring Boot 项目中 Controller 调用 Service、Service 内部调用 Repository、测试时 mock 依赖。
立即学习“Java免费学习笔记(深入)”;
- 错的写法:
private AlipayPayment payment = new AlipayPayment(); - 对的写法:
private PaymentProcessor payment;,然后由容器注入具体实现 - 如果非要用
new(比如工具类),至少把构造参数改成接口:public OrderService(PaymentProcessor payment),别写成AlipayPayment
接口粒度太粗或太细都会拖慢扩展速度
接口太大,实现类被迫实现一堆用不到的方法,违反接口隔离原则;接口太小,新增一个业务动作就得新增三四个接口,调用方要同时持有多个引用,耦合反而更重。
性能影响:接口方法过多不会影响运行时性能,但会让 IDE 自动补全变卡、git diff 更难读、新人理解成本陡增。
- 判断标准:看是否“总是一起被调用”。如果
createOrder()和cancelOrder()几乎总在同一个业务分支里出现,它们可以共存于OrderManager接口;但notifyUser()出现在 80% 的流程里,就该单独拆成Notifier - 别提前抽象:刚写完第一个
FileStorage实现时,别急着定义Storage接口,等第二个实现(如S3Storage)出现再抽,否则大概率抽错 - 兼容性提示:JDK 8+ 允许接口加
default方法,但仅限于不改变语义的微小增强(比如给log()加个默认时间戳),千万别用它来“修复”设计缺陷
测试时用接口写 mock,比用实现类写单元测试靠谱得多
用 Mockito.mock(EmailService.class) 是白忙活——你 mock 的是具体类,而生产代码依赖的是 EmailSender 接口。mock 错类型,@InjectMocks 根本不会把 mock 注入进去,测试通过纯属巧合。
容易踩的坑:用 PowerMock 去 mock 静态方法或私有方法,本质是绕过接口契约,等于在测试里埋下“这个类其实不守合同”的伏笔。
- 正确做法:
Mockito.mock(EmailSender.class),然后在被测类里确保字段类型是EmailSender - 验证是否真用了接口:检查被测类的字段声明、构造函数参数、setter 方法签名,三者都得是接口类型
- 如果发现某个类只有实现没有接口(比如
JsonUtils工具类),先别急着 mock,考虑把它包装成JsonSerializer接口再引入——工具类本身不扩展,但它的使用方式需要可替换
真正卡住扩展的,从来不是技术选型或框架限制,而是某次“就加一个小功能”时,随手把新逻辑塞进了已有接口,或者在 service 层直接 new 了一个实现类。接口契约一旦松动,后面所有人只能跟着惯性往下写。











