应根据业务变化方向选择:若变化围绕同类事物扩展(如支付方式、图形导出),选抽象类以复用状态与逻辑;若能力需自由拼装(如支付+通知+风控),或跨异构类型统一约束(如双因素认证),则选接口;实际中常协同使用,抽象类定主干流程,接口注入可变策略。

选抽象类还是接口,关键不是看语法差异,而是看业务变化的方向。用错一个,后面加功能就得大改结构;用对了,新需求加进去几乎不用动老代码。
看变化是否围绕“同一类事物”展开
如果新增功能始终属于同一种东西的延伸,比如所有支付方式都要支持分账回调、所有图形都要导出 SVG、所有动物都要冬眠,说明分类没变,只是同类行为在扩展——这时候抽象类更合适。
- 抽象类能自然携带共用状态(如订单号、创建时间)和通用逻辑(如统一日志、参数校验)
- 子类继承后只写差异部分,不用重复初始化或流程模板
- 后续要给所有子类加个默认方法(比如 logOperation()),直接在抽象类里补上,自动生效
看变化是否来自“能力自由拼装”
如果业务越来越灵活:今天订单配微信支付+短信通知,明天换支付宝+邮件+风控拦截,后天又加汇率转换——这些能力彼此独立、可插拔、不绑定具体类型,那就该用接口。
- 一个类可以同时实现 Payable、Notifiable、RiskCheckable,互不影响
- 新加一种能力(比如 Refundable),只影响需要退款的类,其他类完全无感
- Java 8+ 的 default 方法还能提供基础实现,避免重复写同样逻辑
看未来是否要约束“谁可以拥有这个能力”
如果这个能力要被完全不同类型的模块使用,比如“所有能登录的模块都必须支持双因素认证”,而登录方可能是 Web 页面、小程序、IoT 设备——它们根本不是同类事物,硬塞进一个抽象类会破坏语义。这时接口更自然。
- 接口不关心“你是什么”,只规定“你能做什么”
- 不同技术栈、不同团队开发的模块,只要实现同一接口,就能被统一调度或注入
- 测试时也容易 mock,比如用 Mockito.mock(Exportable.class) 快速构造行为桩
实际项目中常协同使用
大型系统往往既需要统一骨架,又需灵活扩展能力。典型做法是:用抽象类封装主干流程,用接口注入策略。
- 比如 AbstractOrderService 定义下单主流程(校验→锁库存→生成单据→发消息),而把“库存锁定方式”抽为 InventoryLockStrategy 接口,由 RedisLock 或 DB Lock 实现类动态注入
- Spring 框架大量采用这种模式:JdbcDaoSupport 是抽象类,提供 JdbcTemplate 封装;PlatformTransactionManager 是接口,适配 JDBC、JTA、Reactive 等多种事务机制











