模板方法用组合还是继承?多平台电子面单的抉择

模板方法用组合还是继承?多平台电子面单的抉择

摘要:模板方法模式通常用抽象类定义算法骨架,但在多平台电子面单架构中,我们却选择了"组合"方式------WaybillFetchTemplate 通过注入策略对象来固定流程,而非让子类继承。本文从真实代码出发,对比两种实现方式,拆解为什么在这个场景下"组合优于继承",并总结出模板方法选型的决策框架。
📖 系列导航


文章目录


一、一个经典问题:模板方法怎么写?

模板方法模式(Template Method Pattern)是 GoF 23 种设计模式中最简单也最常用的行为型模式之一。它的核心思想是:在父类中定义算法骨架,将某些步骤延迟到子类实现

教科书上的标准实现通常是这样的:

java 复制代码
// 抽象父类:定义算法骨架
public abstract class AbstractWaybillFetcher {
    
    // 模板方法:固定流程
    public final void fetch() {
        validate();           // 1. 校验
        Object request = buildRequest(); // 2. 构建请求
        String response = callApi(request); // 3. 调用API
        parseResponse(response); // 4. 解析响应
        save();               // 5. 持久化
    }
    
    private void validate() { /* 通用校验 */ }
    protected abstract Object buildRequest(); // 子类实现
    protected abstract void parseResponse(String response);
    protected abstract void save();
    protected String callApi(Object request) { /* 通用调用 */ return ""; }
}

// 子类:奇门实现
public class QiMenWaybillFetcher extends AbstractWaybillFetcher {
    @Override
    protected Object buildRequest() { /* 构建奇门SDK请求 */ }
    @Override
    protected void parseResponse(String response) { /* 解析奇门响应 */ }
    @Override
    protected void save() { /* 保存运单 */ }
}

这种写法用了二十多年,看起来毫无问题。但在我们的多平台电子面单架构中,却主动放弃了这个"标准答案",选择了一个看似"离经叛道"的做法:用组合代替继承

🏭 设计模式视角 :模板方法模式是 GoF 23 种行为型模式之一,理解它的两种实现方式(继承 vs 组合)对于判断"何时用继承、何时用组合"至关重要。在《Java 23种设计模式:从踩坑到精通》系列的 第22篇(策略模式)第23篇(模板方法模式) 中,我深度对比了两种模式的适用场景与选型边界,欢迎延伸阅读。


二、我们的选择:组合式模板

在我们的架构中,模板类 WaybillFetchTemplate 是一个普通的、非抽象的类,它不要求任何子类继承它:

java 复制代码
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) {
        // 1. 构建请求(策略)
        Object request = requestStrategy.buildRequest(ctx);
        // 2. 调用API(ApiInvoker)
        String response = apiInvoker.invoke(ctx, request, traceId);
        // 3. 业务异常判断(策略)
        if (!exceptionStrategy.isBusinessSuccess(response)) {
            markException(ctx.getTicket(), exceptionStrategy.extractErrorMsg(response));
            return false;
        }
        // 4. 解析响应(策略)
        List<Detail> details = parseStrategy.parseResponse(ctx, response);
        if (details.isEmpty()) {
            markException(ctx.getTicket(), "未获取到运单号");
            return false;
        }
        // 5. 持久化
        persistence.saveAndBind(ctx.getTicket(), details, ctx.isFirst());
        return true;
    }
}

使用时,通过工厂注入不同平台的策略实例:

java 复制代码
// 奇门
RequestStrategy req = new QiMenRequestStrategy();
ParseStrategy parse = new QiMenParseStrategy();
ExceptionStrategy ex = new QiMenExceptionStrategy();
WaybillFetchTemplate template = new WaybillFetchTemplate(req, parse, ex, invoker, persistence);
template.execute(ctx);

// 抖音
req = new DouYinRequestStrategy();
parse = new DouYinParseStrategy();
ex = new DouYinExceptionStrategy();
template = new WaybillFetchTemplate(req, parse, ex, invoker, persistence);
template.execute(ctx);

可以看到,WaybillFetchTemplate 没有任何 abstract 方法,也没有任何子类。它的"可变部分"全部通过构造函数注入的策略对象来实现。


三、为什么不用继承?四个核心理由

理由一:策略已经用接口独立了,继承会造成冗余

在我们的架构中,平台差异已经被三个策略接口完美隔离:

java 复制代码
public interface RequestStrategy { Object buildRequest(WaybillContext ctx); }
public interface ParseStrategy { List<Detail> parseResponse(WaybillContext ctx, String response); }
public interface ExceptionStrategy { boolean isBusinessSuccess(String response); }

如果再用一个抽象的 AbstractWaybillFetchTemplate 让各平台子类去实现,等于把同样的差异逻辑在两个地方重复定义。子类既要实现策略接口,又要覆写模板的抽象方法------这是典型的"过度抽象"。

理由二:避免类爆炸

假设我们用继承方式实现,每接入一个新平台就要写一个子类:

复制代码
AbstractWaybillFetchTemplate
    ├── QiMenWaybillFetchTemplate
    ├── DouYinWaybillFetchTemplate
    ├── DouYinDaiFaWaybillFetchTemplate
    ├── JDWaybillFetchTemplate
    └── PDDWaybillFetchTemplate

十几个平台就是十几个子类,而它们之间的唯一区别只是策略不同。用组合,只需一个 WaybillFetchTemplate,通过注入不同策略实例即可覆盖所有平台。

理由三:运行时灵活性

组合允许在运行时动态替换策略。比如,通过工厂根据平台编码返回不同的策略组合:

java 复制代码
RequestStrategy req = strategyFactory.getRequestStrategy(platformCode, original);
ParseStrategy parse = strategyFactory.getParseStrategy(platformCode, original);
ExceptionStrategy ex = strategyFactory.getExceptionStrategy(platformCode, original);
WaybillFetchTemplate template = new WaybillFetchTemplate(req, parse, ex, ...);

继承的类关系在编译期就固定了,无法做到这种运行时动态组装。

理由四:更好的可测试性

组合式模板可以单独测试,注入 Mock 的策略对象即可,无需启动 Spring 容器或继承任何基类:

java 复制代码
@Test
public void testTemplate() {
    RequestStrategy mockReq = ctx -> "mock request";
    ParseStrategy mockParse = (ctx, resp) -> Collections.singletonList(mockDetail);
    ExceptionStrategy mockEx = resp -> true;
    
    WaybillFetchTemplate template = new WaybillFetchTemplate(mockReq, mockParse, mockEx, mockInvoker, mockPersistence);
    boolean result = template.execute(ctx);
    assertTrue(result);
}

如果用继承,测试时需要创建匿名子类或启动整个依赖链,成本高得多。


四、对比表格:继承 vs 组合实现模板方法

维度 继承方式 组合方式
代码量 每个平台一个子类 一个模板类 + 策略接口
扩展性 新增平台需新增子类 新增平台只需新增策略实现
灵活性 编译期绑定 运行时动态替换
测试性 需创建子类实例 直接 Mock 策略注入
类数量 N 个平台 → N 个子类 1 个模板类
复用性 子类间无法复用策略 策略可跨平台组合
适合场景 步骤固定、差异集中、子类数量少 步骤固定、差异分散、实现类数量多

五、什么时候该用继承?

组合不是银弹。在以下场景中,继承方式仍然更合适:

  • 需要共享实例状态 :比如父类持有 commonDaologger 等字段,子类直接使用。这正是我们架构中 DefaultBaseManager 用抽象类的原因。
  • 子类数量极少:只有两三个子类时,继承的简洁性优于组合的灵活性。
  • 步骤之间有强关联:模板的某些步骤需要访问父类的内部状态,不适合拆分成独立接口。

在我们的电子面单架构中,策略已经通过接口独立,且平台数量可能增长到十几个,组合的优势远大于继承。


六、延伸:顺丰子母件------模板层分支的威力

组合式模板还有一个额外优势:在模板层增加流程分支非常自然

顺丰超过 10 件需要走子母件模式(分批调用 API),我们的处理方式是直接在 WaybillFetchTemplate 中增加一个 executeSFMoreTen 方法:

java 复制代码
public boolean execute(WaybillContext ctx) {
    if (isSFMoreTen(ctx)) {
        return executeSFMoreTen(ctx, traceId);  // 子母件分支
    }
    return executeNormal(ctx, traceId);         // 普通分支
}

如果用继承,子母件逻辑要么重复在每个子类中实现,要么在父类中增加复杂的分支判断,很容易失控。组合方式让模板层成为唯一的流程控制点,分支逻辑清晰可维护。


七、总结:模板方法选型决策框架

结合我们的实战经验,可以提炼出以下决策框架:

  1. 先看差异是否已经抽象成接口:如果平台差异已经通过策略接口独立,优先用组合。
  2. 看平台数量:超过 5 个平台时,组合避免类爆炸的优势明显。
  3. 看是否需要运行时灵活替换:需要则用组合,不需要则继承也可。
  4. 看是否需要共享实例状态:如果需要且差异未接口化,继承更合适。

面试绝杀一句话:模板方法模式的核心是"固定流程骨架,差异化具体步骤"。这个骨架既可以用继承实现,也可以用组合实现。在我们的多平台电子面单架构中,因为平台差异已经通过三层策略接口独立,组合方式更灵活、更简洁。


八、系列导航与参考

本篇文章是「电商多平台电子面单对接实战」的第八篇(设计模式篇),聚焦模板方法模式在真实项目中的选型实践。

系列文章目录


延伸阅读:Java 23种设计模式实战系列

本文中模板方法选型的核心------组合优于继承,以及背后涉及的策略模式、模板方法模式等设计理念,在《Java 23种设计模式:从踩坑到精通》系列中有更体系化的讲解。如果你对以下问题感兴趣,推荐延伸阅读:

  • 策略模式 vs 模板方法模式:两者如何选择?各适用于什么场景?
  • 组合优于继承:六大设计原则中的这一条,在真实项目中如何落地?
  • 工厂模式 + 策略模式:如何配合实现复合Key路由?

📖 《Java 23 种设计模式:从踩坑到精通》


九、一起交流,共同进步

技术之路,一个人走得快,一群人走得远。

如果您在项目中也有过"继承还是组合"的纠结,希望本文的实战经验能给您带来启发。

  • 📌 关注我 :点击上方"关注",第一时间获取系列更新推送。
  • 💬 留言讨论:您在实际项目中选择过组合式模板方法吗?遇到过什么坑?欢迎在评论区分享。
  • 🔗 分享转发 :如果本文对您有帮助,请 点赞收藏分享,让更多同行看到。

标签#Java #设计模式 #模板方法模式 #组合优于继承 #电子面单 #架构设计 #重构