副标题:为什么基础设施用抽象类,平台差异用接口,模板方法用组合?
📌「Java面试·实战笔记」系列第 2 篇
上一篇我用大白话比喻,帮大家彻底搞懂了接口和抽象类的基础区别,没看过的朋友可以跳转第一篇:【接口和抽象类有什么区别?电子面单多平台对接实战】。但说实话,背概念没用,落地到项目才是真本事。
今天这篇,我直接拿线上真实落地的多平台电子面单生产架构跟大家拆解,全程无虚构代码、无空洞八股文。
重点讲透三个核心问题:
项目里的DefaultBaseManager,为啥一定要用抽象类?
多平台差异化逻辑,为啥拆成三层策略接口,不用抽象类?
模板方法模式,为啥抛弃传统继承,改用组合方式实现?
看完这篇,你以后面试被问“接口和抽象类怎么选型”,再也不用死记硬背,直接套生产落地逻辑回答,“吊打”大部分八股文选手。
文章目录
- 副标题:为什么基础设施用抽象类,平台差异用接口,模板方法用组合?
- 1 先复盘:别停留在“背概念”,要落地到“写代码”
- 2 真实架构全景速览
- 3 抽象类:专门用来做「基础设施复用」
- 3.1 生产真实代码:DefaultBaseManager
- 3.2 重点:为啥这里必须用抽象类?接口不行吗?
- 3.3 JDK源码佐证:大佬也是这么写的
- 4 接口:专门用来「隔离平台业务差异」
- 4.1 生产核心设计:三层策略接口
- 4.2 为啥这里用接口,不用抽象类?
- 4.3 延伸:API调度也用接口统一
- 5 重点进阶:模板方法为啥用组合,不用继承?
- 5.1 传统模板方法的坑
- 5.2 生产真实组合模板代码
- 5.3 为啥组合比继承更香?4个实战理由
- 5.4 极简可运行Demo,秒懂组合模板
- 6 终极选型总结(面试直接套)
- 7 生产踩坑真实教训(避坑必看)
- 7.1 策略路由维度不足,导致策略覆盖
- 7.2 抽象类方法权限乱用
- 7.4 传统继承模板的耦合致命坑
- 7.3 接口default方法菱形冲突
- 8 延伸思考:新增支付宝平台,该怎么改代码?
- 参考答案
- 9 评论区面试挑战
- 10 全文总结(面试绝杀一句话)
- 🔜 本系列完整连载阅读顺序
- 已更新内容
- 待更新连载
1 先复盘:别停留在“背概念”,要落地到“写代码”
上一篇我们总结了最基础的选型逻辑,这里快速回顾一下:
抽象类:代表is-a父子关系,适合共享状态、通用模板、公共基础设施能力
接口:代表can-do能力契约,适合解耦、多态、隔离业务差异
之前的示例是简化版demo,今天咱们直接上真实业务架构——多平台电子面单系统。
这个系统要对接奇门、抖音、京东、支付宝等十几家电商平台,每个平台的请求、响应、异常规则全都不一样,非常适合用来讲接口、抽象类、组合模式的精准选型。
2 真实架构全景速览
先看精简后的分层架构图,一眼看懂整体设计:
整个架构的核心规律,一句话总结:
上层通用基础设施,用抽象类;下层平台差异化业务,用接口;固定流程模板,用组合实现。
下面逐层拆开讲,每一个选型都告诉你“为什么这么写,这么写的好处是什么”。
3 抽象类:专门用来做「基础设施复用」
3.1 生产真实代码:DefaultBaseManager
我们项目里所有的业务Service,全部统一继承DefaultBaseManager:
不管是订单修改服务、面单取号服务,都能直接复用父类的公共能力,不用每个类重复定义。
publicclassTocOrderExpressModifyServiceextendsDefaultBaseManager{publicBatchResulteditTocLogistic(List<Long>ids,Stringexpress,StringproductCode){// 直接调用父类事务方法,不用自己写事务逻辑TransactionStatusstatus=beginTxPropagationRequiresNew();// ... 业务逻辑commitTransaction(status);}}publicclassWaybillFetchServiceextendsDefaultBaseManager{publicbooleanfetchWaybill(TocWmsPickTicketticket,intexsitJianNum,StringproductCode){// 直接用父类的DAO、日志对象ctx.getExt().put(WaybillContext.KEY_COMMON_DAO,this.commonDao);logger.info("开始取号...");}}而这个抽象父类,统一封装了所有业务服务的通用基础设施:
数据库操作对象commonDao
统一日志对象logger
全套事务管理方法:开启事务、提交事务、回滚事务
3.2 重点:为啥这里必须用抽象类?接口不行吗?
答案:完全不行,语法和设计层面都不支持。
我给大家讲三个最核心的原因,面试直接背这三点就够了:
① 接口不能定义实例变量,无法共享状态
commonDao 是需要Spring注入的实例对象,每个子类都要共用。但接口里的变量默认是static final 常量,根本不能定义可变的实例属性。
// 编译直接报错!接口做不到状态共享publicinterfaceBaseManager{CommonDaocommonDao;}② 抽象类可以写构造方法,统一管理依赖注入
所有子类的DAO、日志依赖,都可以在抽象父类统一初始化,子类直接复用,不用重复注入。接口没有构造方法,完全做不到。
③ 语义是标准的 is-a 关系
所有业务Service,本质就是一个基础业务管理器,完全符合抽象类的父子继承关系,用继承天然合理。
最终结论:
只要需要共享实例状态、封装通用基础设施、统一初始化依赖,优先用抽象类,接口替代不了。(面试直接背诵)
3.3 JDK源码佐证:大佬也是这么写的
不光我们业务项目,JDK底层核心源码全是这个思路:
AbstractList:持有modCount状态字段,封装集合通用逻辑
AbstractMap:缓存keySet、values,提供Map通用骨架方法
AQS抽象队列同步器:持有state、head、tail核心状态,是锁机制的基础父类
这些核心抽象类,全部都是封装通用能力、共享状态,和我们的DefaultBaseManager设计思路一模一样。
4 接口:专门用来「隔离平台业务差异」
4.1 生产核心设计:三层策略接口
多平台对接最大的痛点:每个平台的请求、响应、异常判断逻辑全都不一样。
如果写一堆if-else判断平台,代码会乱到没法维护。所以我们直接抽了三层独立接口,把所有平台差异彻底剥离:
// 1. 请求构建策略:不同平台拼参规则不同publicinterfaceRequestStrategy{ObjectbuildRequest(WaybillContextctx);}// 2. 响应解析策略:不同平台返回报文格式不同publicinterfaceParseStrategy{List<TocPickTicketWayBillDetailsNew>parseResponse(Stringresponse,WaybillContextctx);}// 3. 异常判断策略:不同平台报错字段、提示文案不同publicinterfaceExceptionStrategy{booleanisBusinessSuccess(Stringresponse);StringextractErrorMsg(Stringresponse);}然后每个平台单独实现这三套接口,各自写自己的差异化逻辑:
// 奇门平台专属实现publicclassQiMenRequestStrategyimplementsRequestStrategy{@OverridepublicObjectbuildRequest(WaybillContextctx){returnQiMenWaybillBuilder.buildRequest(ctx);}}// 抖音平台专属实现publicclassDouYinRequestStrategyimplementsRequestStrategy{@OverridepublicObjectbuildRequest(WaybillContextctx){returnbuildDouYinJson(ctx);}}4.2 为啥这里用接口,不用抽象类?
四个通俗好懂的理由:
① 语义是can-do能力,不是is-a父子关系
抖音、奇门的请求构建类,不需要继承某个父类,只需要具备“构建请求”的能力就行,这就是接口的核心语义:能力契约。
② 策略类无状态,不需要共享属性
所有策略实现类都没有自定义成员变量,所有参数都通过上下文传入,无状态的业务能力,用接口最轻量化。
③ 支持灵活多实现,扩展性拉满
一个平台后续如果新增取消、重打等能力,可以直接多实现多个接口,抽象类只能单继承,完全没这么灵活。
④ 完美贴合开闭原则
新增支付宝、拼多多等新平台,只需要新增实现类,不用改一行旧代码。如果用抽象类,很容易需要修改父类逻辑,破坏开闭原则。
4.3 延伸:API调度也用接口统一
我们的API调用层,同样用接口做统一约束:
publicinterfaceRequestHandler{Stringhandle(WaybillContextctx,Objectrequest,StringtraceId)throwsIOException;}奇门、抖音、通用HTTP、OAuth2鉴权,各自实现这个接口,工厂根据平台编码+子渠道复合维度路由,精准匹配对应处理器,彻底避免策略覆盖bug。
5 重点进阶:模板方法为啥用组合,不用继承?
5.1 传统模板方法的坑
很多人学模板方法模式,只知道用抽象类+子类继承重写。
但在我们的生产架构里,完全抛弃了这种写法,改用组合式模板 WaybillFetchTemplate。
5.2 生产真实组合模板代码
publicclassWaybillFetchTemplate{// 直接注入三层策略接口,组合复用,不用继承privatefinalRequestStrategyrequestStrategy;privatefinalParseStrategyparseStrategy;privatefinalExceptionStrategyexceptionStrategy;privatefinalApiInvokerapiInvoker;privatefinalWaybillPersistencepersistence;// 构造函数注入所有策略,灵活组装publicWaybillFetchTemplate(RequestStrategyreq,ParseStrategyparse,ExceptionStrategyex,ApiInvokerinvoker,WaybillPersistencepersist){this.requestStrategy=req;this.parseStrategy=parse;this.exceptionStrategy=ex;this.apiInvoker=invoker;this.persistence=persist;}// 固定的核心流程骨架,永不修改publicbooleanexecute(WaybillContextctx){StringtraceId=ctx.getTicket().getCode()+"_"+System.currentTimeMillis();try{// 1. 差异化:构建请求Objectrequest=requestStrategy.buildRequest(ctx);// 2. 通用:调用APIStringresponse=apiInvoker.invoke(ctx,request,traceId);// 3. 差异化:业务异常判断if(!exceptionStrategy.isBusinessSuccess(response)){StringerrMsg=exceptionStrategy.extractErrorMsg(response);markException(ctx.getTicket(),errMsg);returnfalse;}// 4. 差异化:解析响应List<Detail>details=parseStrategy.parseResponse(response,ctx);if(details==null||details.isEmpty()){markException(ctx.getTicket(),"未获取到运单号");returnfalse;}// 5. 通用:持久化数据booleanisFirst=(ctx.getExsitJianNum()==0);persistence.saveAndBind(ctx.getTicket(),details,isFirst);returntrue;}catch(Exceptione){markException(ctx.getTicket(),"系统异常: "+e.getMessage());returnfalse;}}}5.3 为啥组合比继承更香?4个实战理由
① 差异逻辑已经通过接口剥离完毕,没必要再继承
平台的所有差异化步骤,已经交给三层策略接口实现了。如果再搞一个抽象模板类让子类继承重写,纯属重复造轮子,代码冗余。
② 彻底避免类爆炸
如果用继承,10个平台就要写10个模板子类。用组合,一个模板类适配所有平台,只需要注入不同策略即可。
③ 运行时动态灵活
继承的类关系编译期就固定死了,组合可以在运行时通过工厂动态替换、组合策略,适配不同平台、不同渠道。
④ 测试极其简单
可以直接Mock策略接口,单独测试模板核心流程,不用启动Spring容器,不用依赖继承关系。
⑤ 严格遵循单一职责原则,代码职责高度拆分
通过组合模式将「固定流程骨架」和「差异化业务逻辑」完全拆分:WaybillFetchTemplate 只专注负责流程编排、通用逻辑处理、异常兜底,各司其职;而三层策略接口只专注各自的单一差异化能力,完全贴合单一职责原则。反观传统继承式模板方法,所有逻辑集中在父类和子类中,极易导致模板类职责臃肿、代码耦合严重,后续维护迭代成本极高。
面试金句:
模板方法的核心是“固定流程、差异化步骤”。传统继承式适合无前置解耦的场景,而我们项目中差异逻辑已通过接口独立,用组合实现更轻量、更灵活、符合单一职责。
5.4 极简可运行Demo,秒懂组合模板
下面这段代码可以直接复制运行,帮你直观理解核心思想:
publicclassTemplateDemo{// 差异化策略接口interfaceStrategy{Stringexecute();}// 固定模板类(组合策略,不继承)staticclassTemplate{privatefinalStrategystrategy;publicTemplate(Strategys){this.strategy=s;}publicvoidrun(){System.out.println("1. 公共前置步骤");System.out.println("2. "+strategy.execute());System.out.println("3. 公共后置步骤");}}publicstaticvoidmain(String[]args){// 平台A差异化逻辑newTemplate(()->"平台A:淘宝SDK签名 + 调用奇门API").run();// 平台B差异化逻辑newTemplate(()->"平台B:MD5签名 + 调用抖音API").run();}}运行结果:
1. 公共前置步骤 2. 平台A:淘宝SDK签名 + 调用奇门API 3. 公共后置步骤 1. 公共前置步骤 2. 平台B:MD5签名 + 调用抖音API 3. 公共后置步骤核心:流程骨架固定,差异化步骤可随意替换,这就是组合模板的精髓。
6 终极选型总结(面试直接套)
| 业务场景 | 选型方案 | 生产案例 |
|---|---|---|
| 封装DAO、日志、事务等通用基础设施 | 抽象类 | DefaultBaseManager |
| 隔离各平台差异化业务逻辑 | 接口 | 三层策略接口 |
| 统一API调用能力契约 | 接口 | RequestHandler |
| 固定流程骨架,步骤已通过接口解耦 | 组合类 | WaybillFetchTemplate |
三分钟选型口诀:
有共享状态、通用基础设施 →抽象类
无状态、纯能力契约、业务差异解耦 →接口
流程固定、步骤可替换,且已接口化 →组合优先于继承
7 生产踩坑真实教训(避坑必看)
7.1 策略路由维度不足,导致策略覆盖
早期我们只用platFormCode单维度缓存策略,导致抖音普通、抖音代发策略互相覆盖:
requestMap.put("DY",newDouYinRequestStrategy());// 抖音普通requestMap.put("DY",newDouYinDaiFaRequestStrategy());// 直接覆盖上面的策略!教训:同平台多子渠道,必须用platFormCode + 子渠道标识复合键路由,和ApiInvoker保持统一。
7.2 抽象类方法权限乱用
公共通用方法如果定义为protected,子类可以随意重写,容易绕过核心校验逻辑。
规范:公共固定流程用private/final保护,只暴露必须实现的抽象方法。
7.4 传统继承模板的耦合致命坑
这也是我们彻底抛弃继承式模板方法的核心踩坑经验:早期项目曾使用抽象模板类+子类继承的方式实现流程编排,产生了严重的继承耦合问题。
一方面,子类强依赖父类的实现逻辑,一旦父类修改通用流程、调整参数校验规则、优化异常兜底逻辑,所有继承该模板的平台子类都会被动受影响,极易引发全平台隐性bug,牵一发而动全身,线上风险极高;另一方面,子类可以随意重写父类的通用固定方法,部分开发不规范的重写,会破坏模板预设的核心流程逻辑,导致不同平台的执行流程不统一,排查问题难度极大。
核心教训:继承是静态强耦合关系,会大幅提升代码维护风险;而组合是弱依赖关系,通过注入策略实现能力组装,完全解耦核心流程与差异化逻辑,从根源规避继承耦合问题。
7.3 接口default方法菱形冲突
多个接口定义同名default方法,实现类必须强制重写,否则编译报错。开发中尽量规避同名默认方法,减少冲突。
8 延伸思考:新增支付宝平台,该怎么改代码?
大家可以先自己思考一下,再看答案,加深理解!
👋先独立思考,再往下核对答案
参考答案
需要新增的代码(零侵入旧代码):
支付宝请求策略:ZFBRequestStrategy 实现 RequestStrategy
支付宝解析策略:ZFBParseStrategy 实现 ParseStrategy
支付宝异常策略:ZFBExceptionStrategy 实现 ExceptionStrategy
支付宝API处理器:ZFBHandler 实现 RequestHandler(或复用通用SimpleHttpHandler)
需要改动的代码:
策略工厂注册新策略
API调用器注册新处理器
完全不用动的核心代码:
模板流程、服务层逻辑全部零改动!
这就是开闭原则的终极魅力:新增功能只加代码,不改旧逻辑。
9 评论区面试挑战
面试原题:
DefaultBaseManager用抽象类,RequestStrategy用接口,请从设计语义和语法限制两个角度说明选型原因?
标准答案:
设计语义:DefaultBaseManager是is-a父子关系,是所有服务的基础父类;RequestStrategy是can-do能力契约,只约束“能构建请求”的行为,无继承关系。
语法限制:DefaultBaseManager需要持有DAO、日志等实例状态,接口无法定义实例变量;策略类无状态、纯行为定义,接口完全适配。
10 全文总结(面试绝杀一句话)
在我们多平台电子面单架构中,抽象类负责基础设施状态复用,接口负责平台业务差异解耦,模板方法通过组合实现灵活的流程编排,三者配合完美实现了“新增平台不改动核心代码”的开闭原则。
🔜 本系列完整连载阅读顺序
已更新内容
- 第1篇:《接口与抽象类到底怎么分?通俗比喻+基础选型框架》
👉 上篇直达:接口和抽象类有什么区别?电子面单多平台对接实战
内容概览:用快递家族生活化比喻讲清接口、抽象类底层语义,搭建基础选型判断框架,通过简化面单代码区分is-a与can-do关系,扫清概念层面所有八股误区。
- 第2篇:本文《从多平台电子面单架构看接口与抽象类的真实选型》
内容概览:基于线上电子面单生产架构落地拆解,结合DefaultBaseManager抽象基类、三层策略接口、组合式模板,给出可直接套用的工程选型标准,附带真实踩坑与面试标准答案。
📌 全文核心知识点速览(面试极速复盘)
- 抽象类核心选型场景:需共享实例状态、封装通用基础设施、统一依赖注入,适配is-a父子关系,不可被接口替代。
- 接口核心选型场景:无状态纯能力契约、业务差异解耦、多灵活扩展,适配can-do能力关系,贴合开闭原则。
- 组合模板核心优势:规避类爆炸、运行时动态适配、低耦合易测试、贴合单一职责,优于传统继承模板。
- 核心架构精髓:基础设施靠抽象类复用、业务差异靠接口解耦、流程骨架靠组合灵活编排。
- 核心避坑要点:策略路由需复合维度、通用方法加权限保护、规避接口默认方法冲突、摒弃继承耦合设计。
待更新连载
- 第 3 篇:《模板方法模式实战:为什么组合优于继承?》
内容预告:深挖模板方法两种实现方案,对比继承耦合缺陷与组合模式的扩展性,附完整可运行Demo。
- 第 4 篇:《三层策略模式拆解:彻底解耦多平台差异化逻辑》
内容预告:拆解Request/Parse/Exception三层策略设计思路,讲解策略工厂复合键路由、多平台扩展落地规范。
- 第 5 篇:《电子面单全架构复盘:从踩坑到最优设计》
内容预告:完整复盘整套多平台电子面单链路,整合抽象类、接口、策略、模板方法整套设计模式落地经验。
关注专栏,持续更新生产级Java面试实战内容,拒绝空洞八股!
你在项目中有没有纠结过接口和抽象类的选型?踩过哪些继承、策略路由的坑?欢迎评论区交流!