从多平台电子面单架构看接口与抽象类的真实选型

副标题:为什么基础设施用抽象类,平台差异用接口,模板方法用组合?

📌 「Java面试·实战笔记」系列第 2 篇
上一篇 我用大白话比喻,帮大家彻底搞懂了接口和抽象类的基础区别,没看过的朋友可以跳转第一篇:【接口和抽象类有什么区别?电子面单多平台对接实战】。但说实话,背概念没用,落地到项目才是真本事

今天这篇,我直接拿线上真实落地的多平台电子面单生产架构跟大家拆解,全程无虚构代码、无空洞八股文。

重点讲透三个核心问题:

  1. 项目里的 DefaultBaseManager,为啥一定要用抽象类?

  2. 多平台差异化逻辑,为啥拆成三层策略接口,不用抽象类?

  3. 模板方法模式,为啥抛弃传统继承,改用组合方式实现?

看完这篇,你以后面试被问“接口和抽象类怎么选型”,再也不用死记硬背,直接套生产落地逻辑回答,“吊打”大部分八股文选手。



1 先复盘:别停留在“背概念”,要落地到“写代码”

上一篇我们总结了最基础的选型逻辑,这里快速回顾一下:

  • 抽象类:代表 is-a 父子关系,适合共享状态、通用模板、公共基础设施能力

  • 接口:代表 can-do 能力契约,适合解耦、多态、隔离业务差异

之前的示例是简化版demo,今天咱们直接上真实业务架构——多平台电子面单系统。

这个系统要对接奇门、抖音、京东、支付宝等十几家电商平台,每个平台的请求、响应、异常规则全都不一样,非常适合用来讲接口、抽象类、组合模式的精准选型


2 真实架构全景速览

先看精简后的分层架构图,一眼看懂整体设计:
真实架构全景

整个架构的核心规律,一句话总结:

上层通用基础设施,用抽象类;下层平台差异化业务,用接口;固定流程模板,用组合实现。

下面逐层拆开讲,每一个选型都告诉你“为什么这么写,这么写的好处是什么”。


3 抽象类:专门用来做「基础设施复用」

3.1 生产真实代码:DefaultBaseManager

我们项目里所有的业务Service,全部统一继承 DefaultBaseManager

不管是订单修改服务、面单取号服务,都能直接复用父类的公共能力,不用每个类重复定义。

public class TocOrderExpressModifyService extends DefaultBaseManager {
    public BatchResult editTocLogistic(List<Long> ids, String express, String productCode) {
        // 直接调用父类事务方法,不用自己写事务逻辑
        TransactionStatus status = beginTxPropagationRequiresNew();
        // ... 业务逻辑
        commitTransaction(status);
    }
}

public class WaybillFetchService extends DefaultBaseManager {
    public boolean fetchWaybill(TocWmsPickTicket ticket, int exsitJianNum, String productCode) {
        // 直接用父类的DAO、日志对象
        ctx.getExt().put(WaybillContext.KEY_COMMON_DAO, this.commonDao);
        logger.info("开始取号...");
    }
}

而这个抽象父类,统一封装了所有业务服务的通用基础设施:

  • 数据库操作对象 commonDao

  • 统一日志对象 logger

  • 全套事务管理方法:开启事务、提交事务、回滚事务

3.2 重点:为啥这里必须用抽象类?接口不行吗?

答案:完全不行,语法和设计层面都不支持。

我给大家讲三个最核心的原因,面试直接背这三点就够了:

① 接口不能定义实例变量,无法共享状态

commonDao 是需要Spring注入的实例对象,每个子类都要共用。但接口里的变量默认是static final 常量,根本不能定义可变的实例属性。

// 编译直接报错!接口做不到状态共享
public interface BaseManager {
    CommonDao commonDao; 
}

② 抽象类可以写构造方法,统一管理依赖注入

所有子类的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. 请求构建策略:不同平台拼参规则不同
public interface RequestStrategy {
    Object buildRequest(WaybillContext ctx);
}

// 2. 响应解析策略:不同平台返回报文格式不同
public interface ParseStrategy {
    List<TocPickTicketWayBillDetailsNew> parseResponse(String response, WaybillContext ctx);
}

// 3. 异常判断策略:不同平台报错字段、提示文案不同
public interface ExceptionStrategy {
    boolean isBusinessSuccess(String response);
    String extractErrorMsg(String response);
}

然后每个平台单独实现这三套接口,各自写自己的差异化逻辑:

// 奇门平台专属实现
public class QiMenRequestStrategy implements RequestStrategy {
    @Override
    public Object buildRequest(WaybillContext ctx) {
        return QiMenWaybillBuilder.buildRequest(ctx);
    }
}

// 抖音平台专属实现
public class DouYinRequestStrategy implements RequestStrategy {
    @Override
    public Object buildRequest(WaybillContext ctx) {
        return buildDouYinJson(ctx);
    }
}

4.2 为啥这里用接口,不用抽象类?

四个通俗好懂的理由:

① 语义是can-do能力,不是is-a父子关系

抖音、奇门的请求构建类,不需要继承某个父类,只需要具备“构建请求”的能力就行,这就是接口的核心语义:能力契约。

② 策略类无状态,不需要共享属性

所有策略实现类都没有自定义成员变量,所有参数都通过上下文传入,无状态的业务能力,用接口最轻量化。

③ 支持灵活多实现,扩展性拉满

一个平台后续如果新增取消、重打等能力,可以直接多实现多个接口,抽象类只能单继承,完全没这么灵活。

④ 完美贴合开闭原则

新增支付宝、拼多多等新平台,只需要新增实现类,不用改一行旧代码。如果用抽象类,很容易需要修改父类逻辑,破坏开闭原则。

4.3 延伸:API调度也用接口统一

我们的API调用层,同样用接口做统一约束:

public interface RequestHandler {
    String handle(WaybillContext ctx, Object request, String traceId) throws IOException;
}

奇门、抖音、通用HTTP、OAuth2鉴权,各自实现这个接口,工厂根据平台编码+子渠道复合维度路由,精准匹配对应处理器,彻底避免策略覆盖bug。


5 重点进阶:模板方法为啥用组合,不用继承?

5.1 传统模板方法的坑

很多人学模板方法模式,只知道用抽象类+子类继承重写

但在我们的生产架构里,完全抛弃了这种写法,改用 组合式模板 WaybillFetchTemplate

5.2 生产真实组合模板代码

public class WaybillFetchTemplate {
    // 直接注入三层策略接口,组合复用,不用继承
    private final RequestStrategy requestStrategy;
    private final ParseStrategy parseStrategy;
    private final ExceptionStrategy exceptionStrategy;
    private final ApiInvoker apiInvoker;
    private final WaybillPersistence persistence;

    // 构造函数注入所有策略,灵活组装
    public WaybillFetchTemplate(RequestStrategy req, ParseStrategy parse,
            ExceptionStrategy ex, ApiInvoker invoker, WaybillPersistence persist) {
        this.requestStrategy = req;
        this.parseStrategy = parse;
        this.exceptionStrategy = ex;
        this.apiInvoker = invoker;
        this.persistence = persist;
    }

    // 固定的核心流程骨架,永不修改
    public boolean execute(WaybillContext ctx) {
        String traceId = ctx.getTicket().getCode() + "_" + System.currentTimeMillis();
        try {
            // 1. 差异化:构建请求
            Object request = requestStrategy.buildRequest(ctx);
            // 2. 通用:调用API
            String response = apiInvoker.invoke(ctx, request, traceId);
            // 3. 差异化:业务异常判断
            if (!exceptionStrategy.isBusinessSuccess(response)) {
                String errMsg = exceptionStrategy.extractErrorMsg(response);
                markException(ctx.getTicket(), errMsg);
                return false;
            }
            // 4. 差异化:解析响应
            List<Detail> details = parseStrategy.parseResponse(response, ctx);
            if (details == null || details.isEmpty()) {
                markException(ctx.getTicket(), "未获取到运单号");
                return false;
            }
            // 5. 通用:持久化数据
            boolean isFirst = (ctx.getExsitJianNum() == 0);
            persistence.saveAndBind(ctx.getTicket(), details, isFirst);
            return true;
        } catch (Exception e) {
            markException(ctx.getTicket(), "系统异常: " + e.getMessage());
            return false;
        }
    }
}

5.3 为啥组合比继承更香?4个实战理由

① 差异逻辑已经通过接口剥离完毕,没必要再继承

平台的所有差异化步骤,已经交给三层策略接口实现了。如果再搞一个抽象模板类让子类继承重写,纯属重复造轮子,代码冗余。

② 彻底避免类爆炸

如果用继承,10个平台就要写10个模板子类。用组合,一个模板类适配所有平台,只需要注入不同策略即可。

③ 运行时动态灵活

继承的类关系编译期就固定死了,组合可以在运行时通过工厂动态替换、组合策略,适配不同平台、不同渠道。

④ 测试极其简单

可以直接Mock策略接口,单独测试模板核心流程,不用启动Spring容器,不用依赖继承关系。

⑤ 严格遵循单一职责原则,代码职责高度拆分

通过组合模式将「固定流程骨架」和「差异化业务逻辑」完全拆分:WaybillFetchTemplate 只专注负责流程编排、通用逻辑处理、异常兜底,各司其职;而三层策略接口只专注各自的单一差异化能力,完全贴合单一职责原则。反观传统继承式模板方法,所有逻辑集中在父类和子类中,极易导致模板类职责臃肿、代码耦合严重,后续维护迭代成本极高。

面试金句

模板方法的核心是“固定流程、差异化步骤”。传统继承式适合无前置解耦的场景,而我们项目中差异逻辑已通过接口独立,用组合实现更轻量、更灵活、符合单一职责。

5.4 极简可运行Demo,秒懂组合模板

下面这段代码可以直接复制运行,帮你直观理解核心思想:

public class TemplateDemo {
    // 差异化策略接口
    interface Strategy {
        String execute();
    }

    // 固定模板类(组合策略,不继承)
    static class Template {
        private final Strategy strategy;
        public Template(Strategy s) { this.strategy = s; }

        public void run() {
            System.out.println("1. 公共前置步骤");
            System.out.println("2. " + strategy.execute());
            System.out.println("3. 公共后置步骤");
        }
    }

    public static void main(String[] args) {
        // 平台A差异化逻辑
        new Template(() -> "平台A:淘宝SDK签名 + 调用奇门API").run();
        // 平台B差异化逻辑
        new Template(() -> "平台B:MD5签名 + 调用抖音API").run();
    }
}

运行结果:

1. 公共前置步骤
2. 平台A:淘宝SDK签名 + 调用奇门API
3. 公共后置步骤
1. 公共前置步骤
2. 平台B:MD5签名 + 调用抖音API
3. 公共后置步骤

核心:流程骨架固定,差异化步骤可随意替换,这就是组合模板的精髓。


6 终极选型总结(面试直接套)

业务场景选型方案生产案例
封装DAO、日志、事务等通用基础设施抽象类DefaultBaseManager
隔离各平台差异化业务逻辑接口三层策略接口
统一API调用能力契约接口RequestHandler
固定流程骨架,步骤已通过接口解耦组合类WaybillFetchTemplate

三分钟选型口诀

  1. 有共享状态、通用基础设施 → 抽象类

  2. 无状态、纯能力契约、业务差异解耦 → 接口

  3. 流程固定、步骤可替换,且已接口化 → 组合优先于继承


7 生产踩坑真实教训(避坑必看)

7.1 策略路由维度不足,导致策略覆盖

早期我们只用 platFormCode 单维度缓存策略,导致抖音普通、抖音代发策略互相覆盖:

requestMap.put("DY", new DouYinRequestStrategy());      // 抖音普通
requestMap.put("DY", new DouYinDaiFaRequestStrategy()); // 直接覆盖上面的策略!

教训:同平台多子渠道,必须用 platFormCode + 子渠道标识 复合键路由,和ApiInvoker保持统一。

7.2 抽象类方法权限乱用

公共通用方法如果定义为protected,子类可以随意重写,容易绕过核心校验逻辑。

规范:公共固定流程用private/final保护,只暴露必须实现的抽象方法。

7.4 传统继承模板的耦合致命坑

这也是我们彻底抛弃继承式模板方法的核心踩坑经验:早期项目曾使用抽象模板类+子类继承的方式实现流程编排,产生了严重的继承耦合问题

一方面,子类强依赖父类的实现逻辑,一旦父类修改通用流程、调整参数校验规则、优化异常兜底逻辑,所有继承该模板的平台子类都会被动受影响,极易引发全平台隐性bug,牵一发而动全身,线上风险极高;另一方面,子类可以随意重写父类的通用固定方法,部分开发不规范的重写,会破坏模板预设的核心流程逻辑,导致不同平台的执行流程不统一,排查问题难度极大。

核心教训:继承是静态强耦合关系,会大幅提升代码维护风险;而组合是弱依赖关系,通过注入策略实现能力组装,完全解耦核心流程与差异化逻辑,从根源规避继承耦合问题。

7.3 接口default方法菱形冲突

多个接口定义同名default方法,实现类必须强制重写,否则编译报错。开发中尽量规避同名默认方法,减少冲突。


8 延伸思考:新增支付宝平台,该怎么改代码?

大家可以先自己思考一下,再看答案,加深理解!

👋 先独立思考,再往下核对答案


参考答案

需要新增的代码(零侵入旧代码)

  1. 支付宝请求策略:ZFBRequestStrategy 实现 RequestStrategy

  2. 支付宝解析策略:ZFBParseStrategy 实现 ParseStrategy

  3. 支付宝异常策略:ZFBExceptionStrategy 实现 ExceptionStrategy

  4. 支付宝API处理器:ZFBHandler 实现 RequestHandler(或复用通用SimpleHttpHandler)

需要改动的代码

  1. 策略工厂注册新策略

  2. API调用器注册新处理器

完全不用动的核心代码

模板流程、服务层逻辑全部零改动!

这就是开闭原则的终极魅力:新增功能只加代码,不改旧逻辑。


9 评论区面试挑战

面试原题

DefaultBaseManager用抽象类,RequestStrategy用接口,请从设计语义语法限制两个角度说明选型原因?

标准答案

  1. 设计语义:DefaultBaseManager是is-a父子关系,是所有服务的基础父类;RequestStrategy是can-do能力契约,只约束“能构建请求”的行为,无继承关系。

  2. 语法限制:DefaultBaseManager需要持有DAO、日志等实例状态,接口无法定义实例变量;策略类无状态、纯行为定义,接口完全适配。


10 全文总结(面试绝杀一句话)

在我们多平台电子面单架构中,抽象类负责基础设施状态复用,接口负责平台业务差异解耦,模板方法通过组合实现灵活的流程编排,三者配合完美实现了“新增平台不改动核心代码”的开闭原则。


🔜 本系列完整连载阅读顺序

已更新内容

  1. 第1篇:《接口与抽象类到底怎么分?通俗比喻+基础选型框架》
    👉 上篇直达:接口和抽象类有什么区别?电子面单多平台对接实战

内容概览:用快递家族生活化比喻讲清接口、抽象类底层语义,搭建基础选型判断框架,通过简化面单代码区分is-a与can-do关系,扫清概念层面所有八股误区。

  1. 第2篇:本文《从多平台电子面单架构看接口与抽象类的真实选型》

内容概览:基于线上电子面单生产架构落地拆解,结合DefaultBaseManager抽象基类、三层策略接口、组合式模板,给出可直接套用的工程选型标准,附带真实踩坑与面试标准答案。

📌 全文核心知识点速览(面试极速复盘)

  1. 抽象类核心选型场景:需共享实例状态、封装通用基础设施、统一依赖注入,适配is-a父子关系,不可被接口替代。
  2. 接口核心选型场景:无状态纯能力契约、业务差异解耦、多灵活扩展,适配can-do能力关系,贴合开闭原则。
  3. 组合模板核心优势:规避类爆炸、运行时动态适配、低耦合易测试、贴合单一职责,优于传统继承模板。
  4. 核心架构精髓:基础设施靠抽象类复用、业务差异靠接口解耦、流程骨架靠组合灵活编排。
  5. 核心避坑要点:策略路由需复合维度、通用方法加权限保护、规避接口默认方法冲突、摒弃继承耦合设计。

待更新连载

  • 第 3 篇:《模板方法模式实战:为什么组合优于继承?》

内容预告:深挖模板方法两种实现方案,对比继承耦合缺陷与组合模式的扩展性,附完整可运行Demo。

  • 第 4 篇:《三层策略模式拆解:彻底解耦多平台差异化逻辑》

内容预告:拆解Request/Parse/Exception三层策略设计思路,讲解策略工厂复合键路由、多平台扩展落地规范。

  • 第 5 篇:《电子面单全架构复盘:从踩坑到最优设计》

内容预告:完整复盘整套多平台电子面单链路,整合抽象类、接口、策略、模板方法整套设计模式落地经验。

关注专栏,持续更新生产级Java面试实战内容,拒绝空洞八股!


你在项目中有没有纠结过接口和抽象类的选型?踩过哪些继承、策略路由的坑?欢迎评论区交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值