[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-14":26,"\u002Farticles\u002Fdesign-pattern-14\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-14\u002Fadjacent":71,"\u002Fcategories":75,"render-design-pattern-14":101},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113933745433088002","design-pattern-14","设计模式修炼之路 ⑭ 模板模式——焊死骨架，留白细节","final 焊死流程骨架、四种方法各司其职、钩子是选答题、从 Servlet 到 AQS 全是填空","2113897628365230081",1,3418,8834,23,"2024-06-13T15:33:43","上一篇代理模式收官结构型时留足了排面：\"结构型管'类怎么组装'，行为型管'类怎么协作'——模板模式打头阵，干的事你天天在干：父类把流程骨架焊死，子类只往空位里填肉。\"今天它来了：**模板模式（Template Pattern）**，GoF 23 人名单正牌选手、行为型十一员的第一位（本系列含编外的空对象共十二篇）。系列正式进入下半场——创建型管\"对象从哪来\"，结构型管\"类怎么组装\"，行为型管\"类与类怎么配合干活\"。前十三篇的模式你也许还能说\"平时真没主动写过\"，从这一篇开始不行了：HttpServlet、AbstractList、JUnit、JdbcTemplate、AQS，这些你天天摸的东西，骨子里全是它。\n\n## 一、什么是模板模式？\n\n先看定义：\n\n> 定义一个操作中算法的骨架，而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下，重定义算法的某些步骤。\n\n翻译成人话：**同一套流程在好几个类里重复出现，只有个别步骤不一样——把流程原样搬到父类里焊成 final，不一样的步骤留成空，子类只管填空**。步骤顺序、调用时机，全归父类管；每一步\"具体怎么干\"，归子类管。\n\n生活类比是考试卷：题型、题序、分值，在卷子印好的那一刻全定死，你只能在横线上填答案。选择题不会可以蒙个 C（钩子的默认实现），但你说\"我不想做选择题\"、或者想把大题挪到第一页——监考老师（final）第一个不答应。\n\n它只有两个角色，但父类肚子里装着**四种方法**——这是本篇最值钱的知识点：\n\n- **模板方法（template method）**：public final 修饰，定义流程骨架，按顺序调用下面三种方法。final 是灵魂：流程焊死，子类连重写的资格都没有；\n- **抽象方法**：必填的空。子类不填，编译器不放行；\n- **公共方法（concrete method）**：父类实现好的公共步骤，所有子类共享一份；\n- **钩子方法（hook）**：父类给一个\"人畜无害\"的默认实现（空方法，或返回默认值），子类想覆盖就覆盖，不覆盖也不碍事。\n\n精髓一句话：**变的留给子类填空，不变的焊在父类共享；顺序父类说了算，细节子类说了算**。\n\n## 二、模板模式的优缺点\n\n### 优点\n\n1. **消灭重复，一处修改全局生效**：公共步骤只在父类存在一份，\"日志要加时间戳\"这种需求从改 N 个类变成改 1 个类；\n2. **流程受编译器保护**：\"不改变算法结构\"不是注释里的君子协定，是 final 关键字执法——先校验后发送这种铁律，子类想破也破不了；\n3. **加变体就是加子类**：新渠道、新格式，extends 一个类填几个空就完事，开闭原则模板分；\n4. **好莱坞原则（Hollywood Principle）**：Don't call us, we'll call you——父类调子类，子类不调父类；框架调你，你不调框架。控制权牢牢握在父类手里，这个思想后来有个更响亮的名字：控制反转，Spring IoC 的源头之一。\n\n### 缺点\n\n1. **类数量膨胀**：一个变体一个子类，渠道一多，包里全是 XxxSender；\n2. **继承是硬绑定**：父类一动，波及全部子类（脆弱基类问题）；而且每个子类都烧掉了 Java 单继承的唯一名额——想同时复用两套骨架，没门；\n3. **阅读要上下横跳**：流程在父类、实现在子类，追一个调用两头跑，IDE 的调用层级树成了标配；\n4. **骨架太死偶尔憋屈**：个别子类想在流程中间插一步？只能指望父类当初埋了钩子——埋钩子是框架作者在赌自己预见得全。\n\n## 三、模板模式的应用场景\n\n- **多个类流程相同、个别步骤不同**：多渠道消息推送、多支付渠道对接、多格式文件导出、一切\"对接第 N 方\"的活——第四节就拿它当例子；\n- **框架定流程、用户填空**：框架设计的第一模式，没有之一。框架作者写父类焊骨架，框架用户写子类填空——第五节你会看到 Servlet、JUnit、JdbcTemplate、AQS、Spring Boot 启动全在这条路上；\n- **需要强制流程一致性的地方**：必须先校验再操作、必须先开事务再执行最后提交、必须 try-finally 关资源——焊进模板方法，新人想写错都难。\n\n**什么时候别用**：流程只有一套、没有第二个变体——别为\"可能有变化\"上抽象父类，那是过度设计；变化的不是个别步骤而是整个算法——那是下一篇策略模式的地盘。\n\n## 四、模板模式的实现方式\n\n示例代码在 design-pattern 项目的 template 包里：\n\n```\ndesign-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── template                        \u002F\u002F 模板模式\n    ├── BadSmsSender.java           \u002F\u002F 反面教材：短信渠道复制粘贴\n    ├── BadEmailSender.java         \u002F\u002F 反面教材：邮件渠道再复制一遍\n    ├── AbstractMessageSender.java  \u002F\u002F 抽象类：模板方法 + 三个必填空 + 一个钩子\n    ├── SmsSender.java              \u002F\u002F 填空 1：短信\n    ├── EmailSender.java            \u002F\u002F 填空 2：邮件\n    ├── DingTalkSender.java         \u002F\u002F 填空 3：钉钉，覆盖钩子开回执\n    └── TemplateTest.java           \u002F\u002F 测试入口\n```\n\n场景：消息推送。短信、邮件、钉钉三个渠道，流程一模一样——校验目标、渲染内容、真正发送、记录日志，一步不多一步不少；只有每一步\"具体怎么干\"各不相同。\n\n### 1. 反面教材：每个渠道复制一遍流程\n\n```java\n\u002F\u002F 反面教材：短信渠道，四步流程全写在业务里\npublic class BadSmsSender {\n    public void send(String target, String userName) {\n        if (!target.matches(\"1\\\\d{10}\")) {\n            System.out.println(\"[坏例子-短信] 手机号不合法：\" + target);\n            return;\n        }\n        String content = \"【商城】尊敬的\" + userName + \"，您的验证码 666888，5 分钟内有效\";\n        System.out.println(\"[坏例子-短信] 调用短信网关，发往 \" + target);\n        System.out.println(\"[坏例子-短信] 记录发送日志：\" + target + \" -> \" + content);\n    }\n}\n\n\u002F\u002F 反面教材：邮件渠道——结构一模一样，再复制一遍\npublic class BadEmailSender {\n    public void send(String target, String userName) {\n        if (!target.contains(\"@\")) {\n            System.out.println(\"[坏例子-邮件] 邮箱不合法：\" + target);\n            return;\n        }\n        String content = \"\u003Chtml>\u003Cbody>尊敬的 \" + userName + \"，您本月的账单已出，请查收\u003C\u002Fbody>\u003C\u002Fhtml>\";\n        System.out.println(\"[坏例子-邮件] 通过 SMTP 发往 \" + target);\n        System.out.println(\"[坏例子-邮件] 记录发送日志：\" + target + \" -> \" + content);\n    }\n}\n```\n\n运行结果：\n\n```\n===== 反面教材：每个渠道把流程复制一遍 =====\n[坏例子-短信] 调用短信网关，发往 13800138000\n[坏例子-短信] 记录发送日志：13800138000 -> 【商城】尊敬的张三，您的验证码 666888，5 分钟内有效\n[坏例子-邮件] 通过 SMTP 发往 zhangsan@example.com\n[坏例子-邮件] 记录发送日志：zhangsan@example.com -> \u003Chtml>\u003Cbody>尊敬的 张三，您本月的账单已出，请查收\u003C\u002Fbody>\u003C\u002Fhtml>\n```\n\n两个类，四步流程，肉眼可见地重复。三宗罪：\n\n1. **公共代码复制了 N 份**：\"记日志\"这步逐字相同。哪天产品说\"日志要带时间戳\"，两个类都得改；渠道加到五个，改五处，漏一处就是线上事故；\n2. **流程顺序无保障**：新同事接手写钉钉渠道，\"先发送、后校验\"照样编译通过——没有任何东西拦得住他，直到垃圾短信真的发了出去；\n3. **新渠道只能靠复制**：Ctrl+C、Ctrl+V、再逐行改——复制十行改三行，剩下七行就是将来所有 bug 的藏身处。\n\n病根一脉相承：该共享的没共享，该隔离的没隔离——只是这次重复的是流程，变化的是步骤。\n\n### 2. 正解：流程上提父类，焊成 final\n\n```java\n\u002F\u002F 抽象类：四种方法各就各位\npublic abstract class AbstractMessageSender {\n\n    \u002F\u002F 模板方法：final 焊死流程骨架——校验、渲染、发送、记日志的顺序，谁也改不了\n    public final void send(String target, String userName) {\n        if (!checkTarget(target)) {\n            System.out.println(\"  [模板] 目标地址不合法，中止发送：\" + target);\n            return;\n        }\n        System.out.println(\"  [模板] 目标校验通过：\" + target);\n        String content = render(userName);\n        System.out.println(\"  [模板] 渲染内容：\" + content);\n        doSend(target, content);\n        System.out.println(\"  [模板] 记录发送日志：\" + target);\n        if (needReceipt()) {\n            System.out.println(\"  [模板] 开启回执确认，等待对方已读\");\n        }\n    }\n\n    \u002F\u002F 抽象方法：必填的空——每种渠道的校验规则不同\n    protected abstract boolean checkTarget(String target);\n\n    \u002F\u002F 抽象方法：必填的空——每种渠道的内容模板不同\n    protected abstract String render(String userName);\n\n    \u002F\u002F 抽象方法：必填的空——每种渠道的发送方式不同\n    protected abstract void doSend(String target, String content);\n\n    \u002F\u002F 钩子方法：父类给默认实现（不开回执），子类看需要覆盖\n    protected boolean needReceipt() {\n        return false;\n    }\n}\n```\n\n对着第一节那张\"四种方法\"清单逐个对号：`send()` 是模板方法，final 修饰，四步顺序焊死；`checkTarget()` \u002F `render()` \u002F `doSend()` 是三个必填的抽象方法；记日志直接写死在骨架里，是公共方法；`needReceipt()` 是钩子。\n\n子类从此只干填空这一件事：\n\n```java\n\u002F\u002F 填空 1：短信渠道\npublic class SmsSender extends AbstractMessageSender {\n\n    @Override\n    protected boolean checkTarget(String target) {\n        return target.matches(\"1\\\\d{10}\");\n    }\n\n    @Override\n    protected String render(String userName) {\n        return \"【商城】尊敬的\" + userName + \"，您的验证码 666888，5 分钟内有效\";\n    }\n\n    @Override\n    protected void doSend(String target, String content) {\n        System.out.println(\"  [短信] 调用短信网关，发往 \" + target);\n    }\n}\n\n\u002F\u002F 填空 3：钉钉渠道——顺手覆盖钩子\npublic class DingTalkSender extends AbstractMessageSender {\n\n    @Override\n    protected boolean checkTarget(String target) {\n        return target.startsWith(\"ding:\");\n    }\n\n    @Override\n    protected String render(String userName) {\n        return \"【重要告警】\" + userName + \"，线上服务异常，请立即处理\";\n    }\n\n    @Override\n    protected void doSend(String target, String content) {\n        System.out.println(\"  [钉钉] 调用机器人 webhook 推送：\" + target);\n    }\n\n    \u002F\u002F 覆盖钩子：告警消息必须确认已读\n    @Override\n    protected boolean needReceipt() {\n        return true;\n    }\n}\n```\n\nEmailSender 结构完全相同，不再赘列。测试：\n\n```java\nSystem.out.println(\"===== 正解：模板模式，骨架焊死、子类填空 =====\");\nSystem.out.println(\"--- 短信渠道 ---\");\nAbstractMessageSender sms = new SmsSender();\nsms.send(\"13800138000\", \"张三\");\nsms.send(\"12345\", \"张三\");\n\nSystem.out.println(\"--- 邮件渠道 ---\");\nAbstractMessageSender email = new EmailSender();\nemail.send(\"zhangsan@example.com\", \"张三\");\n\nSystem.out.println(\"--- 钉钉渠道（覆盖钩子，开回执） ---\");\nAbstractMessageSender dingTalk = new DingTalkSender();\ndingTalk.send(\"ding:9527\", \"张三\");\n```\n\n运行结果：\n\n```\n===== 正解：模板模式，骨架焊死、子类填空 =====\n--- 短信渠道 ---\n  [模板] 目标校验通过：13800138000\n  [模板] 渲染内容：【商城】尊敬的张三，您的验证码 666888，5 分钟内有效\n  [短信] 调用短信网关，发往 13800138000\n  [模板] 记录发送日志：13800138000\n  [模板] 目标地址不合法，中止发送：12345\n--- 邮件渠道 ---\n  [模板] 目标校验通过：zhangsan@example.com\n  [模板] 渲染内容：\u003Chtml>\u003Cbody>尊敬的 张三，您本月的账单已出，请查收\u003C\u002Fbody>\u003C\u002Fhtml>\n  [邮件] 通过 SMTP 发往 zhangsan@example.com\n  [模板] 记录发送日志：zhangsan@example.com\n--- 钉钉渠道（覆盖钩子，开回执） ---\n  [模板] 目标校验通过：ding:9527\n  [模板] 渲染内容：【重要告警】张三，线上服务异常，请立即处理\n  [钉钉] 调用机器人 webhook 推送：ding:9527\n  [模板] 记录发送日志：ding:9527\n  [模板] 开启回执确认，等待对方已读\n```\n\n三处细节值得咂摸：\n\n1. **\"12345\" 那单被拦在校验**：校验顺序是父类定的，SmsSender 只提供了\"什么算合法手机号\"，没有机会说\"先发了再说\"——final 的功劳；\n2. **三个渠道的日志格式天然一致**：因为记日志的代码全程序只有一份，在父类骨架里；\n3. **钉钉比别家多一行\"开启回执确认\"**：它覆盖了钩子 needReceipt() 返回 true，而短信、邮件对\"回执\"这件事毫不知情、毫发无损——这就是钩子的分寸感：默认不动声色，需要才出手。\n\n### 3. 钩子方法：必答题和选答题\n\n钩子常见两种形态，上面见了一种，补上另一种：\n\n1. **boolean 开关**：父类骨架里写 `if (needReceipt()) {...}`，子类覆盖返回值，决定\"这一步跑不跑\"；\n2. **空方法**：父类给 `protected void afterSend() {}` 空实现，骨架末尾调用它，子类想要\"发送后再干点什么\"就覆盖。\n\n钩子和抽象方法的本质区别一句话：**抽象方法是必答题，钩子是选答题**。子类十有八九用不上某个钩子，但父类必须提前埋好——埋少了子类憋屈，埋多了父类臃肿，这个分寸是框架作者的功力所在。\n\n## 五、在框架里找它\n\n按\"填空姿势\"从传统到现代排开，全是你天天摸的东西。\n\n### 1. HttpServlet：service() 焊死分发骨架\n\n你写 Servlet 十年，从没写过 service()——因为它在 HttpServlet 里焊死了：解析请求 → 取出请求方法 → 是 GET 调 doGet、是 POST 调 doPost。你写的 doGet\u002FdoPost 就是填的空。更有意思的是，doGet 在 HttpServlet 里自带默认实现：返回 405 Method Not Allowed——你不覆盖它，容器就替你回一个 405。**默认实现返回错误码**，这是钩子最硬核的用法：\"你要么好好填，要么我明确告诉你这题没答\"。\n\n### 2. AbstractList：JDK 的骨架实现\n\n想实现一个 List，别直接 implements List——那意味着几十个方法全得自己写。继承 AbstractList，只填 `get(int)` 和 `size()` 两个空，iterator()、indexOf()、addAll() 父类全用你的 get() 拼好了。ArrayList 是它的子类，LinkedList 的父类 AbstractSequentialList 也是它的子类。同门还有 AbstractMap、AbstractSet、AbstractCollection。《Effective Java》第 20 条专门讲这招，术语叫**骨架实现（skeletal implementation）**：接口定契约，抽象类给骨架，你只填核心那几个空。\n\n### 3. JUnit：生命周期也是填空\n\nJUnit 4 的 BlockJUnit4ClassRunner 里焊死了一条流水线：跑 @Before 们 → 跑 @Test → 跑 @After 们，超时控制、异常捕获、@Ignore 处理全在骨架里。你写的 @Test 是必填的空，@Before\u002F@After 是选填的空——把\"每个用例前后要做的事\"做成填空位，这就是测试框架十几年不变的地基。\n\n### 4. JdbcTemplate：填空从覆盖方法进化成传 Lambda\n\n获取连接 → 创建语句 → 执行 SQL → 遍历 ResultSet → 关资源，五步全是容易写错的事（忘了关、忘了 try-finally），JdbcTemplate 全部焊死，你只填一个 RowMapper：\n\n```java\nUser u = jdbcTemplate.queryForObject(\n        \"select * from user where id = ?\",\n        (rs, rowNum) -> new User(rs.getLong(\"id\"), rs.getString(\"name\")), \u002F\u002F 填空：一行怎么变对象\n        userId);\n```\n\n注意变化：**填空的载体从\"继承父类覆盖方法\"变成了\"传一个 Lambda 进来\"**。模板思想一点没变——骨架焊死、细节外置——只是实现从继承换成了组合。JDK 8 之后的新框架越来越多走这条路：回调即填空，这是模板模式在 Lambda 时代的进化形态。\n\n### 5. AQS：并发框架的总骨架\n\n面试必问的 AQS，全名 AbstractQueuedSynchronizer，名字自带 Abstract。acquire() 焊死了\"tryAcquire 抢锁 → 抢不到入队 → 阻塞排队等唤醒\"整套骨架，ReentrantLock、Semaphore、CountDownLatch 各填各的 tryAcquire\u002FtryRelease。所谓\"读懂 AQS 源码\"，一半就是在读一个巨型模板方法。\n\n### 6. Spring 的 refresh()：一次方法调用启动整个容器\n\nAbstractApplicationContext.refresh() 把 Spring 启动焊成十几步：prepareBeanFactory → invokeBeanFactoryPostProcessors → registerBeanPostProcessors → onRefresh → finishBeanFactoryInitialization……其中 onRefresh() 是留给子类的空方法，默认实现一行\"无事可做\"。SpringBoot 的 ServletWebServerApplicationContext 重写了它，在那一格空里创建内嵌 Tomcat。所以你每次启动 SpringBoot 应用，本质就是调用了一次模板方法——**内嵌 Web 容器，只是它填的一个空**。\n\n## 六、模板 vs 策略：把\"变化\"抽出去的两种手法\n\n老规矩，把容易混淆的邻居拉出来遛遛。模板和策略干的是同一件事——**把变化的部分抽出去**——但抽法和粒度完全不同：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>模板模式\u003C\u002Fth>\u003Cth>策略模式\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>手段\u003C\u002Ftd>\u003Ctd>继承\u003C\u002Ftd>\u003Ctd>组合\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>变化的粒度\u003C\u002Ftd>\u003Ctd>流程里的个别步骤\u003C\u002Ftd>\u003Ctd>整个算法整体替换\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>有没有固定流程\u003C\u002Ftd>\u003Ctd>有，骨架焊死\u003C\u002Ftd>\u003Ctd>没有，每个策略独立完整\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>切换时机\u003C\u002Ftd>\u003Ctd>编译期定死（new 哪个子类就是哪个）\u003C\u002Ftd>\u003Ctd>运行时随便换（set 进去就行）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>耦合\u003C\u002Ftd>\u003Ctd>is-a，强绑定\u003C\u002Ftd>\u003Ctd>has-a，松耦合\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n判别一句话：**流程不变、只换某几步，用模板；整个打法互换、没有共同骨架，用策略**。本篇的推送是模板——\"校验→渲染→发送→记日志\"为三个渠道共享；排序是策略——快排换冒泡，换的是整个算法，中间没有可共享的流程。\n\n模板还有个先天短板第二节埋过伏笔：靠继承，每个子类烧掉 Java 单继承的唯一名额。JdbcTemplate 已经示范了出路——模板思想完全可以用组合承载，填空从覆盖方法变成传回调。**继承版模板是 1994 年的答案，回调版模板是这个时代的答案**——但无论哪版，\"骨架焊死、细节外置\"八个字从没变过。\n\n## 写在最后\n\n回顾这一篇：模板模式把重复出现的流程上提到父类，焊成 final 模板方法；子类只填抽象方法那些必填的空、按需覆盖钩子那些选填的空——模板方法、抽象方法、公共方法、钩子，四种方法各司其职；final 让\"不许改流程\"从口头约定变成编译器执法；好莱坞原则\"父类调子类\"是控制反转的雏形；从 HttpServlet、AbstractList 到 JUnit、JdbcTemplate、AQS、Spring refresh，框架世界的半壁江山都是\"框架焊骨架、用户填空\"这一个套路；它和策略的分界在粒度——换步骤是模板，换打法是策略。行为型的第一位，也是你每天用得最多的一位。\n\n下一篇 ⑮ 策略模式——模板填的是流程里的某几步，策略换的是整个打法。它还有个响亮的外号：**if-else 的解药**。你代码里那个嵌套五层、每来一个新需求就得多写一个 else if 的方法，读完下一篇就有解了。\n","\u003Cp>上一篇代理模式收官结构型时留足了排面：\"结构型管'类怎么组装'，行为型管'类怎么协作'——模板模式打头阵，干的事你天天在干：父类把流程骨架焊死，子类只往空位里填肉。\"今天它来了：\u003Cstrong>模板模式（Template Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、行为型十一员的第一位（本系列含编外的空对象共十二篇）。系列正式进入下半场——创建型管\"对象从哪来\"，结构型管\"类怎么组装\"，行为型管\"类与类怎么配合干活\"。前十三篇的模式你也许还能说\"平时真没主动写过\"，从这一篇开始不行了：HttpServlet、AbstractList、JUnit、JdbcTemplate、AQS，这些你天天摸的东西，骨子里全是它。\u003C\u002Fp>\n\u003Ch2>一、什么是模板模式？\u003C\u002Fh2>\n\u003Cp>先看定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>定义一个操作中算法的骨架，而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下，重定义算法的某些步骤。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：\u003Cstrong>同一套流程在好几个类里重复出现，只有个别步骤不一样——把流程原样搬到父类里焊成 final，不一样的步骤留成空，子类只管填空\u003C\u002Fstrong>。步骤顺序、调用时机，全归父类管；每一步\"具体怎么干\"，归子类管。\u003C\u002Fp>\n\u003Cp>生活类比是考试卷：题型、题序、分值，在卷子印好的那一刻全定死，你只能在横线上填答案。选择题不会可以蒙个 C（钩子的默认实现），但你说\"我不想做选择题\"、或者想把大题挪到第一页——监考老师（final）第一个不答应。\u003C\u002Fp>\n\u003Cp>它只有两个角色，但父类肚子里装着\u003Cstrong>四种方法\u003C\u002Fstrong>——这是本篇最值钱的知识点：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>模板方法（template method）\u003C\u002Fstrong>：public final 修饰，定义流程骨架，按顺序调用下面三种方法。final 是灵魂：流程焊死，子类连重写的资格都没有；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>抽象方法\u003C\u002Fstrong>：必填的空。子类不填，编译器不放行；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>公共方法（concrete method）\u003C\u002Fstrong>：父类实现好的公共步骤，所有子类共享一份；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>钩子方法（hook）\u003C\u002Fstrong>：父类给一个\"人畜无害\"的默认实现（空方法，或返回默认值），子类想覆盖就覆盖，不覆盖也不碍事。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓一句话：\u003Cstrong>变的留给子类填空，不变的焊在父类共享；顺序父类说了算，细节子类说了算\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2>二、模板模式的优缺点\u003C\u002Fh2>\n\u003Ch3>优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>消灭重复，一处修改全局生效\u003C\u002Fstrong>：公共步骤只在父类存在一份，\"日志要加时间戳\"这种需求从改 N 个类变成改 1 个类；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>流程受编译器保护\u003C\u002Fstrong>：\"不改变算法结构\"不是注释里的君子协定，是 final 关键字执法——先校验后发送这种铁律，子类想破也破不了；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>加变体就是加子类\u003C\u002Fstrong>：新渠道、新格式，extends 一个类填几个空就完事，开闭原则模板分；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>好莱坞原则（Hollywood Principle）\u003C\u002Fstrong>：Don't call us, we'll call you——父类调子类，子类不调父类；框架调你，你不调框架。控制权牢牢握在父类手里，这个思想后来有个更响亮的名字：控制反转，Spring IoC 的源头之一。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>缺点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量膨胀\u003C\u002Fstrong>：一个变体一个子类，渠道一多，包里全是 XxxSender；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>继承是硬绑定\u003C\u002Fstrong>：父类一动，波及全部子类（脆弱基类问题）；而且每个子类都烧掉了 Java 单继承的唯一名额——想同时复用两套骨架，没门；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>阅读要上下横跳\u003C\u002Fstrong>：流程在父类、实现在子类，追一个调用两头跑，IDE 的调用层级树成了标配；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>骨架太死偶尔憋屈\u003C\u002Fstrong>：个别子类想在流程中间插一步？只能指望父类当初埋了钩子——埋钩子是框架作者在赌自己预见得全。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、模板模式的应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>\u003Cstrong>多个类流程相同、个别步骤不同\u003C\u002Fstrong>：多渠道消息推送、多支付渠道对接、多格式文件导出、一切\"对接第 N 方\"的活——第四节就拿它当例子；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>框架定流程、用户填空\u003C\u002Fstrong>：框架设计的第一模式，没有之一。框架作者写父类焊骨架，框架用户写子类填空——第五节你会看到 Servlet、JUnit、JdbcTemplate、AQS、Spring Boot 启动全在这条路上；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>需要强制流程一致性的地方\u003C\u002Fstrong>：必须先校验再操作、必须先开事务再执行最后提交、必须 try-finally 关资源——焊进模板方法，新人想写错都难。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候别用\u003C\u002Fstrong>：流程只有一套、没有第二个变体——别为\"可能有变化\"上抽象父类，那是过度设计；变化的不是个别步骤而是整个算法——那是下一篇策略模式的地盘。\u003C\u002Fp>\n\u003Ch2>四、模板模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 template 包里：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>design-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── template                        \u002F\u002F 模板模式\n    ├── BadSmsSender.java           \u002F\u002F 反面教材：短信渠道复制粘贴\n    ├── BadEmailSender.java         \u002F\u002F 反面教材：邮件渠道再复制一遍\n    ├── AbstractMessageSender.java  \u002F\u002F 抽象类：模板方法 + 三个必填空 + 一个钩子\n    ├── SmsSender.java              \u002F\u002F 填空 1：短信\n    ├── EmailSender.java            \u002F\u002F 填空 2：邮件\n    ├── DingTalkSender.java         \u002F\u002F 填空 3：钉钉，覆盖钩子开回执\n    └── TemplateTest.java           \u002F\u002F 测试入口\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：消息推送。短信、邮件、钉钉三个渠道，流程一模一样——校验目标、渲染内容、真正发送、记录日志，一步不多一步不少；只有每一步\"具体怎么干\"各不相同。\u003C\u002Fp>\n\u003Ch3>1. 反面教材：每个渠道复制一遍流程\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 反面教材：短信渠道，四步流程全写在业务里\npublic class BadSmsSender {\n    public void send(String target, String userName) {\n        if (!target.matches(\"1\\\\d{10}\")) {\n            System.out.println(\"[坏例子-短信] 手机号不合法：\" + target);\n            return;\n        }\n        String content = \"【商城】尊敬的\" + userName + \"，您的验证码 666888，5 分钟内有效\";\n        System.out.println(\"[坏例子-短信] 调用短信网关，发往 \" + target);\n        System.out.println(\"[坏例子-短信] 记录发送日志：\" + target + \" -&gt; \" + content);\n    }\n}\n\n\u002F\u002F 反面教材：邮件渠道——结构一模一样，再复制一遍\npublic class BadEmailSender {\n    public void send(String target, String userName) {\n        if (!target.contains(\"@\")) {\n            System.out.println(\"[坏例子-邮件] 邮箱不合法：\" + target);\n            return;\n        }\n        String content = \"&lt;html&gt;&lt;body&gt;尊敬的 \" + userName + \"，您本月的账单已出，请查收&lt;\u002Fbody&gt;&lt;\u002Fhtml&gt;\";\n        System.out.println(\"[坏例子-邮件] 通过 SMTP 发往 \" + target);\n        System.out.println(\"[坏例子-邮件] 记录发送日志：\" + target + \" -&gt; \" + content);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>===== 反面教材：每个渠道把流程复制一遍 =====\n[坏例子-短信] 调用短信网关，发往 13800138000\n[坏例子-短信] 记录发送日志：13800138000 -&gt; 【商城】尊敬的张三，您的验证码 666888，5 分钟内有效\n[坏例子-邮件] 通过 SMTP 发往 zhangsan@example.com\n[坏例子-邮件] 记录发送日志：zhangsan@example.com -&gt; &lt;html&gt;&lt;body&gt;尊敬的 张三，您本月的账单已出，请查收&lt;\u002Fbody&gt;&lt;\u002Fhtml&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>两个类，四步流程，肉眼可见地重复。三宗罪：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>公共代码复制了 N 份\u003C\u002Fstrong>：\"记日志\"这步逐字相同。哪天产品说\"日志要带时间戳\"，两个类都得改；渠道加到五个，改五处，漏一处就是线上事故；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>流程顺序无保障\u003C\u002Fstrong>：新同事接手写钉钉渠道，\"先发送、后校验\"照样编译通过——没有任何东西拦得住他，直到垃圾短信真的发了出去；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>新渠道只能靠复制\u003C\u002Fstrong>：Ctrl+C、Ctrl+V、再逐行改——复制十行改三行，剩下七行就是将来所有 bug 的藏身处。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>病根一脉相承：该共享的没共享，该隔离的没隔离——只是这次重复的是流程，变化的是步骤。\u003C\u002Fp>\n\u003Ch3>2. 正解：流程上提父类，焊成 final\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 抽象类：四种方法各就各位\npublic abstract class AbstractMessageSender {\n\n    \u002F\u002F 模板方法：final 焊死流程骨架——校验、渲染、发送、记日志的顺序，谁也改不了\n    public final void send(String target, String userName) {\n        if (!checkTarget(target)) {\n            System.out.println(\"  [模板] 目标地址不合法，中止发送：\" + target);\n            return;\n        }\n        System.out.println(\"  [模板] 目标校验通过：\" + target);\n        String content = render(userName);\n        System.out.println(\"  [模板] 渲染内容：\" + content);\n        doSend(target, content);\n        System.out.println(\"  [模板] 记录发送日志：\" + target);\n        if (needReceipt()) {\n            System.out.println(\"  [模板] 开启回执确认，等待对方已读\");\n        }\n    }\n\n    \u002F\u002F 抽象方法：必填的空——每种渠道的校验规则不同\n    protected abstract boolean checkTarget(String target);\n\n    \u002F\u002F 抽象方法：必填的空——每种渠道的内容模板不同\n    protected abstract String render(String userName);\n\n    \u002F\u002F 抽象方法：必填的空——每种渠道的发送方式不同\n    protected abstract void doSend(String target, String content);\n\n    \u002F\u002F 钩子方法：父类给默认实现（不开回执），子类看需要覆盖\n    protected boolean needReceipt() {\n        return false;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>对着第一节那张\"四种方法\"清单逐个对号：\u003Ccode>send()\u003C\u002Fcode> 是模板方法，final 修饰，四步顺序焊死；\u003Ccode>checkTarget()\u003C\u002Fcode> \u002F \u003Ccode>render()\u003C\u002Fcode> \u002F \u003Ccode>doSend()\u003C\u002Fcode> 是三个必填的抽象方法；记日志直接写死在骨架里，是公共方法；\u003Ccode>needReceipt()\u003C\u002Fcode> 是钩子。\u003C\u002Fp>\n\u003Cp>子类从此只干填空这一件事：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 填空 1：短信渠道\npublic class SmsSender extends AbstractMessageSender {\n\n    @Override\n    protected boolean checkTarget(String target) {\n        return target.matches(\"1\\\\d{10}\");\n    }\n\n    @Override\n    protected String render(String userName) {\n        return \"【商城】尊敬的\" + userName + \"，您的验证码 666888，5 分钟内有效\";\n    }\n\n    @Override\n    protected void doSend(String target, String content) {\n        System.out.println(\"  [短信] 调用短信网关，发往 \" + target);\n    }\n}\n\n\u002F\u002F 填空 3：钉钉渠道——顺手覆盖钩子\npublic class DingTalkSender extends AbstractMessageSender {\n\n    @Override\n    protected boolean checkTarget(String target) {\n        return target.startsWith(\"ding:\");\n    }\n\n    @Override\n    protected String render(String userName) {\n        return \"【重要告警】\" + userName + \"，线上服务异常，请立即处理\";\n    }\n\n    @Override\n    protected void doSend(String target, String content) {\n        System.out.println(\"  [钉钉] 调用机器人 webhook 推送：\" + target);\n    }\n\n    \u002F\u002F 覆盖钩子：告警消息必须确认已读\n    @Override\n    protected boolean needReceipt() {\n        return true;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>EmailSender 结构完全相同，不再赘列。测试：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">System.out.println(\"===== 正解：模板模式，骨架焊死、子类填空 =====\");\nSystem.out.println(\"--- 短信渠道 ---\");\nAbstractMessageSender sms = new SmsSender();\nsms.send(\"13800138000\", \"张三\");\nsms.send(\"12345\", \"张三\");\n\nSystem.out.println(\"--- 邮件渠道 ---\");\nAbstractMessageSender email = new EmailSender();\nemail.send(\"zhangsan@example.com\", \"张三\");\n\nSystem.out.println(\"--- 钉钉渠道（覆盖钩子，开回执） ---\");\nAbstractMessageSender dingTalk = new DingTalkSender();\ndingTalk.send(\"ding:9527\", \"张三\");\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>===== 正解：模板模式，骨架焊死、子类填空 =====\n--- 短信渠道 ---\n  [模板] 目标校验通过：13800138000\n  [模板] 渲染内容：【商城】尊敬的张三，您的验证码 666888，5 分钟内有效\n  [短信] 调用短信网关，发往 13800138000\n  [模板] 记录发送日志：13800138000\n  [模板] 目标地址不合法，中止发送：12345\n--- 邮件渠道 ---\n  [模板] 目标校验通过：zhangsan@example.com\n  [模板] 渲染内容：&lt;html&gt;&lt;body&gt;尊敬的 张三，您本月的账单已出，请查收&lt;\u002Fbody&gt;&lt;\u002Fhtml&gt;\n  [邮件] 通过 SMTP 发往 zhangsan@example.com\n  [模板] 记录发送日志：zhangsan@example.com\n--- 钉钉渠道（覆盖钩子，开回执） ---\n  [模板] 目标校验通过：ding:9527\n  [模板] 渲染内容：【重要告警】张三，线上服务异常，请立即处理\n  [钉钉] 调用机器人 webhook 推送：ding:9527\n  [模板] 记录发送日志：ding:9527\n  [模板] 开启回执确认，等待对方已读\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处细节值得咂摸：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>\"12345\" 那单被拦在校验\u003C\u002Fstrong>：校验顺序是父类定的，SmsSender 只提供了\"什么算合法手机号\"，没有机会说\"先发了再说\"——final 的功劳；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>三个渠道的日志格式天然一致\u003C\u002Fstrong>：因为记日志的代码全程序只有一份，在父类骨架里；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>钉钉比别家多一行\"开启回执确认\"\u003C\u002Fstrong>：它覆盖了钩子 needReceipt() 返回 true，而短信、邮件对\"回执\"这件事毫不知情、毫发无损——这就是钩子的分寸感：默认不动声色，需要才出手。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>3. 钩子方法：必答题和选答题\u003C\u002Fh3>\n\u003Cp>钩子常见两种形态，上面见了一种，补上另一种：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>boolean 开关\u003C\u002Fstrong>：父类骨架里写 \u003Ccode>if (needReceipt()) {...}\u003C\u002Fcode>，子类覆盖返回值，决定\"这一步跑不跑\"；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>空方法\u003C\u002Fstrong>：父类给 \u003Ccode>protected void afterSend() {}\u003C\u002Fcode> 空实现，骨架末尾调用它，子类想要\"发送后再干点什么\"就覆盖。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>钩子和抽象方法的本质区别一句话：\u003Cstrong>抽象方法是必答题，钩子是选答题\u003C\u002Fstrong>。子类十有八九用不上某个钩子，但父类必须提前埋好——埋少了子类憋屈，埋多了父类臃肿，这个分寸是框架作者的功力所在。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Cp>按\"填空姿势\"从传统到现代排开，全是你天天摸的东西。\u003C\u002Fp>\n\u003Ch3>1. HttpServlet：service() 焊死分发骨架\u003C\u002Fh3>\n\u003Cp>你写 Servlet 十年，从没写过 service()——因为它在 HttpServlet 里焊死了：解析请求 → 取出请求方法 → 是 GET 调 doGet、是 POST 调 doPost。你写的 doGet\u002FdoPost 就是填的空。更有意思的是，doGet 在 HttpServlet 里自带默认实现：返回 405 Method Not Allowed——你不覆盖它，容器就替你回一个 405。\u003Cstrong>默认实现返回错误码\u003C\u002Fstrong>，这是钩子最硬核的用法：\"你要么好好填，要么我明确告诉你这题没答\"。\u003C\u002Fp>\n\u003Ch3>2. AbstractList：JDK 的骨架实现\u003C\u002Fh3>\n\u003Cp>想实现一个 List，别直接 implements List——那意味着几十个方法全得自己写。继承 AbstractList，只填 \u003Ccode>get(int)\u003C\u002Fcode> 和 \u003Ccode>size()\u003C\u002Fcode> 两个空，iterator()、indexOf()、addAll() 父类全用你的 get() 拼好了。ArrayList 是它的子类，LinkedList 的父类 AbstractSequentialList 也是它的子类。同门还有 AbstractMap、AbstractSet、AbstractCollection。《Effective Java》第 20 条专门讲这招，术语叫\u003Cstrong>骨架实现（skeletal implementation）\u003C\u002Fstrong>：接口定契约，抽象类给骨架，你只填核心那几个空。\u003C\u002Fp>\n\u003Ch3>3. JUnit：生命周期也是填空\u003C\u002Fh3>\n\u003Cp>JUnit 4 的 BlockJUnit4ClassRunner 里焊死了一条流水线：跑 @Before 们 → 跑 @Test → 跑 @After 们，超时控制、异常捕获、@Ignore 处理全在骨架里。你写的 @Test 是必填的空，@Before\u002F@After 是选填的空——把\"每个用例前后要做的事\"做成填空位，这就是测试框架十几年不变的地基。\u003C\u002Fp>\n\u003Ch3>4. JdbcTemplate：填空从覆盖方法进化成传 Lambda\u003C\u002Fh3>\n\u003Cp>获取连接 → 创建语句 → 执行 SQL → 遍历 ResultSet → 关资源，五步全是容易写错的事（忘了关、忘了 try-finally），JdbcTemplate 全部焊死，你只填一个 RowMapper：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">User u = jdbcTemplate.queryForObject(\n        \"select * from user where id = ?\",\n        (rs, rowNum) -&gt; new User(rs.getLong(\"id\"), rs.getString(\"name\")), \u002F\u002F 填空：一行怎么变对象\n        userId);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意变化：\u003Cstrong>填空的载体从\"继承父类覆盖方法\"变成了\"传一个 Lambda 进来\"\u003C\u002Fstrong>。模板思想一点没变——骨架焊死、细节外置——只是实现从继承换成了组合。JDK 8 之后的新框架越来越多走这条路：回调即填空，这是模板模式在 Lambda 时代的进化形态。\u003C\u002Fp>\n\u003Ch3>5. AQS：并发框架的总骨架\u003C\u002Fh3>\n\u003Cp>面试必问的 AQS，全名 AbstractQueuedSynchronizer，名字自带 Abstract。acquire() 焊死了\"tryAcquire 抢锁 → 抢不到入队 → 阻塞排队等唤醒\"整套骨架，ReentrantLock、Semaphore、CountDownLatch 各填各的 tryAcquire\u002FtryRelease。所谓\"读懂 AQS 源码\"，一半就是在读一个巨型模板方法。\u003C\u002Fp>\n\u003Ch3>6. Spring 的 refresh()：一次方法调用启动整个容器\u003C\u002Fh3>\n\u003Cp>AbstractApplicationContext.refresh() 把 Spring 启动焊成十几步：prepareBeanFactory → invokeBeanFactoryPostProcessors → registerBeanPostProcessors → onRefresh → finishBeanFactoryInitialization……其中 onRefresh() 是留给子类的空方法，默认实现一行\"无事可做\"。SpringBoot 的 ServletWebServerApplicationContext 重写了它，在那一格空里创建内嵌 Tomcat。所以你每次启动 SpringBoot 应用，本质就是调用了一次模板方法——\u003Cstrong>内嵌 Web 容器，只是它填的一个空\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2>六、模板 vs 策略：把\"变化\"抽出去的两种手法\u003C\u002Fh2>\n\u003Cp>老规矩，把容易混淆的邻居拉出来遛遛。模板和策略干的是同一件事——\u003Cstrong>把变化的部分抽出去\u003C\u002Fstrong>——但抽法和粒度完全不同：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>模板模式\u003C\u002Fth>\n   \u003Cth>策略模式\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>手段\u003C\u002Ftd>\n   \u003Ctd>继承\u003C\u002Ftd>\n   \u003Ctd>组合\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>变化的粒度\u003C\u002Ftd>\n   \u003Ctd>流程里的个别步骤\u003C\u002Ftd>\n   \u003Ctd>整个算法整体替换\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>有没有固定流程\u003C\u002Ftd>\n   \u003Ctd>有，骨架焊死\u003C\u002Ftd>\n   \u003Ctd>没有，每个策略独立完整\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>切换时机\u003C\u002Ftd>\n   \u003Ctd>编译期定死（new 哪个子类就是哪个）\u003C\u002Ftd>\n   \u003Ctd>运行时随便换（set 进去就行）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>耦合\u003C\u002Ftd>\n   \u003Ctd>is-a，强绑定\u003C\u002Ftd>\n   \u003Ctd>has-a，松耦合\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>判别一句话：\u003Cstrong>流程不变、只换某几步，用模板；整个打法互换、没有共同骨架，用策略\u003C\u002Fstrong>。本篇的推送是模板——\"校验→渲染→发送→记日志\"为三个渠道共享；排序是策略——快排换冒泡，换的是整个算法，中间没有可共享的流程。\u003C\u002Fp>\n\u003Cp>模板还有个先天短板第二节埋过伏笔：靠继承，每个子类烧掉 Java 单继承的唯一名额。JdbcTemplate 已经示范了出路——模板思想完全可以用组合承载，填空从覆盖方法变成传回调。\u003Cstrong>继承版模板是 1994 年的答案，回调版模板是这个时代的答案\u003C\u002Fstrong>——但无论哪版，\"骨架焊死、细节外置\"八个字从没变过。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cp>回顾这一篇：模板模式把重复出现的流程上提到父类，焊成 final 模板方法；子类只填抽象方法那些必填的空、按需覆盖钩子那些选填的空——模板方法、抽象方法、公共方法、钩子，四种方法各司其职；final 让\"不许改流程\"从口头约定变成编译器执法；好莱坞原则\"父类调子类\"是控制反转的雏形；从 HttpServlet、AbstractList 到 JUnit、JdbcTemplate、AQS、Spring refresh，框架世界的半壁江山都是\"框架焊骨架、用户填空\"这一个套路；它和策略的分界在粒度——换步骤是模板，换打法是策略。行为型的第一位，也是你每天用得最多的一位。\u003C\u002Fp>\n\u003Cp>下一篇 ⑮ 策略模式——模板填的是流程里的某几步，策略换的是整个打法。它还有个响亮的外号：\u003Cstrong>if-else 的解药\u003C\u002Fstrong>。你代码里那个嵌套五层、每来一个新需求就得多写一个 else if 的方法，读完下一篇就有解了。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,53,62],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":36,"publishedAt":52},"2113932970753527809","design-pattern-13","设计模式修炼之路 ⑬ 代理模式——控制访问，而非增强","控制访问而非增强、JDK 动态代理的 $Proxy0、CGLIB 与 Spring AOP 的老搭档",3804,9153,"2024-05-02T10:29:24",{"id":54,"slug":55,"title":56,"summary":57,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":58,"likeCount":4,"wordCount":59,"readingMinutes":60,"publishedAt":61},"2113931965261418497","design-pattern-15","设计模式修炼之路 ⑮ 策略模式——if-else 的解药","一个分支一个类、工厂加 Map 连选择处都不写 if-else、Spring 自动收集、线程池拒绝策略",1681,7953,20,"2024-06-16T18:10:44",{"id":63,"slug":64,"title":65,"summary":66,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":67,"likeCount":4,"wordCount":68,"readingMinutes":69,"publishedAt":70},"2113932208883372034","design-pattern-17","设计模式修炼之路 ⑰ 责任链模式——请求沿链条传递","客户端只认识链头、节点自治三段式、纯链与管线式、从 Servlet Filter 到 Netty Pipeline",3708,5950,15,"2024-07-23T20:24:49",{"code":4,"message":5,"data":72},{"prev":73,"next":74},{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":36,"publishedAt":52},{"id":54,"slug":55,"title":56,"summary":57,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":58,"likeCount":4,"wordCount":59,"readingMinutes":60,"publishedAt":61},{"code":4,"message":5,"data":76},[77,83,88,94],{"id":78,"name":79,"slug":80,"description":81,"sortOrder":4,"articleCount":82},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":84,"slug":85,"description":86,"sortOrder":4,"articleCount":87},"设计模式","design-pattern","设计模式系列","34",{"id":89,"name":90,"slug":91,"description":92,"sortOrder":33,"articleCount":93},"1946000000000000002","随笔","essay","生活与思考","0",{"id":95,"name":96,"slug":97,"description":98,"sortOrder":99,"articleCount":100},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":102,"toc":103},"\u003Cp>上一篇代理模式收官结构型时留足了排面：\"结构型管'类怎么组装'，行为型管'类怎么协作'——模板模式打头阵，干的事你天天在干：父类把流程骨架焊死，子类只往空位里填肉。\"今天它来了：\u003Cstrong>模板模式（Template Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、行为型十一员的第一位（本系列含编外的空对象共十二篇）。系列正式进入下半场——创建型管\"对象从哪来\"，结构型管\"类怎么组装\"，行为型管\"类与类怎么配合干活\"。前十三篇的模式你也许还能说\"平时真没主动写过\"，从这一篇开始不行了：HttpServlet、AbstractList、JUnit、JdbcTemplate、AQS，这些你天天摸的东西，骨子里全是它。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、什么是模板模式？\u003C\u002Fh2>\n\u003Cp>先看定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>定义一个操作中算法的骨架，而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下，重定义算法的某些步骤。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：\u003Cstrong>同一套流程在好几个类里重复出现，只有个别步骤不一样——把流程原样搬到父类里焊成 final，不一样的步骤留成空，子类只管填空\u003C\u002Fstrong>。步骤顺序、调用时机，全归父类管；每一步\"具体怎么干\"，归子类管。\u003C\u002Fp>\n\u003Cp>生活类比是考试卷：题型、题序、分值，在卷子印好的那一刻全定死，你只能在横线上填答案。选择题不会可以蒙个 C（钩子的默认实现），但你说\"我不想做选择题\"、或者想把大题挪到第一页——监考老师（final）第一个不答应。\u003C\u002Fp>\n\u003Cp>它只有两个角色，但父类肚子里装着\u003Cstrong>四种方法\u003C\u002Fstrong>——这是本篇最值钱的知识点：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>模板方法（template method）\u003C\u002Fstrong>：public final 修饰，定义流程骨架，按顺序调用下面三种方法。final 是灵魂：流程焊死，子类连重写的资格都没有；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>抽象方法\u003C\u002Fstrong>：必填的空。子类不填，编译器不放行；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>公共方法（concrete method）\u003C\u002Fstrong>：父类实现好的公共步骤，所有子类共享一份；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>钩子方法（hook）\u003C\u002Fstrong>：父类给一个\"人畜无害\"的默认实现（空方法，或返回默认值），子类想覆盖就覆盖，不覆盖也不碍事。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓一句话：\u003Cstrong>变的留给子类填空，不变的焊在父类共享；顺序父类说了算，细节子类说了算\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、模板模式的优缺点\u003C\u002Fh2>\n\u003Ch3 id=\"toc-3\">优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>消灭重复，一处修改全局生效\u003C\u002Fstrong>：公共步骤只在父类存在一份，\"日志要加时间戳\"这种需求从改 N 个类变成改 1 个类；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>流程受编译器保护\u003C\u002Fstrong>：\"不改变算法结构\"不是注释里的君子协定，是 final 关键字执法——先校验后发送这种铁律，子类想破也破不了；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>加变体就是加子类\u003C\u002Fstrong>：新渠道、新格式，extends 一个类填几个空就完事，开闭原则模板分；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>好莱坞原则（Hollywood Principle）\u003C\u002Fstrong>：Don't call us, we'll call you——父类调子类，子类不调父类；框架调你，你不调框架。控制权牢牢握在父类手里，这个思想后来有个更响亮的名字：控制反转，Spring IoC 的源头之一。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3 id=\"toc-4\">缺点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量膨胀\u003C\u002Fstrong>：一个变体一个子类，渠道一多，包里全是 XxxSender；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>继承是硬绑定\u003C\u002Fstrong>：父类一动，波及全部子类（脆弱基类问题）；而且每个子类都烧掉了 Java 单继承的唯一名额——想同时复用两套骨架，没门；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>阅读要上下横跳\u003C\u002Fstrong>：流程在父类、实现在子类，追一个调用两头跑，IDE 的调用层级树成了标配；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>骨架太死偶尔憋屈\u003C\u002Fstrong>：个别子类想在流程中间插一步？只能指望父类当初埋了钩子——埋钩子是框架作者在赌自己预见得全。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-5\">三、模板模式的应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>\u003Cstrong>多个类流程相同、个别步骤不同\u003C\u002Fstrong>：多渠道消息推送、多支付渠道对接、多格式文件导出、一切\"对接第 N 方\"的活——第四节就拿它当例子；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>框架定流程、用户填空\u003C\u002Fstrong>：框架设计的第一模式，没有之一。框架作者写父类焊骨架，框架用户写子类填空——第五节你会看到 Servlet、JUnit、JdbcTemplate、AQS、Spring Boot 启动全在这条路上；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>需要强制流程一致性的地方\u003C\u002Fstrong>：必须先校验再操作、必须先开事务再执行最后提交、必须 try-finally 关资源——焊进模板方法，新人想写错都难。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候别用\u003C\u002Fstrong>：流程只有一套、没有第二个变体——别为\"可能有变化\"上抽象父类，那是过度设计；变化的不是个别步骤而是整个算法——那是下一篇策略模式的地盘。\u003C\u002Fp>\n\u003Ch2 id=\"toc-6\">四、模板模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 template 包里：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>design-pattern\u002Fsrc\u002Fmain\u002Fjava\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>└── template                        \u002F\u002F 模板模式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── BadSmsSender.java           \u002F\u002F 反面教材：短信渠道复制粘贴\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── BadEmailSender.java         \u002F\u002F 反面教材：邮件渠道再复制一遍\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── AbstractMessageSender.java  \u002F\u002F 抽象类：模板方法 + 三个必填空 + 一个钩子\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── SmsSender.java              \u002F\u002F 填空 1：短信\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── EmailSender.java            \u002F\u002F 填空 2：邮件\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── DingTalkSender.java         \u002F\u002F 填空 3：钉钉，覆盖钩子开回执\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    └── TemplateTest.java           \u002F\u002F 测试入口\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：消息推送。短信、邮件、钉钉三个渠道，流程一模一样——校验目标、渲染内容、真正发送、记录日志，一步不多一步不少；只有每一步\"具体怎么干\"各不相同。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">1. 反面教材：每个渠道复制一遍流程\u003C\u002Fh3>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 反面教材：短信渠道，四步流程全写在业务里\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadSmsSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> send\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">userName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">target.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">matches\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">d{10}\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[坏例子-短信] 手机号不合法：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String content \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"【商城】尊敬的\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> userName \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"，您的验证码 666888，5 分钟内有效\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[坏例子-短信] 调用短信网关，发往 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[坏例子-短信] 记录发送日志：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" -> \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 反面教材：邮件渠道——结构一模一样，再复制一遍\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadEmailSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> send\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">userName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">target.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">contains\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"@\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[坏例子-邮件] 邮箱不合法：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String content \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x3C;html>&#x3C;body>尊敬的 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> userName \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"，您本月的账单已出，请查收&#x3C;\u002Fbody>&#x3C;\u002Fhtml>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[坏例子-邮件] 通过 SMTP 发往 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[坏例子-邮件] 记录发送日志：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" -> \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>===== 反面教材：每个渠道把流程复制一遍 =====\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[坏例子-短信] 调用短信网关，发往 13800138000\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[坏例子-短信] 记录发送日志：13800138000 -> 【商城】尊敬的张三，您的验证码 666888，5 分钟内有效\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[坏例子-邮件] 通过 SMTP 发往 zhangsan@example.com\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[坏例子-邮件] 记录发送日志：zhangsan@example.com -> &#x3C;html>&#x3C;body>尊敬的 张三，您本月的账单已出，请查收&#x3C;\u002Fbody>&#x3C;\u002Fhtml>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>两个类，四步流程，肉眼可见地重复。三宗罪：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>公共代码复制了 N 份\u003C\u002Fstrong>：\"记日志\"这步逐字相同。哪天产品说\"日志要带时间戳\"，两个类都得改；渠道加到五个，改五处，漏一处就是线上事故；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>流程顺序无保障\u003C\u002Fstrong>：新同事接手写钉钉渠道，\"先发送、后校验\"照样编译通过——没有任何东西拦得住他，直到垃圾短信真的发了出去；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>新渠道只能靠复制\u003C\u002Fstrong>：Ctrl+C、Ctrl+V、再逐行改——复制十行改三行，剩下七行就是将来所有 bug 的藏身处。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>病根一脉相承：该共享的没共享，该隔离的没隔离——只是这次重复的是流程，变化的是步骤。\u003C\u002Fp>\n\u003Ch3 id=\"toc-8\">2. 正解：流程上提父类，焊成 final\u003C\u002Fh3>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 抽象类：四种方法各就各位\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> abstract\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AbstractMessageSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 模板方法：final 焊死流程骨架——校验、渲染、发送、记日志的顺序，谁也改不了\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> send\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">userName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">checkTarget\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(target)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [模板] 目标地址不合法，中止发送：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [模板] 目标校验通过：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String content \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(userName);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [模板] 渲染内容：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        doSend\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(target, content);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [模板] 记录发送日志：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">needReceipt\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [模板] 开启回执确认，等待对方已读\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 抽象方法：必填的空——每种渠道的校验规则不同\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> abstract\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> checkTarget\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 抽象方法：必填的空——每种渠道的内容模板不同\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> abstract\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">userName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 抽象方法：必填的空——每种渠道的发送方式不同\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> abstract\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doSend\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">content\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 钩子方法：父类给默认实现（不开回执），子类看需要覆盖\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> needReceipt\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>对着第一节那张\"四种方法\"清单逐个对号：\u003Ccode>send()\u003C\u002Fcode> 是模板方法，final 修饰，四步顺序焊死；\u003Ccode>checkTarget()\u003C\u002Fcode> \u002F \u003Ccode>render()\u003C\u002Fcode> \u002F \u003Ccode>doSend()\u003C\u002Fcode> 是三个必填的抽象方法；记日志直接写死在骨架里，是公共方法；\u003Ccode>needReceipt()\u003C\u002Fcode> 是钩子。\u003C\u002Fp>\n\u003Cp>子类从此只干填空这一件事：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 填空 1：短信渠道\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SmsSender\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AbstractMessageSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> checkTarget\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">matches\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">d{10}\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">userName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"【商城】尊敬的\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> userName \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"，您的验证码 666888，5 分钟内有效\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doSend\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">content\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [短信] 调用短信网关，发往 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 填空 3：钉钉渠道——顺手覆盖钩子\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DingTalkSender\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AbstractMessageSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> checkTarget\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">startsWith\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ding:\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">userName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"【重要告警】\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> userName \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"，线上服务异常，请立即处理\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> doSend\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">content\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [钉钉] 调用机器人 webhook 推送：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 覆盖钩子：告警消息必须确认已读\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> needReceipt\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>EmailSender 结构完全相同，不再赘列。测试：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"===== 正解：模板模式，骨架焊死、子类填空 =====\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"--- 短信渠道 ---\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">AbstractMessageSender sms \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SmsSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">sms.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">send\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"13800138000\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"张三\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">sms.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">send\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"12345\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"张三\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"--- 邮件渠道 ---\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">AbstractMessageSender email \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> EmailSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">email.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">send\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"zhangsan@example.com\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"张三\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"--- 钉钉渠道（覆盖钩子，开回执） ---\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">AbstractMessageSender dingTalk \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DingTalkSender\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">dingTalk.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">send\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ding:9527\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"张三\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>===== 正解：模板模式，骨架焊死、子类填空 =====\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>--- 短信渠道 ---\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 目标校验通过：13800138000\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 渲染内容：【商城】尊敬的张三，您的验证码 666888，5 分钟内有效\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [短信] 调用短信网关，发往 13800138000\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 记录发送日志：13800138000\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 目标地址不合法，中止发送：12345\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>--- 邮件渠道 ---\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 目标校验通过：zhangsan@example.com\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 渲染内容：&#x3C;html>&#x3C;body>尊敬的 张三，您本月的账单已出，请查收&#x3C;\u002Fbody>&#x3C;\u002Fhtml>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [邮件] 通过 SMTP 发往 zhangsan@example.com\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 记录发送日志：zhangsan@example.com\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>--- 钉钉渠道（覆盖钩子，开回执） ---\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 目标校验通过：ding:9527\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 渲染内容：【重要告警】张三，线上服务异常，请立即处理\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [钉钉] 调用机器人 webhook 推送：ding:9527\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 记录发送日志：ding:9527\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [模板] 开启回执确认，等待对方已读\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处细节值得咂摸：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>\"12345\" 那单被拦在校验\u003C\u002Fstrong>：校验顺序是父类定的，SmsSender 只提供了\"什么算合法手机号\"，没有机会说\"先发了再说\"——final 的功劳；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>三个渠道的日志格式天然一致\u003C\u002Fstrong>：因为记日志的代码全程序只有一份，在父类骨架里；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>钉钉比别家多一行\"开启回执确认\"\u003C\u002Fstrong>：它覆盖了钩子 needReceipt() 返回 true，而短信、邮件对\"回执\"这件事毫不知情、毫发无损——这就是钩子的分寸感：默认不动声色，需要才出手。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3 id=\"toc-9\">3. 钩子方法：必答题和选答题\u003C\u002Fh3>\n\u003Cp>钩子常见两种形态，上面见了一种，补上另一种：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>boolean 开关\u003C\u002Fstrong>：父类骨架里写 \u003Ccode>if (needReceipt()) {...}\u003C\u002Fcode>，子类覆盖返回值，决定\"这一步跑不跑\"；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>空方法\u003C\u002Fstrong>：父类给 \u003Ccode>protected void afterSend() {}\u003C\u002Fcode> 空实现，骨架末尾调用它，子类想要\"发送后再干点什么\"就覆盖。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>钩子和抽象方法的本质区别一句话：\u003Cstrong>抽象方法是必答题，钩子是选答题\u003C\u002Fstrong>。子类十有八九用不上某个钩子，但父类必须提前埋好——埋少了子类憋屈，埋多了父类臃肿，这个分寸是框架作者的功力所在。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">五、在框架里找它\u003C\u002Fh2>\n\u003Cp>按\"填空姿势\"从传统到现代排开，全是你天天摸的东西。\u003C\u002Fp>\n\u003Ch3 id=\"toc-11\">1. HttpServlet：service() 焊死分发骨架\u003C\u002Fh3>\n\u003Cp>你写 Servlet 十年，从没写过 service()——因为它在 HttpServlet 里焊死了：解析请求 → 取出请求方法 → 是 GET 调 doGet、是 POST 调 doPost。你写的 doGet\u002FdoPost 就是填的空。更有意思的是，doGet 在 HttpServlet 里自带默认实现：返回 405 Method Not Allowed——你不覆盖它，容器就替你回一个 405。\u003Cstrong>默认实现返回错误码\u003C\u002Fstrong>，这是钩子最硬核的用法：\"你要么好好填，要么我明确告诉你这题没答\"。\u003C\u002Fp>\n\u003Ch3 id=\"toc-12\">2. AbstractList：JDK 的骨架实现\u003C\u002Fh3>\n\u003Cp>想实现一个 List，别直接 implements List——那意味着几十个方法全得自己写。继承 AbstractList，只填 \u003Ccode>get(int)\u003C\u002Fcode> 和 \u003Ccode>size()\u003C\u002Fcode> 两个空，iterator()、indexOf()、addAll() 父类全用你的 get() 拼好了。ArrayList 是它的子类，LinkedList 的父类 AbstractSequentialList 也是它的子类。同门还有 AbstractMap、AbstractSet、AbstractCollection。《Effective Java》第 20 条专门讲这招，术语叫\u003Cstrong>骨架实现（skeletal implementation）\u003C\u002Fstrong>：接口定契约，抽象类给骨架，你只填核心那几个空。\u003C\u002Fp>\n\u003Ch3 id=\"toc-13\">3. JUnit：生命周期也是填空\u003C\u002Fh3>\n\u003Cp>JUnit 4 的 BlockJUnit4ClassRunner 里焊死了一条流水线：跑 @Before 们 → 跑 @Test → 跑 @After 们，超时控制、异常捕获、@Ignore 处理全在骨架里。你写的 @Test 是必填的空，@Before\u002F@After 是选填的空——把\"每个用例前后要做的事\"做成填空位，这就是测试框架十几年不变的地基。\u003C\u002Fp>\n\u003Ch3 id=\"toc-14\">4. JdbcTemplate：填空从覆盖方法进化成传 Lambda\u003C\u002Fh3>\n\u003Cp>获取连接 → 创建语句 → 执行 SQL → 遍历 ResultSet → 关资源，五步全是容易写错的事（忘了关、忘了 try-finally），JdbcTemplate 全部焊死，你只填一个 RowMapper：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">User u \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> jdbcTemplate.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">queryForObject\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"select * from user where id = ?\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        (rs, rowNum) \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> User\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(rs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getLong\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">), rs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">)), \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 填空：一行怎么变对象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        userId);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意变化：\u003Cstrong>填空的载体从\"继承父类覆盖方法\"变成了\"传一个 Lambda 进来\"\u003C\u002Fstrong>。模板思想一点没变——骨架焊死、细节外置——只是实现从继承换成了组合。JDK 8 之后的新框架越来越多走这条路：回调即填空，这是模板模式在 Lambda 时代的进化形态。\u003C\u002Fp>\n\u003Ch3 id=\"toc-15\">5. AQS：并发框架的总骨架\u003C\u002Fh3>\n\u003Cp>面试必问的 AQS，全名 AbstractQueuedSynchronizer，名字自带 Abstract。acquire() 焊死了\"tryAcquire 抢锁 → 抢不到入队 → 阻塞排队等唤醒\"整套骨架，ReentrantLock、Semaphore、CountDownLatch 各填各的 tryAcquire\u002FtryRelease。所谓\"读懂 AQS 源码\"，一半就是在读一个巨型模板方法。\u003C\u002Fp>\n\u003Ch3 id=\"toc-16\">6. Spring 的 refresh()：一次方法调用启动整个容器\u003C\u002Fh3>\n\u003Cp>AbstractApplicationContext.refresh() 把 Spring 启动焊成十几步：prepareBeanFactory → invokeBeanFactoryPostProcessors → registerBeanPostProcessors → onRefresh → finishBeanFactoryInitialization……其中 onRefresh() 是留给子类的空方法，默认实现一行\"无事可做\"。SpringBoot 的 ServletWebServerApplicationContext 重写了它，在那一格空里创建内嵌 Tomcat。所以你每次启动 SpringBoot 应用，本质就是调用了一次模板方法——\u003Cstrong>内嵌 Web 容器，只是它填的一个空\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2 id=\"toc-17\">六、模板 vs 策略：把\"变化\"抽出去的两种手法\u003C\u002Fh2>\n\u003Cp>老规矩，把容易混淆的邻居拉出来遛遛。模板和策略干的是同一件事——\u003Cstrong>把变化的部分抽出去\u003C\u002Fstrong>——但抽法和粒度完全不同：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>模板模式\u003C\u002Fth>\n   \u003Cth>策略模式\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>手段\u003C\u002Ftd>\n   \u003Ctd>继承\u003C\u002Ftd>\n   \u003Ctd>组合\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>变化的粒度\u003C\u002Ftd>\n   \u003Ctd>流程里的个别步骤\u003C\u002Ftd>\n   \u003Ctd>整个算法整体替换\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>有没有固定流程\u003C\u002Ftd>\n   \u003Ctd>有，骨架焊死\u003C\u002Ftd>\n   \u003Ctd>没有，每个策略独立完整\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>切换时机\u003C\u002Ftd>\n   \u003Ctd>编译期定死（new 哪个子类就是哪个）\u003C\u002Ftd>\n   \u003Ctd>运行时随便换（set 进去就行）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>耦合\u003C\u002Ftd>\n   \u003Ctd>is-a，强绑定\u003C\u002Ftd>\n   \u003Ctd>has-a，松耦合\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>判别一句话：\u003Cstrong>流程不变、只换某几步，用模板；整个打法互换、没有共同骨架，用策略\u003C\u002Fstrong>。本篇的推送是模板——\"校验→渲染→发送→记日志\"为三个渠道共享；排序是策略——快排换冒泡，换的是整个算法，中间没有可共享的流程。\u003C\u002Fp>\n\u003Cp>模板还有个先天短板第二节埋过伏笔：靠继承，每个子类烧掉 Java 单继承的唯一名额。JdbcTemplate 已经示范了出路——模板思想完全可以用组合承载，填空从覆盖方法变成传回调。\u003Cstrong>继承版模板是 1994 年的答案，回调版模板是这个时代的答案\u003C\u002Fstrong>——但无论哪版，\"骨架焊死、细节外置\"八个字从没变过。\u003C\u002Fp>\n\u003Ch2 id=\"toc-18\">写在最后\u003C\u002Fh2>\n\u003Cp>回顾这一篇：模板模式把重复出现的流程上提到父类，焊成 final 模板方法；子类只填抽象方法那些必填的空、按需覆盖钩子那些选填的空——模板方法、抽象方法、公共方法、钩子，四种方法各司其职；final 让\"不许改流程\"从口头约定变成编译器执法；好莱坞原则\"父类调子类\"是控制反转的雏形；从 HttpServlet、AbstractList 到 JUnit、JdbcTemplate、AQS、Spring refresh，框架世界的半壁江山都是\"框架焊骨架、用户填空\"这一个套路；它和策略的分界在粒度——换步骤是模板，换打法是策略。行为型的第一位，也是你每天用得最多的一位。\u003C\u002Fp>\n\u003Cp>下一篇 ⑮ 策略模式——模板填的是流程里的某几步，策略换的是整个打法。它还有个响亮的外号：\u003Cstrong>if-else 的解药\u003C\u002Fstrong>。你代码里那个嵌套五层、每来一个新需求就得多写一个 else if 的方法，读完下一篇就有解了。\u003C\u002Fp>",[104,107,110,114,117,120,123,126,129,132,135,138,141,144,147,150,153,156],{"id":105,"text":106,"level":99},"toc-1","一、什么是模板模式？",{"id":108,"text":109,"level":99},"toc-2","二、模板模式的优缺点",{"id":111,"text":112,"level":113},"toc-3","优点",3,{"id":115,"text":116,"level":113},"toc-4","缺点",{"id":118,"text":119,"level":99},"toc-5","三、模板模式的应用场景",{"id":121,"text":122,"level":99},"toc-6","四、模板模式的实现方式",{"id":124,"text":125,"level":113},"toc-7","1. 反面教材：每个渠道复制一遍流程",{"id":127,"text":128,"level":113},"toc-8","2. 正解：流程上提父类，焊成 final",{"id":130,"text":131,"level":113},"toc-9","3. 钩子方法：必答题和选答题",{"id":133,"text":134,"level":99},"toc-10","五、在框架里找它",{"id":136,"text":137,"level":113},"toc-11","1. HttpServlet：service() 焊死分发骨架",{"id":139,"text":140,"level":113},"toc-12","2. AbstractList：JDK 的骨架实现",{"id":142,"text":143,"level":113},"toc-13","3. JUnit：生命周期也是填空",{"id":145,"text":146,"level":113},"toc-14","4. JdbcTemplate：填空从覆盖方法进化成传 Lambda",{"id":148,"text":149,"level":113},"toc-15","5. AQS：并发框架的总骨架",{"id":151,"text":152,"level":113},"toc-16","6. Spring 的 refresh()：一次方法调用启动整个容器",{"id":154,"text":155,"level":99},"toc-17","六、模板 vs 策略：把\"变化\"抽出去的两种手法",{"id":157,"text":158,"level":99},"toc-18","写在最后"]