副标题:电子面单重构踩坑:当初选抽象类做基类,现在看是最稳的决定
这是我的《Java面试·实战笔记》系列第一篇。
网上讲接口和抽象类的文章,清一色都是动物、汽车的八股案例,太脱离实际了。
这篇文章我换个方式,完全基于我线上 WMS 多平台电子面单重构的真实经历,不讲空话、不背概念。
很多小伙伴都有个通病:面试背得滚瓜烂熟,真正写业务代码、做架构设计时,彻底懵圈,分不清到底该用接口还是抽象类。
今天我就从一次真实的团队架构争议入手,带大家彻底搞懂三件事:
两者的核心本质区别、生产环境的选型标准、Java8 default 方法的真实局限。
为了方便新手入门,本文先用简化 Demo 讲透核心逻辑,文末会特意区分:Demo 是教学用的继承模板版本,我们线上真实架构是组合策略版本,杜绝大家看完和生产代码对不上的情况。
1、从一次真实的架构扯皮现场说起
之前接手公司电子面单模块重构,最棘手的就是多平台适配问题。
这套系统需要同时对接淘宝奇门、抖音、京东、拼多多、快手、小红书等十几个电商平台,渠道非常杂。
虽然平台多,但整体的业务流程是完全统一的:
参数校验 → 组装请求报文 → 平台专属签名 → 调用第三方API → 解析响应数据 → 记录操作日志
但核心的差异化细节特别多:每个平台的签名算法、接口地址、报文格式、错误码规则,全都不通用。
当时团队评审方案,直接分成了两派,谁也说服不了谁:
接口派:直接定义顶层接口就行,靠 Java8 default 方法复用通用逻辑,每个平台单独写实现类,拓展灵活,还没有单继承的束缚。
抽象类派:必须抽抽象基类!大量公共字段、通用校验、固定流程完全可以统一封装,子类只需要专注实现差异化逻辑,能少写大量重复代码。
当初争执了很久,没有标准答案。但现在这套架构线上稳定跑了大半年,日均几十万单稳稳运行,我可以很肯定地说:
只要是带业务状态、有固定执行流程的业务模板,抽象类一定是最优解。
接口看起来灵活,但根本替代不了抽象类。
2、核心区别人话解读:告别机械八股
不用记网上那些冗长的定义,只要吃透两个核心语义,日常开发、面试选型完全够用:
抽象类:is-a,定义「我是谁、我有什么、我该怎么干」
抽象类适用于高度同源、属性和流程高度重合的一类对象。
它最大的优势是能存状态、能封装通用逻辑、能固定执行骨架。
核心作用就两个:代码复用、状态复用。
接口:can-do,定义「我能干嘛、我具备什么能力」
接口是纯粹的能力契约,无状态、无专属属性,只定义行为标准。
它不关心实现类是什么、有什么属性,只约束"你必须具备某个功能",主打横向拓展、能力组合、业务解耦。
生产级核心对照表
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 设计语义 | is-a 同源同类事物,属于同一个体系 | can-do 具备某项独立能力,不限出身 |
| 状态存储 | 支持普通成员变量、可存业务状态(核心优势) | 仅静态常量,无法存储实例业务状态 |
| 构造方法 | 有,可统一初始化子类公共字段 | 无构造方法 |
| 继承/实现 | 单继承,有层级约束,血脉唯一 | 多实现,可自由组合多种能力 |
| 方法能力 | 普通方法+抽象方法,可固化业务模板 | 抽象方法+default/static,仅适合能力拓展 |
| 核心场景 | 模板方法、全局代码复用、状态统一管理 | 策略模式、横向拓展、业务解耦、多态 |
| 电子面单落地 | 请求构建基类、基础公共流程复用 | 取消/查询/重打等可选能力、平台策略解耦 |
极简总结:
抽象类拼的是血脉和家底,接口签的是能力和合同。
为了让大家更直观看懂两者的架构差异,我放一张极简业务类图,对应我们电子面单的设计思想:

核心架构思想一眼看懂:垂直继承用抽象类做统一复用,水平实现用接口做能力拓展。
3、实战落地:为什么请求构建器必须用抽象类?
回到我们的电子面单业务,十几个平台的请求构建器,有两个无法规避的核心特征:
1、存在大量公共业务状态:快递公司编码、寄收件人信息、店铺编码、扩展参数等
2、拥有一套完全固定的执行流程:参数校验、报文组装、平台签名、API调用、响应解析、日志记录
这两个需求,接口天生满足不了。
简化可运行 Demo(教学专用)
提前说明:下面的代码是继承式模板教学版本,用来帮大家理解抽象类的核心价值,和我们线上的组合式生产架构做区分。
java
abstract class AbstractWaybillBuilder {
// 所有平台共用的业务状态
protected String cpCode;
protected String sender;
protected String receiver;
public void setCpCode(String cpCode) { this.cpCode = cpCode; }
public void setSender(String sender) { this.sender = sender; }
public void setReceiver(String receiver) { this.receiver = receiver; }
// 模板方法:固定不可修改的全局流程
public final WaybillResponse build(WaybillContext ctx) {
validate();
String signedData = sign("{}");
String response = callApi(signedData);
return parseResponse(response);
}
// 公共通用逻辑:所有平台统一参数校验
protected void validate() {
if (org.springframework.util.StringUtils.isEmpty(receiver)) {
throw new RuntimeException("收件人信息不能为空");
}
System.out.println("参数校验通过");
}
// 平台差异化钩子方法,强制子类实现
protected abstract String sign(String data);
protected abstract String callApi(String signedData);
protected abstract WaybillResponse parseResponse(String response);
}
各平台具体实现示例
java
// 奇门平台实现
class QiMenWaybillBuilder extends AbstractWaybillBuilder {
@Override
protected String sign(String data) {
System.out.println("奇门SDK签名逻辑");
return "qimen_signed";
}
@Override
protected String callApi(String signedData) {
System.out.println("调用奇门官方SDK接口");
return "{\"success\":true}";
}
@Override
protected WaybillResponse parseResponse(String response) {
return new WaybillResponse(true);
}
}
// 抖音平台实现
class DouYinWaybillBuilder extends AbstractWaybillBuilder {
@Override
protected String sign(String data) {
System.out.println("抖音MD5自定义签名");
return "douyin_signed";
}
@Override
protected String callApi(String signedData) {
System.out.println("调用抖音开放平台HTTP接口");
return "{\"success\":true}";
}
@Override
protected WaybillResponse parseResponse(String response) {
return new WaybillResponse(true);
}
}
直白说:这里为什么不能用接口?
1、接口存不住 protected 业务字段,如果用接口,每个平台实现类都要重复定义一堆公共属性,代码极度冗余。
2、接口无法定义final 固定模板流程,没法强制所有平台遵守统一执行顺序,后期迭代维护一定会乱套。
3、参数校验、日志记录这类通用逻辑,用接口需要每个子类重复实现,完全违背代码复用的初衷。
这就是抽象类的核心价值:统一管理状态、固化业务流程、最大限度消除重复代码。
4、接口的正确用法:做横向能力拓展
抽象类帮我们搞定了所有平台通用的属性和主干流程。
但有一部分能力,不是所有平台都支持,属于可选的拓展功能:
- 部分平台支持面单取消
- 部分平台支持物流轨迹查询
- 部分平台支持面单重新打印
这种按需开启的可选能力,绝对不能塞进抽象父类,否则会导致大量子类出现空实现,代码极其臃肿。
这种场景下,接口就是最优解。
java
// 可取消面单能力
public interface Cancelable {
boolean cancel(String waybillCode, String reason);
}
// 可查询物流轨迹能力
public interface Queryable {
Object queryTrace(String waybillCode);
}
// 可重新打印面单能力
public interface Reprintable {
String reprint(String waybillCode);
}
比如奇门平台支持取消+重打,就多实现两个接口;抖音仅支持物流查询,就只实现查询接口。按需组合、灵活适配。
完美实现:能力按需拓展、无代码侵入、不破坏主干继承体系。
到这里,企业级开发的黄金设计范式就很清晰了:
同源复用、固定模板、状态管理 → 抽象类
差异化拓展、可选能力、业务解耦 → 接口
5、高频答疑:Java8 default 能替代抽象类吗?
这是绝大多数初级开发者都会踩的误区。
很多人觉得:Java8之后接口能写default默认方法,能复用代码,是不是就不用抽象类了?
直接给出结论:完全不能替代。
default 方法只能复用通用代码逻辑,完全无法持有、管理实例状态。
只要你的业务基类需要统一管理字段、初始化状态、固化流程模板,就必须用抽象类,接口根本扛不住。
看 JDK 源码就能印证,官方从来没有淘汰抽象类:
- AbstractList、AbstractMap 依靠抽象类缓存状态、封装集合通用模板
- AQS 依靠抽象类维护核心 state 状态、队列节点,实现并发核心逻辑
这些底层核心类,全部依赖抽象类的状态承载能力,default 接口完全替代不了。
额外避坑:default 方法菱形冲突
如果一个类同时实现两个接口,且两个接口存在同名 default 方法,直接编译报错。
这是多实现带来的天然问题,而抽象类单继承的特性,从根源上避免了这类冲突。
这里再补充面试必考的default 菱形冲突原理示意图,帮大家彻底吃透坑点:

java
interface Cancelable {
default void log() {
System.out.println("记录取消面单日志");
}
}
interface Reprintable {
default void log() {
System.out.println("记录重打面单日志");
}
}
// 编译直接报错!编译器不知道调用哪个log()
class QiMenBuilder implements Cancelable, Reprintable {
}
结论:多个接口存在同名 default 方法时,实现类必须手动重写该方法,否则编译不通过。这也是接口灵活性带来的硬性短板,抽象类完全不存在该问题。
6、生产级避坑总结

上面这 6 条都是我踩过的真实坑,全部来自电子面单线上迭代、CodeReview、线上问题复盘的真实经验,实用性拉满:
1、别为了几行重复代码强行抽抽象类
只有多个子类存在大量公共状态、统一流程时再抽取,避免过度设计,增加架构复杂度。
2、接口不要堆大量业务常量
少量配套常量可以接受,快递公司编码、渠道类型这类大批量常量,务必单独抽枚举或常量类。
3、default 方法只做工具逻辑,不碰业务模板
无状态的通用工具逻辑可以用default复用,带流程顺序、状态依赖的业务模板,必须用抽象类。
4、抽象类模板方法务必加 final
防止子类重写打乱全局固定流程,保证所有平台业务逻辑统一、规范。
5、纯能力型逻辑一律用接口,别塞父类
避免抽象父类职责臃肿,违背单一职责原则,后期极难维护。
7、重点澄清:教学 Demo 和线上生产架构的差异
很多小伙伴看完会有疑问:
我们项目里的 WaybillFetchTemplate 为什么不是抽象类继承,而是组合接口的写法?
这里统一解释,彻底消除大家的困惑:
1、文中的 Demo 是继承式模板方法,写法简单、逻辑直观,专门用来帮新手理解抽象类的核心价值;
2、我们的线上真实架构,为了适配更复杂的业务场景,做了分层优化:
-
基础设施复用(DAO、缓存、基础操作):用抽象类 DefaultBaseManager
-
平台差异化逻辑(请求、解析、异常判断):用三层策略接口解耦
-
核心业务模板流程 WaybillFetchTemplate :采用组合策略实现,而非继承
核心取舍原因:
继承式模板耦合度高,策略逻辑和子类强绑定,无法自由组合搭配;
组合式模板更灵活,支持运行时动态替换请求、解析策略,能适配各类小众渠道的特殊需求。
简单总结:简单固定的业务用继承模板,复杂多变的业务用组合模板。
下一篇我会深入我们在多平台电子面单项目中的真实架构,看看 DefaultBaseManager 为什么是抽象类、三层策略为什么是接口、模板方法为什么用组合实现。详细讲透「为什么基础设施用抽象类,平台差异用接口,模板方法用组合」的企业级落地思路。
8、最终选型结论
以下最终选型结论,面试可直接背诵。
优先用抽象类:
多个类属于同一业务体系,存在公共状态、固定执行流程,需要统一模板复用、统一管控。
优先用接口:
只需要定义能力契约,需要横向拓展、多能力组合、实现业务解耦,无业务状态需要存储。
终极一句话总结:
抽象类负责体系、状态、流程模板 ,接口负责能力、契约、多态解耦,两者互补配合,不存在谁替代谁。
大家可以思考一下:
我们电子面单的取号流程是固定五步法:构建请求 → 调用API → 异常校验 → 解析响应 → 持久化数据。
这种固定的业务流程,到底该用抽象类继承实现,还是组合接口实现?两种方式的优缺点分别是什么?
欢迎评论区交流探讨,下一篇文章我会公开线上最终的架构方案和详细的取舍理由。