本文是「设计模式实战解读」系列第三篇。系列文章统一按照定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ的结构展开每篇聚焦一个模式讲透。一句话定义模板方法模式Template Method在父类中定义一个算法的骨架步骤顺序把某些步骤的具体实现延迟到子类中。子类可以重写步骤细节但不能改变整体流程。归属行为型模式。一、没有模板方法时的痛点假设你在做一个数据同步模块需要支持多种数据源的全量同步// MySQL → 目标系统publicclassMySQLSyncJob{publicvoidexecute(){log(开始同步);ConnectionconnDriverManager.getConnection(jdbc:mysql://...);// 建立连接ResultSetrsconn.createStatement().executeQuery(SELECT * FROM orders);// 拉取数据ListMapdataconvertToList(rs);// 数据转换targetApi.batchPush(data);// 推送到目标conn.close();// 关闭连接log(同步完成);}}// PostgreSQL → 目标系统publicclassPostgresSyncJob{publicvoidexecute(){log(开始同步);ConnectionconnDriverManager.getConnection(jdbc:postgresql://...);// 建立连接ResultSetrsconn.createStatement().executeQuery(SELECT * FROM orders);// 拉取数据ListMapdataconvertToList(rs);// 数据转换targetApi.batchPush(data);// 推送到目标conn.close();// 关闭连接log(同步完成);}}// MongoDB → 目标系统publicclassMongoSyncJob{publicvoidexecute(){log(开始同步);MongoClientclientMongoClients.create(mongodb://...);// 建立连接FindIterableDocumentdocsclient.getDatabase(db).getCollection(orders).find();// 拉取ListMapdataconvertDocs(docs);// 数据转换不同targetApi.batchPush(data);// 推送到目标client.close();// 关闭连接log(同步完成);}}三段代码的骨架完全相同日志 → 连接 → 拉取 → 转换 → 推送 → 关闭 → 日志只是具体步骤的实现不同。这就是典型的代码重复——流程是通用的差异只在某几步。问题骨架被重复了 N 遍修改一处比如加个耗时统计要改 N 个文件流程一致性无法保证——有人忘了关连接有人忘了打日志难以做统一的异常处理、监控埋点、重试逻辑二、模式结构┌───────────────────────────────────┐ │ AbstractSyncJob (抽象父类) │ ├───────────────────────────────────┤ │ execute() ← 模板方法 │ 流程骨架final 不可重写 │ # connect() ← 抽象步骤 │ 子类必须实现 │ # fetchData() ← 抽象步骤 │ 子类必须实现 │ # convertData() ← 抽象步骤 │ 子类必须实现 │ # pushData() ← 具体步骤 │ 父类默认实现子类可选重写 │ # close() ← 抽象步骤 │ 子类必须实现 │ # beforeExecute() ← 钩子方法 │ 默认空实现子类可选重写 │ # afterExecute() ← 钩子方法 │ 默认空实现子类可选重写 └───────────────────┬───────────────┘ │ ┌───────────┼───────────┐ ↓ ↓ ↓ MySQLSyncJob PostgresSyncJob MongoSyncJob三种方法类型模板方法Template Method定义流程骨架通常标记final防止子类重写抽象步骤Abstract Step子类必须实现的变化点钩子方法Hook有默认空实现子类按需重写三、核心实现3.1 基础版publicabstractclassAbstractSyncJob{// 模板方法定义流程骨架不允许子类修改流程顺序publicfinalvoidexecute(){longstartSystem.currentTimeMillis();log(同步任务开始);beforeExecute();// 钩子执行前Objectconnectionconnect();// 步骤1建立连接ListMapString,ObjectrawDatafetchData(connection);// 步骤2拉取数据ListMapString,ObjectconvertedconvertData(rawData);// 步骤3转换数据pushData(converted);// 步骤4推送数据close(connection);// 步骤5关闭连接afterExecute();// 钩子执行后longcostSystem.currentTimeMillis()-start;log(同步任务完成耗时: costms);}// 抽象步骤子类必须实现protectedabstractObjectconnect();protectedabstractListMapString,ObjectfetchData(Objectconnection);protectedabstractListMapString,ObjectconvertData(ListMapString,ObjectrawData);protectedabstractvoidclose(Objectconnection);// 具体步骤有默认实现子类可以重写protectedvoidpushData(ListMapString,Objectdata){targetApi.batchPush(data);}// 钩子方法默认空实现protectedvoidbeforeExecute(){}protectedvoidafterExecute(){}privatevoidlog(Stringmsg){System.out.println([getClass().getSimpleName()] msg);}}3.2 子类实现publicclassMySQLSyncJobextendsAbstractSyncJob{privatefinalStringjdbcUrl;publicMySQLSyncJob(StringjdbcUrl){this.jdbcUrljdbcUrl;}OverrideprotectedObjectconnect(){returnDriverManager.getConnection(jdbcUrl);}OverrideprotectedListMapString,ObjectfetchData(Objectconnection){Connectionconn(Connection)connection;// 执行 SQL转成 ListMapreturnJdbcUtils.queryForList(conn,SELECT * FROM orders);}OverrideprotectedListMapString,ObjectconvertData(ListMapString,ObjectrawData){// MySQL 的字段名转换逻辑returnrawData.stream().map(row-convertFieldNames(row,mysql)).collect(Collectors.toList());}Overrideprotectedvoidclose(Objectconnection){((Connection)connection).close();}}publicclassMongoSyncJobextendsAbstractSyncJob{privatefinalStringmongoUri;OverrideprotectedObjectconnect(){returnMongoClients.create(mongoUri);}OverrideprotectedListMapString,ObjectfetchData(Objectconnection){MongoClientclient(MongoClient)connection;// MongoDB 查询逻辑returnMongoUtils.findAll(client,db,orders);}OverrideprotectedListMapString,ObjectconvertData(ListMapString,ObjectrawData){// MongoDB 的 _id 转换、嵌套文档展平等returnrawData.stream().map(MongoFieldConverter::flatten).collect(Collectors.toList());}Overrideprotectedvoidclose(Objectconnection){((MongoClient)connection).close();}// 重写钩子MongoDB 同步前先检查集合是否存在OverrideprotectedvoidbeforeExecute(){checkCollectionExists();}}3.3 关键设计点execute()标final防止子类不小心改了流程顺序抽象步骤用protected abstract强制子类实现钩子用protected非 abstract有默认空实现子类选择性重写模板方法内统一做耗时统计、异常处理——修改一处所有子类生效四、真实应用场景4.1 框架级应用框架模板方法在哪骨架步骤子类扩展点SpringAbstractApplicationContext.refresh()Bean 生命周期加载流程onRefresh() 等钩子ServletHttpServlet.service()解析请求类型→分发doGet()/doPost()JUnitTestCase.runBare()setUp→test→tearDownsetUp()/tearDown()MyBatisBaseExecutor.query()缓存检查→查询→结果处理doQuery()Spring MVCDispatcherServlet.doDispatch()请求映射→处理→视图渲染HandlerAdapterRocketMQDefaultMQPushConsumer拉取→过滤→消费→ACKconsumeMessage()4.2 业务场景业务骨架不变的流程变化的步骤数据同步连接→拉取→转换→推送→关闭连接方式、数据转换逻辑导出报告查数据→组装→渲染→写文件渲染引擎PDF/Excel/HTML审批流提交→校验→通知→归档校验规则、通知渠道支付参数组装→签名→请求→验签→返回签名算法、请求格式连接器执行鉴权→构建请求→发送→解析响应→记录日志鉴权方式、请求构造、响应解析消息消费接收→反序列化→校验→处理→ACK处理逻辑4.3 连接器 Handler 的模板方法在 iPaaS 引擎中每个连接器的执行流程是模板化的AbstractConnectorHandler.execute() ← 模板方法 ├── 1. resolveAuth() ← 解析鉴权信息OAuth/APIKey/Basic ├── 2. buildRequest() ← 构建 HTTP/RPC 请求子类实现 ├── 3. executeRequest() ← 发送请求通用 HTTP 客户端 ├── 4. parseResponse() ← 解析响应子类实现 ├── 5. handleError() ← 错误处理有默认实现子类可重写 └── 6. recordLog() ← 记录执行日志通用新增一个连接器比如对接飞书只需要实现buildRequest()和parseResponse()其他步骤由父类统一保证。这让连接器开发从写全链路变成只写差异。五、常见变种5.1 模板方法 策略模式当变化的步骤可以独立抽象成策略时可以用组合代替继承publicclassSyncJob{privatefinalDataFetcherfetcher;// 策略1拉取privatefinalDataConverterconverter;// 策略2转换privatefinalDataPusherpusher;// 策略3推送// 模板方法流程固定步骤委托给策略publicvoidexecute(){Objectdatafetcher.fetch();Objectconvertedconverter.convert(data);pusher.push(converted);}}这种方式比纯继承更灵活——可以自由组合 fetcher/converter/pusher不需要为每种组合创建子类。5.2 带回调的模板方法Java 8 可以用 Lambda 代替继承publicclassSyncTemplate{publicvoidexecute(SupplierObjectconnect,FunctionObject,ListMapString,Objectfetch,UnaryOperatorListMapString,Objectconvert,ConsumerObjectclose){Objectconnconnect.get();try{ListMapString,Objectdatafetch.apply(conn);ListMapString,Objectconvertedconvert.apply(data);pushData(converted);}finally{close.accept(conn);}}}// 使用syncTemplate.execute(()-DriverManager.getConnection(url),conn-JdbcUtils.queryForList((Connection)conn,sql),data-data.stream().map(this::convertFields).collect(toList()),conn-((Connection)conn).close());优势不需要定义子类一次性使用更轻量。5.3 多级模板方法父类定义大骨架中间层抽象类定义子骨架AbstractHandler └── AbstractHttpHandler (骨架: buildUrl → addHeaders → sendRequest → parseBody) ├── RestApiHandler └── GraphQLHandler └── AbstractMqHandler (骨架: connectBroker → subscribe → consume → ack) ├── RocketMqHandler └── KafkaHandler六、优缺点优点缺点复用骨架代码消除重复子类受父类约束灵活性有限流程一致性有保障继承层次深时可读性下降统一做横切逻辑日志/监控/异常模板方法越多父类越臃肿新增变种只需写差异部分不了解父类全貌时容易误用子类不能破坏整体流程对组合友好度不如策略模式七、避坑指南坑 1模板方法没标 final不加final子类可能重写模板方法本身破坏流程——导致某些步骤被跳过。务必给模板方法加final。坑 2步骤太多导致超级父类当骨架步骤超过 7 个时父类变得很难理解。解法把步骤分组用多级模板中间层抽象类或组合模式分解。坑 3抽象步骤和钩子的边界不清抽象步骤abstract子类必须实现的核心差异钩子非 abstract 空实现子类可选增强的扩展点如果搞反了——把钩子做成 abstract子类被迫实现一堆空方法把核心步骤做成钩子子类忘了重写导致默认行为不对。坑 4子类之间有交叉逻辑MySQLSyncJob 和 PostgresSyncJob 的connect()很像都是 JDBC但和 MongoSyncJob 完全不同。这时应该加一个中间层AbstractJdbcSyncJob把 JDBC 共性抽上去AbstractSyncJob ├── AbstractJdbcSyncJob (共享 JDBC connect/close) │ ├── MySQLSyncJob │ └── PostgresSyncJob └── MongoSyncJob坑 5模板方法内异常处理不统一父类的execute()应该统一处理异常try-catch 清理资源不要让子类各自处理。否则某些子类忘了 close 连接就会泄漏。八、常见问题FAQQ模板方法和策略模式什么时候该用哪个A如果变化的只是某一步的算法选择用策略模式组合如果有多个步骤需要变化且这些步骤之间有顺序依赖用模板方法继承。实际项目中经常结合使用——模板方法控制流程骨架每一步内部可以委托给策略。Q模板方法的子类可以改变步骤顺序吗A不应该改变。模板方法的价值就在于流程不变细节可变。如果流程本身也需要变化应该用策略模式或责任链模式。Q如何避免继承层次太深A① 控制在 2-3 层以内② 用 Lambda 回调代替子类Java 8 场景③ 用组合策略代替继承。如果发现继承超过 3 层通常意味着需要重构。QSpring 里大量使用模板方法吗A是的。Spring 的核心设计哲学就是框架控制流程开发者填充细节。JdbcTemplate.execute()、AbstractApplicationContext.refresh()、RestTemplate.doExecute()都是模板方法。用 Spring 的人每天都在用模板方法只是很多人没意识到。Q什么时候应该把步骤做成钩子而不是抽象方法A当这个步骤大部分子类不需要重写时设为钩子有默认空实现。当这个步骤每个子类都不一样、必须提供实现时设为抽象方法。判断标准是默认不做任何事是否合理。九、小结模板方法模式的核心价值把不变的流程锁死在父类把变化的细节交给子类。三个实践要点模板方法加 final——锁死流程顺序防止子类越界区分抽象步骤和钩子——必须实现的用 abstract可选的给空实现当继承变复杂时切换到组合——用 Lambda/策略代替子类下一篇我们聊观察者模式——当一个对象状态变化需要通知多个关注方时如何做到松耦合的事件分发。标签#设计模式 #模板方法 #TemplateMethod #行为型模式 #Java #继承 #钩子方法 #Spring #框架设计 #代码复用 #面向对象 #软件工程