一:为什么需要模板方法模式
1、先举个简单的场景设定:做两种早餐(煎蛋三明治 + 火腿三明治)
先明确两个核心前提:
-
两种三明治的制作流程骨架完全一致:准备食材 → 烤面包 → 加核心配料 → 组装 → 装盘
-
只有核心配料不同:
- 煎蛋三明治:加煎蛋
- 火腿三明治:加火腿
即大部分步骤都是重复的,只有加核心配料环节不同而已
2、没有模板模式的实现(纯 “复制粘贴” 式开发)
我们模拟一个新手开发者,为两种三明治分别写 “制作逻辑”,完全不抽离通用流程:
// 煎蛋三明治制作类
public class EggSandwich {
// 制作煎蛋三明治的完整方法
public void makeEggSandwich() {
// 通用步骤1:准备食材(面包、鸡蛋、沙拉酱)
System.out.println("1. 准备食材:面包片、鸡蛋、沙拉酱");
// 通用步骤2:烤面包(两种三明治都要烤)
System.out.println("2. 烤面包片(烤到两面微焦)");
// 煎蛋三明治特有步骤:煎鸡蛋
System.out.println("3. 煎鸡蛋(单面煎,保留溏心)");
// 通用步骤4:组装(抹沙拉酱+夹配料)
System.out.println("4. 抹沙拉酱,把煎蛋夹进两片面包中间");
// 通用步骤5:装盘
System.out.println("5. 装盘,放一片生菜装饰");
System.out.println("=== 煎蛋三明治做好啦 ===");
}
}
// 火腿三明治制作类
public class HamSandwich {
// 制作火腿三明治的完整方法
public void makeHamSandwich() {
// 通用步骤1:准备食材(和煎蛋三明治大部分重复,只改了配料)
System.out.println("1. 准备食材:面包片、火腿、沙拉酱");
// 通用步骤2:烤面包(完全复制煎蛋三明治的逻辑)
System.out.println("2. 烤面包片(烤到两面微焦)");
// 火腿三明治特有步骤:煎火腿
System.out.println("3. 煎火腿(煎到边缘微焦)");
// 通用步骤4:组装(几乎复制,只改了配料)
System.out.println("4. 抹沙拉酱,把火腿夹进两片面包中间");
// 通用步骤5:装盘(完全复制)
System.out.println("5. 装盘,放一片生菜装饰");
System.out.println("=== 火腿三明治做好啦 ===");
}
}
// 测试制作两种三明治
public class TestBreakfast {
public static void main(String[] args) {
// 做煎蛋三明治
EggSandwich eggSandwich = new EggSandwich();
eggSandwich.makeEggSandwich();
System.out.println("-------------------");
// 做火腿三明治
HamSandwich hamSandwich = new HamSandwich();
hamSandwich.makeHamSandwich();
}
}
3、运行结果(先看效果,再找问题)
1. 准备食材:面包片、鸡蛋、沙拉酱
2. 烤面包片(烤到两面微焦)
3. 煎鸡蛋(单面煎,保留溏心)
4. 抹沙拉酱,把煎蛋夹进两片面包中间
5. 装盘,放一片生菜装饰
=== 煎蛋三明治做好啦 ===
-------------------
1. 准备食材:面包片、火腿、沙拉酱
2. 烤面包片(烤到两面微焦)
3. 煎火腿(煎到边缘微焦)
4. 抹沙拉酱,把火腿夹进两片面包中间
5. 装盘,放一片生菜装饰
=== 火腿三明治做好啦 ===
可以看出虽然制作三明治的大部分步骤都是重复的,仅仅加配料环节不同(一个加火腿,一个加鸡蛋)。但是当我们扩大视角,打算研发各种不同类型的三明治,比如三文鱼三明治,海苔三明治等等,我们也仅仅是核心配料不同而已(三文鱼与海苔),其他步骤重复执行很多遍就显的过于冗余,就会引入以下的几个痛点
4、没有模板模式的核心痛点
从这个例子能直观看到 4 个致命问题,就像你每天做早餐都重复走流程,只换核心配料,却每次都要从头记一遍步骤:
(1). 代码 “复制粘贴” 泛滥(最直观)
- 烤面包、装盘、组装的核心逻辑完全一样,却在两个类中重复写了一遍;
- 如果后续加 “芝士三明治”“培根三明治”,这些通用步骤还要再复制 N 遍。
这一点可以联想到我们的方法定义,本质还是将许多重复的逻辑抽取成一个方法统一管理
(2). 维护成本极高(改一处要改所有)
比如老板要求:“烤面包要烤到全焦,不是微焦”—— 你需要:
- 改
EggSandwich的第 2 步; - 改
HamSandwich的第 2 步; - 后续新增的所有三明治类,都要改这一步;→ 只要通用逻辑变了,但是所有类都要改,极易漏改、出错。
(3). 流程容易乱(新手易写错步骤)
如果新来的开发者做 “芝士三明治”,可能把步骤写成:准备食材 → 加芝士 → 烤面包 → 组装 → 装盘→ 烤面包的步骤被插队,三明治烤糊了(业务逻辑出错)。
(4). 逻辑分散(通用规则无法统一)
比如老板要求 “所有三明治装盘都要放番茄片,不是生菜”—— 你需要逐个修改每个三明治类的第 5 步,无法一次性统一生效。
其实第(2)点与第(4)点是类似的,都是通用逻辑变了,所有三明治类都要跟着修改
5、痛点总结(为什么模板模式是刚需)
这个早餐例子的核心问题,就是 “通用流程重复、细节和骨架耦合、流程无法统一”—— 而这正是模板模式要解决的核心问题。
重点还是代码耦合在一起了,每个不同的三明治类内部都要重复定义通用逻辑,通用逻辑一变,所有三明治类都要跟着改变,这一点在代码维护中是很恶心的。几个三明治类还好,改动不多,但是一旦有几百,几千个类,那改起来就很可怕了。所以就需要将通用逻辑与特定逻辑拆分开来,通用逻辑放在模板中,特定逻辑由模板的具体实现类来自定义,类似于java中的父类和子类的,父类定义公共方法与抽象方法,子类继承父类的公共方法,并重写抽象方法来实现子类的特殊逻辑
你可以把这个场景记在心里:
- 没有模板模式 = 每次做三明治都要从头背一遍完整步骤,哪怕大部分步骤都一样;
- 有模板模式 = 把通用步骤写在 “食谱模板” 上,只留 “加什么配料” 的空白让你填,既不用背流程,又能灵活换配料。
模板模式的模板两字就可以理解为这里的食谱模板,所有通用步骤都记在模板中,由模板统一管理,通用步骤改变了,这需要跟着修改模板即可,只有三明治的核心区分---“核心配料”由用户记住即可,其他操作用户可以直接照着模板去做就行了
二:引入模板模式的优点
1、模板模式的核心思路(对应早餐场景)
先把核心思路转化为早餐场景的语言,让你一眼懂:
- 抽离 “通用流程骨架” :把烤面包、装盘、组装这些所有三明治都要走的步骤,写到一个「三明治制作模板」里;
- 固定流程顺序:模板里定死步骤顺序(准备食材→烤面包→加核心配料→组装→装盘),子类不能改;
- 留出 “细节口子” :只把 “加什么核心配料”(煎蛋 / 火腿)、“准备什么食材” 这些可变细节,让子类自己实现。
有没有发现模板模式其实据类似于我们上面说的java的父类与子类关系
2、模板模式的实现(分 3 步,基于早餐例子)
步骤 1:定义「三明治制作模板」(抽象父类)
这是模板模式的核心 —— 抽象父类定死流程骨架,实现通用逻辑,把可变细节定义为抽象方法(留给子类填)。
// 抽象父类:三明治制作模板(定义通用流程,留细节口子)
public abstract class AbstractSandwich {
// 模板方法:定死制作流程的顺序(用final防止子类改流程)
public final void makeSandwich() {
prepareIngredients(); // 步骤1:准备食材(可变,子类实现)
toastBread(); // 步骤2:烤面包(通用,父类实现)
addCoreIngredient(); // 步骤3:加核心配料(可变,子类实现)
assemble(); // 步骤4:组装(通用,父类实现)
plate(); // 步骤5:装盘(通用,父类实现)
System.out.println("=== " + getSandwichName() + "做好啦 ===");
}
// 通用逻辑:所有三明治都要做的步骤,父类统一实现
private void toastBread() {
// 通用规则:烤面包改为“全焦”(后续改这里,所有子类都生效)
System.out.println("2. 烤面包片(烤到两面全焦)");
}
private void assemble() {
System.out.println("4. 抹沙拉酱,把核心配料夹进两片面包中间");
}
private void plate() {
// 通用规则:装盘统一放番茄片(改这里,所有子类都生效)
System.out.println("5. 装盘,放一片番茄装饰");
}
// 可变细节:抽象方法,子类必须实现(留的“空白口子”)
protected abstract void prepareIngredients(); // 准备食材(不同三明治食材不同)
protected abstract void addCoreIngredient(); // 加核心配料(煎蛋/火腿)
protected abstract String getSandwichName(); // 获取三明治名称(用于打印)
}
步骤 2:实现「煎蛋三明治」子类(仅填细节)
子类不用关心流程顺序,只需要实现父类的抽象方法,填好 “自己的细节” 即可。
// 煎蛋三明治:仅实现可变细节,通用流程复用父类
public class EggSandwich extends AbstractSandwich {
@Override
protected void prepareIngredients() {
System.out.println("1. 准备食材:面包片、鸡蛋、沙拉酱");
}
@Override
protected void addCoreIngredient() {
System.out.println("3. 煎鸡蛋(单面煎,保留溏心)");
}
@Override
protected String getSandwichName() {
return "煎蛋三明治";
}
}
步骤 3:实现「火腿三明治」子类(仅填细节)
和煎蛋三明治一样,只关注自己的特有细节,通用流程完全复用父类。
// 火腿三明治:仅实现可变细节
public class HamSandwich extends AbstractSandwich {
@Override
protected void prepareIngredients() {
System.out.println("1. 准备食材:面包片、火腿、沙拉酱");
}
@Override
protected void addCoreIngredient() {
System.out.println("3. 煎火腿(煎到边缘微焦)");
}
@Override
protected String getSandwichName() {
return "火腿三明治";
}
}
3、测试模板模式的效果
// 测试类:制作两种三明治
public class TestBreakfast {
public static void main(String[] args) {
// 做煎蛋三明治
AbstractSandwich eggSandwich = new EggSandwich();
eggSandwich.makeSandwich();
System.out.println("-------------------");
// 做火腿三明治
AbstractSandwich hamSandwich = new HamSandwich();
hamSandwich.makeSandwich();
}
}
运行结果
1. 准备食材:面包片、鸡蛋、沙拉酱
2. 烤面包片(烤到两面全焦)
3. 煎鸡蛋(单面煎,保留溏心)
4. 抹沙拉酱,把核心配料夹进两片面包中间
5. 装盘,放一片番茄装饰
=== 煎蛋三明治做好啦 ===
-------------------
1. 准备食材:面包片、火腿、沙拉酱
2. 烤面包片(烤到两面全焦)
3. 煎火腿(煎到边缘微焦)
4. 抹沙拉酱,把核心配料夹进两片面包中间
5. 装盘,放一片番茄装饰
=== 火腿三明治做好啦 ===
3、模板模式如何解决之前的痛点?(对比解读)
| 无模板模式的痛点 | 模板模式的解决方案 |
|---|---|
| 代码复制粘贴泛滥 | 烤面包、组装、装盘等通用逻辑只写一次(父类),子类无需重复写 |
| 改通用逻辑要改所有类 | 比如要改 “烤面包” 的规则,只需改父类的 toastBread() 方法,所有子类自动生效 |
| 流程容易乱(新手写错步骤) | 模板方法 makeSandwich() 用 final 修饰,子类不能改步骤顺序,流程永远统一 |
| 通用规则无法统一 | 装盘、组装等通用规则在父类统一定义,所有三明治都遵守,不会出现 “生菜 / 番茄” 不统一的问题 |
4、扩展:新增 “芝士三明治”(看模板模式的扩展性)
如果要加一种芝士三明治,只需要新增一个子类,实现抽象方法即可,完全不用改父类(符合 “开闭原则”):
// 新增芝士三明治:仅实现细节,复用所有通用流程
public class CheeseSandwich extends AbstractSandwich {
@Override
protected void prepareIngredients() {
System.out.println("1. 准备食材:面包片、芝士片、沙拉酱");
}
@Override
protected void addCoreIngredient() {
System.out.println("3. 烤芝士片(烤到融化)");
}
@Override
protected String getSandwichName() {
return "芝士三明治";
}
}
// 测试新增的芝士三明治
public class TestNewSandwich {
public static void main(String[] args) {
AbstractSandwich cheeseSandwich = new CheeseSandwich();
cheeseSandwich.makeSandwich();
}
}
总结
模板模式核心要点(基于早餐例子)
- 核心结构:抽象父类(模板)定义流程骨架(
makeSandwich())+ 实现通用逻辑(烤面包、装盘),子类仅实现可变细节(加什么配料); - 关键设计:模板方法用
final修饰,防止子类篡改流程顺序; - 核心价值:复用通用代码、统一流程、降低维护成本、提升扩展性;
- 生活类比:模板模式 = 餐厅的 “标准化食谱”,厨师只需按食谱填 “核心配料”,不用背完整流程,也不会做错步骤。
到这里,你已经通过最贴近生活的 “做三明治” 例子,理解了模板模式的核心逻辑和价值。下面带你了解模板模式的定义
三、模板模式的官方定义
模板模式(Template Method Pattern)是行为型设计模式的核心之一,GOF(四人组)对它的经典定义是:
定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中。模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。
用 “做三明治” 翻译定义
- 算法骨架:做三明治的固定流程(准备食材→烤面包→加配料→组装→装盘);
- 延迟到子类:“加什么配料”“准备什么食材” 这些步骤交给子类(煎蛋 / 火腿三明治)实现;
- 不改变算法结构:子类只能改 “加配料” 的细节,不能改 “先烤面包再加配料” 的流程顺序。
1、模板模式的核心规则(必须遵守)
模板模式的设计有严格的 “规则约束”,这些规则是为了保证 “流程统一、细节灵活”,结合三明治例子逐一解读:
规则 1:核心是 “分离不变与可变”
- 不变部分:所有实现类都共用的通用逻辑(比如烤面包、组装、装盘),必须放在抽象父类中实现;
- 可变部分:不同实现类的特有细节(比如加煎蛋 / 火腿),必须定义为抽象方法(或钩子方法),由子类实现。→ 违反后果:如果把可变细节写在父类,会导致父类臃肿;如果把不变逻辑写在子类,会重复造轮子。
规则 2:模板方法必须用 final 修饰
- 模板方法(定义流程骨架的方法,比如
makeSandwich())是算法的 “核心骨架”,必须用final防止子类重写; - 目的:保证所有子类都遵守统一的流程顺序,避免出现 “先加配料再烤面包” 的逻辑错误。→ 反例:如果
makeSandwich()不加final,子类可能重写流程,导致三明治做糊。
注意:流程骨架是不变的,定义在父类中,由final字段修饰,就像做三明治的固定流程中,加配料就是固定在烤面包后面的,不同的三明治类实现的抽象方法也只是重写核心配料,比如鸡蛋与火腿肠,必须先烤完面包后才能加配料,顺序不能颠倒。不好理解的话,你就想象成子类重写抽象方法(新增核心配料---->火腿肠),然后将抽象方法填充到流程骨架中(即将添加火腿肠配料的动作填充到烤面包后),外界调用流程骨架函数(根据食谱定义的流程顺序:准备食材→烤面包→加火腿肠配料→组装→装盘的顺序开始制作火腿肠)
// 抽象父类:三明治制作模板(定义通用流程,留细节口子)
public abstract class AbstractSandwich {
// 模板方法:定死制作流程的顺序(用final防止子类改流程)
public final void makeSandwich() {
prepareIngredients(); // 步骤1:准备食材(可变,子类实现)
toastBread(); // 步骤2:烤面包(通用,父类实现)
addCoreIngredient(); // 步骤3:加核心配料(可变,子类实现)
assemble(); // 步骤4:组装(通用,父类实现)
plate(); // 步骤5:装盘(通用,父类实现)
System.out.println("=== " + getSandwichName() + "做好啦 ===");
}
// 通用逻辑:所有三明治都要做的步骤,父类统一实现
private void toastBread() {
// 通用规则:烤面包改为“全焦”(后续改这里,所有子类都生效)
System.out.println("2. 烤面包片(烤到两面全焦)");
}
private void assemble() {
System.out.println("4. 抹沙拉酱,把核心配料夹进两片面包中间");
}
private void plate() {
// 通用规则:装盘统一放番茄片(改这里,所有子类都生效)
System.out.println("5. 装盘,放一片番茄装饰");
}
// 可变细节:抽象方法,子类必须实现(留的“空白口子”)
protected abstract void prepareIngredients(); // 准备食材(不同三明治食材不同)
protected abstract void addCoreIngredient(); // 加核心配料(煎蛋/火腿)
protected abstract String getSandwichName(); // 获取三明治名称(用于打印)
}
复用的前面的代码,可以看一下他的流程骨架是用final定义的
规则 3:抽象方法用 protected 修饰
-
父类中定义的可变细节方法(比如
prepareIngredients()),访问修饰符必须是protected; -
原因:
- 保证子类能访问(实现这些方法);
- 防止外部类直接调用(这些方法是 “内部步骤”,外部只需调用模板方法
makeSandwich())。→ 反例:如果用public,外部可能直接调用addCoreIngredient(),导致流程混乱;如果用private,子类无法实现。
先了解一下java的权限以及范围
| 访问修饰符 | 同一类中 | 同一包中的子类 / 非子类 | 不同包中的子类 | 不同包中的非子类 |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
default | ✅ | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
规则 4:遵循 “开闭原则”
- 对扩展开放:新增子类(比如芝士三明治),只需实现抽象方法,无需修改父类;
- 对修改封闭:修改通用逻辑(比如烤面包的规则),只需改父类的通用方法,所有子类自动生效。→ 这是模板模式最核心的设计目标,也是它能降低维护成本的关键。
2、模板模式的核心结构(UML 类图 + 角色解读)
结合三明治例子,先看 UML 类图,再解读每个角色的作用:
模板模式的 3 个核心角色(对应例子)
| 角色名称 | 作用 | 三明治例子中的对应类 / 方法 |
|---|---|---|
| 抽象模板(Abstract Class) | 1. 定义模板方法(流程骨架);2. 实现通用方法;3. 定义抽象方法(细节口子) | AbstractSandwich 类 |
| 具体实现(Concrete Class) | 实现抽象模板中的抽象方法,完成特有细节 | EggSandwich、HamSandwich 类 |
| 模板方法(Template Method) | 定死算法流程,调用通用方法和抽象方法,用 final 修饰 | AbstractSandwich.makeSandwich() |
3、模板模式的 “钩子方法”(扩展知识点)
除了抽象方法,模板模式还有一种灵活的设计 ——钩子方法(Hook Method) ,它是父类中提供默认实现的方法,子类可重写也可不重写(可选细节)。
举例子:给三明治加 “是否加沙拉酱” 的钩子
// 抽象模板类新增钩子方法
public abstract class AbstractSandwich {
// 模板方法(不变)
public final void makeSandwich() {
prepareIngredients();
toastBread();
addCoreIngredient();
// 调用钩子方法:可选加沙拉酱
if (needSaladSauce()) {
addSaladSauce();
}
assemble();
plate();
}
// 通用方法:加沙拉酱
private void addSaladSauce() {
System.out.println("额外:抹沙拉酱");
}
// 🌟 钩子方法:默认加沙拉酱,子类可重写
protected boolean needSaladSauce() {
return true;
}
// 其他方法不变...
}
// 火腿三明治重写钩子:不加沙拉酱
public class HamSandwich extends AbstractSandwich {
// 重写钩子方法
@Override
protected boolean needSaladSauce() {
return false;
}
// 其他方法不变...
}
钩子方法的作用
- 提供 “可选细节”:子类可根据需要选择是否重写,比抽象方法更灵活;
- 控制流程分支:父类模板方法可根据钩子方法的返回值,决定是否执行某个步骤。
模板类里提前写好的 “默认逻辑”(比如默认开 / 关某个步骤),子类可以选择 “重写这个逻辑”(相当于扳动开关),也可以 “不重写”(用默认逻辑)。核心作用:让固定的流程能 “灵活调整”,但又不破坏整体流程结构。比如登录校验功能中,对管理可以设置钩子函数返回false,让他跳过输入密码验证身份的流程,对于普通人,那么钩子函数设为true,让他执行密码验证
总结
模板模式核心知识点回顾
-
定义:定死算法骨架,将可变步骤延迟到子类,不改变流程即可重定义细节;
-
核心规则:
- 模板方法用
final修饰,保证流程统一; - 不变逻辑放父类,可变逻辑放子类抽象方法;
- 抽象方法用
protected修饰,仅子类可访问;
- 模板方法用
-
核心结构:抽象模板(定骨架)+ 具体实现(填细节);
-
扩展:钩子方法提供可选细节,让模板模式更灵活。
四、模板模式的写法
一、模板模式的标准化写法(可直接套用)
先给出模板模式的通用代码骨架,所有场景都能基于这个骨架改造,结合之前的三明治例子做抽象提炼:
1. 第一步:定义抽象模板类(核心)
/**
* 模板模式抽象父类(通用模板)
* 核心:定流程、实现通用逻辑、定义可变细节(抽象方法/钩子方法)
*/
public abstract class AbstractTemplate {
/**
* 🌟 模板方法:定死算法流程(final防止子类篡改)
* 所有子类必须遵循这个流程顺序
*/
public final void templateMethod() {
// 步骤1:通用前置逻辑(父类实现)
preProcess();
// 步骤2:可变细节1(子类实现)
doBusiness1();
// 步骤3:通用核心逻辑(父类实现)
coreProcess();
// 步骤4:可变细节2(子类实现)
doBusiness2();
// 步骤5:钩子方法控制可选流程
if (needOptionalProcess()) {
optionalProcess();
}
// 步骤6:通用后置逻辑(父类实现)
postProcess();
}
// ---------------------- 通用逻辑:父类实现,子类复用 ----------------------
private void preProcess() {
System.out.println("通用前置:参数校验、资源初始化");
}
private void coreProcess() {
System.out.println("通用核心:固定业务逻辑(比如数据库连接、接口调用)");
}
private void postProcess() {
System.out.println("通用后置:资源释放、结果返回");
}
private void optionalProcess() {
System.out.println("通用可选:日志记录、监控上报");
}
// ---------------------- 可变细节:抽象方法,子类必须实现 ----------------------
protected abstract void doBusiness1(); // 业务细节1
protected abstract void doBusiness2(); // 业务细节2
// ---------------------- 钩子方法:可选重写,默认实现 ----------------------
protected boolean needOptionalProcess() {
return true; // 默认执行可选流程
}
}
2. 第二步:实现具体子类(仅填细节)
/**
* 具体实现类1:业务场景A
* 核心:只实现抽象方法,可选重写钩子方法
*/
public class ConcreteTemplateA extends AbstractTemplate {
@Override
protected void doBusiness1() {
System.out.println("业务A细节1:处理订单参数");
}
@Override
protected void doBusiness2() {
System.out.println("业务A细节2:生成订单编号");
}
// 可选:重写钩子方法,关闭可选流程
@Override
protected boolean needOptionalProcess() {
return false;
}
}
/**
* 具体实现类2:业务场景B
*/
public class ConcreteTemplateB extends AbstractTemplate {
@Override
protected void doBusiness1() {
System.out.println("业务B细节1:处理支付参数");
}
@Override
protected void doBusiness2() {
System.out.println("业务B细节2:调用支付接口");
}
// 不重写钩子方法,使用默认逻辑(执行可选流程)
}
3. 第三步:调用模板(外部只关心模板方法)
public class TemplateTest {
public static void main(String[] args) {
// 业务场景A
AbstractTemplate templateA = new ConcreteTemplateA();
templateA.templateMethod();
System.out.println("======= 分割线 =======");
// 业务场景B
AbstractTemplate templateB = new ConcreteTemplateB();
templateB.templateMethod();
}
}
输出结果(验证流程)
通用前置:参数校验、资源初始化
业务A细节1:处理订单参数
通用核心:固定业务逻辑(比如数据库连接、接口调用)
业务A细节2:生成订单编号
通用后置:资源释放、结果返回
======= 分割线 =======
通用前置:参数校验、资源初始化
业务B细节1:处理支付参数
通用核心:固定业务逻辑(比如数据库连接、接口调用)
业务B细节2:调用支付接口
通用可选:日志记录、监控上报
通用后置:资源释放、结果返回
二、模板模式的实际业务写法举例(也是博主找的例子)
数据导入模板(不同格式文件导入)
定义抽象模板类
1. 抽象模板类
public abstract class DataImportTemplate {
// 模板方法(final修饰)
public final void importData(String filePath) {
try {
// 1. 读取文件(钩子)
List<String> lines = readFile(filePath);
// 2. 解析数据(子类实现)
List<Map<String, Object>> dataList = parseData(lines);
// 3. 数据校验(钩子)
if (needValidate()) {
validateData(dataList);
}
// 4. 保存数据(子类实现)
saveData(dataList);
// 5. 记录日志
logSuccess(filePath, dataList.size());
} catch (Exception e) {
// 统一异常处理
logError(filePath, e);
throw new BusinessException("导入失败", e);
}
}
// 抽象方法(子类必须实现)
protected abstract List<Map<String, Object>> parseData(List<String> lines);
protected abstract void saveData(List<Map<String, Object>> dataList);
// 钩子方法(子类可选实现)
protected boolean needValidate() {
return false; // 默认不需要校验
}
protected void validateData(List<Map<String, Object>> dataList) {
// 默认空实现
}
// 公共方法
private List<String> readFile(String filePath) {
// 读取文件通用逻辑
return FileUtils.readLines(new File(filePath), "UTF-8");
}
private void logSuccess(String filePath, int count) {
System.out.println("导入成功:" + filePath + ",共" + count + "条");
}
private void logError(String filePath, Exception e) {
System.out.println("导入失败:" + filePath + "," + e.getMessage());
}
}
定义具体实现子类
// 2. Excel导入实现
@Component
public class ExcelDataImport extends DataImportTemplate {
@Override
protected List<Map<String, Object>> parseData(List<String> lines) {
// Excel解析逻辑
List<Map<String, Object>> result = new ArrayList<>();
for (String line : lines) {
String[] cells = line.split(",");
Map<String, Object> row = new HashMap<>();
row.put("name", cells[0]);
row.put("age", cells[1]);
result.add(row);
}
return result;
}
@Override
protected void saveData(List<Map<String, Object>> dataList) {
// 批量保存到数据库
dataMapper.batchInsert(dataList);
}
@Override
protected boolean needValidate() {
return true; // Excel需要校验
}
@Override
protected void validateData(List<Map<String, Object>> dataList) {
for (Map<String, Object> row : dataList) {
if (row.get("name") == null) {
throw new BusinessException("姓名不能为空");
}
}
}
}
// 3. JSON导入实现
@Component
public class JsonDataImport extends DataImportTemplate {
@Override
protected List<Map<String, Object>> parseData(List<String> lines) {
// JSON解析逻辑
String jsonStr = String.join("", lines);
return JSON.parseObject(jsonStr, new TypeReference<List<Map<String, Object>>>() {});
}
@Override
protected void saveData(List<Map<String, Object>> dataList) {
dataMapper.batchInsert(dataList);
}
// 使用默认的needValidate(返回false)
}
主函数调用
// 4. 使用
@Service
public class ImportService {
@Autowired
private Map<String, DataImportTemplate> importers; // Spring自动注入所有DataImportTemplate实现类
public void importFile(String type, String filePath) {
// 根据类型获取对应的导入器
DataImportTemplate importer = importers.get(type + "DataImport");
//执行模板流程
importer.importData(filePath);
}
}
这里就是让模板方法整合Spring的用法,让所有的数据导入功能类成为bean,然后统一由容器注入到一个Map集合中(因为这些类豆继承了DataImportTemplate类,我们声明Map的value类型为DataImportTemplate,容器会自动注入所有子类的bean),这样我们更方便执行每个类的模板流程,实现不同格式文件的导入功能
三、模板模式与策略模式的结合
1、先搞懂:模板 + 策略 组合的核心逻辑
| 模式 | 核心作用 | 比喻 |
|---|---|---|
| 模板模式 | 固定 “流程骨架”(比如做饭先洗菜→切菜→炒菜→装盘) | 定死 “做饭的步骤顺序”,不能乱 |
| 策略模式 | 替换 “核心算法”(比如炒菜可以选红烧 / 清蒸 / 爆炒) | 换 “炒菜的方式”,步骤不变 |
组合后的效果:流程顺序永远不变,但核心环节的算法可以灵活切换 —— 既保证了流程统一(不会有人先炒菜后洗菜),又能灵活适配不同场景(想吃红烧就红烧,想吃清蒸就清蒸)。
2、为什么常用这个组合?(痛点驱动)
- 单独用模板模式:流程固定,但核心步骤的算法写死在子类里,新增算法要加新子类,代码膨胀;
- 单独用策略模式:算法灵活切换,但流程要重复写(比如每个炒菜策略都要写 “洗菜→切菜→炒菜→装盘”),冗余;
- 组合用:模板定流程,策略管算法,两全其美 —— 新增算法只需加策略类,不用改流程,也不用加新模板子类。
总结:原本单独用模板模式,模板中的核心算法要由子类去实现,但是当子类一多,再加上每个子类的核心算法实现不同,很难统筹管理,但是策略模式的算法切换灵活的特点弥补了这一点,我们可以将子类的核心算法定义成一个一个策略去调用。而策略模式虽然切换算法方便,但是每个策略中的算法流程会重复,而模板模式正好解决了代码重复性的问题,使得他们两个完全互补
3、实用例子
电商订单计算
场景:订单结算流程固定(校验商品→计算金额→优惠抵扣→生成订单),但 “优惠抵扣” 的算法可变(满减、折扣、优惠券、拼团)。
- 模板模式:固定订单结算的 4 步流程;
- 策略模式:把 “优惠抵扣” 封装成策略,可动态切换。
步骤 1:定义优惠策略接口(策略模式核心)
// 优惠策略接口(不同抵扣方式)
public interface DiscountStrategy {
// 计算抵扣金额
BigDecimal calculateDiscount(BigDecimal orderAmount);
}
// 策略1:满减(满100减20)
public class FullReduceStrategy implements DiscountStrategy {
@Override
public BigDecimal calculateDiscount(BigDecimal orderAmount) {
return orderAmount.compareTo(BigDecimal.valueOf(100)) >= 0 ? BigDecimal.valueOf(20) : BigDecimal.ZERO;
}
}
// 策略2:折扣(9折)
public class DiscountStrategyImpl implements DiscountStrategy {
@Override
public BigDecimal calculateDiscount(BigDecimal orderAmount) {
return orderAmount.multiply(BigDecimal.valueOf(0.1));
}
}
// 策略3:无优惠
public class NoneDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal calculateDiscount(BigDecimal orderAmount) {
return BigDecimal.ZERO;
}
}
步骤 2:定义订单结算模板(模板模式核心)
// 订单结算模板(固定流程)
public abstract class OrderSettlementTemplate {
// 模板方法:固定结算流程
public final OrderResult settle(OrderDTO orderDTO, DiscountStrategy discountStrategy) {
// 步骤1:通用流程 - 校验商品(固定)
checkGoods(orderDTO);
// 步骤2:通用流程 - 计算基础金额(固定)
BigDecimal baseAmount = calculateBaseAmount(orderDTO);
// 步骤3:策略模式 - 优惠抵扣(算法可变)
BigDecimal discountAmount = discountStrategy.calculateDiscount(baseAmount);
// 步骤4:通用流程 - 计算最终金额(固定)
BigDecimal finalAmount = baseAmount.subtract(discountAmount);
// 步骤5:通用流程 - 生成订单(固定)
return createOrder(orderDTO, finalAmount);
}
// 通用流程:校验商品
private void checkGoods(OrderDTO orderDTO) {
System.out.println("校验商品:" + orderDTO.getGoodsIds());
}
// 通用流程:计算基础金额
private BigDecimal calculateBaseAmount(OrderDTO orderDTO) {
return orderDTO.getGoodsPrice().multiply(BigDecimal.valueOf(orderDTO.getNum()));
}
// 通用流程:生成订单
private OrderResult createOrder(OrderDTO orderDTO, BigDecimal finalAmount) {
return new OrderResult(orderDTO.getOrderNo(), finalAmount);
}
}
// 具体订单模板(可以扩展不同订单类型,比如普通订单/预售订单)
public class NormalOrderTemplate extends OrderSettlementTemplate {}
步骤 3:使用(动态切换策略)
public class Test {
public static void main(String[] args) {
// 1. 构建订单
OrderDTO order = new OrderDTO("ORDER123", Arrays.asList("G001"), BigDecimal.valueOf(100), 1);
// 2. 订单模板(流程固定)
NormalOrderTemplate template = new NormalOrderTemplate();
// 3. 策略1:满减抵扣
OrderResult result1 = template.settle(order, new FullReduceStrategy());
System.out.println("满减后金额:" + result1.getFinalAmount()); // 80
// 4. 策略2:折扣抵扣(不用改模板,直接换策略)
OrderResult result2 = template.settle(order, new DiscountStrategyImpl());
System.out.println("折扣后金额:" + result2.getFinalAmount()); // 90
}
}
策略模式代替了直接在子类填写模板模式核心算法的步骤,将不同子类实现的核心算法定义在策略模式中,我们这里用户直接传递要执行的策略,然后模板模式的固定流程中关于调用子类重写核心算法的步骤替换为执行选定策略的步骤,
// 步骤3:策略模式 - discountStrategy就是我们用户传递的选定策略,调用策略模式提供的统一接口方法calcalateDiscount来执行选定策略的方法,底层就是执行模板模式子类重写的核心算法
BigDecimal discountAmount = discountStrategy.calculateDiscount(baseAmount);
核心效果
- 订单结算流程永远是 “校验→算基础金额→抵扣→生成订单”,不会乱;
- 新增 “优惠券抵扣” 只需加一个
CouponDiscountStrategy类,不用改模板,也不用改现有代码(符合开闭原则)。
4、组合用法的核心优势
- 流程统一 + 算法灵活:模板保证 “步骤不乱”(比如支付必须先校验参数再调接口),策略保证 “算法可换”(支付宝 / 微信接口随便切);
- 代码复用最大化:通用流程只写一次(校验、结果处理、响应),核心算法封装成策略,新增渠道 / 算法只需加策略类;
- 符合开闭原则:新增功能不用改现有模板和策略,只加新类,企业维护成本极低;
- 可读性高:流程在模板里,算法在策略里,分工明确,新人一看就懂。
5、总结
- 常见度:模板 + 策略是开发的 “黄金组合”,尤其是支付、订单、优惠、风控等场景,几乎是标配;
- 核心场景:只要是 “流程固定,但核心步骤的算法需要动态切换”,就优先用这个组合;
- 记忆口诀:模板定流程,策略换算法,流程不变,算法灵活。
五、接口式模板模式(补充)
下面这些博主也是第一次遇见还能这么写,翻看AI时偶然发现的
一、先理解核心问题?
原继承式模板的问题:
- 子类必须继承抽象父类,和父类强绑定(父类改方法名,所有子类都要改);
- 一个子类只能继承一个父类,想复用多个模板逻辑根本做不到;
原版我们的模板模式是靠父子类实现的,这种的问题时父类名字变了,子类要跟着修改父类名字,这就存在代码耦合
组合式写法的核心改进:
- 通用流程:封装在静态工具类里(比如
SandwichTemplate),不用再用子类继承,直接调用; - 可变细节:通过函数式接口定义(比如
SandwichProcess),用 Lambda 传递,灵活替换; - 无耦合:工具类和业务逻辑完全分离,改工具类不影响业务,改业务不影响工具类。
函数式接口:只有一个抽象方法的接口(可以有默认方法 / 静态方法),专门用来封装 “一段逻辑”(比如 “准备煎蛋食材” 这个动作),是 Java 8 为了支持 Lambda 设计的核心语法。
这里我们解释以下可变细节为什么定义成函数式接口而不推荐为一个类
1、先看:如果用 “定义类” 的方式实现模板模式的可变细节
我们以 “三明治制作” 为例,先写出完整的 “类实现方案”,再找问题:
步骤 1:定义 “可变细节” 的抽象类 / 接口(必须先定义规范)
// 定义可变细节的规范(接口)
interface SandwichProcess {
void prepare();
void addIngredient();
}
// 步骤2:为每个场景写一个实现类
// 煎蛋三明治的细节类
class EggProcess implements SandwichProcess {
@Override
public void prepare() {
System.out.println("准备鸡蛋食材");
}
@Override
public void addIngredient() {
System.out.println("加煎蛋");
}
}
// 火腿三明治的细节类
class HamProcess implements SandwichProcess {
@Override
public void prepare() {
System.out.println("准备火腿食材");
}
@Override
public void addIngredient() {
System.out.println("加火腿");
}
}
// 步骤3:模板工具类调用这些类
class SandwichTemplate {
public static void make(SandwichProcess process) {
process.prepare();
toastBread(); // 通用步骤
process.addIngredient();
assemble(); // 通用步骤
}
private static void toastBread() { System.out.println("烤面包"); }
private static void assemble() { System.out.println("组装"); }
}
// 步骤4:业务调用
public class Test {
public static void main(String[] args) {
SandwichTemplate.make(new EggProcess()); // 传煎蛋类
SandwichTemplate.make(new HamProcess()); // 传火腿类
}
}
这种 “定义类” 的方式,存在 4 个致命问题(企业里绝对要规避)
问题 1:类爆炸 —— 新增一个场景就要新建一个类
- 做煎蛋三明治→
EggProcess类,做火腿三明治→HamProcess类; - 新增 “芝士三明治”→
CheeseProcess,新增 “培根三明治”→BaconProcess; - 企业场景中,比如 “支付渠道” 有 10 种,就要写 10 个
XXXPayProcess类 —— 项目里全是这种 “只有几行逻辑的小类”,维护成本极高(找类、改类都要翻半天)。
问题 2:代码冗余 —— 类的 “模板语法” 远多于 “核心逻辑”
每个XXXProcess类的核心逻辑只有 2 行(prepare+addIngredient),但必须写:
implements SandwichProcess(接口实现);@Override(方法重写);- 类的结构(
class XXX { ... });这些 “模板语法” 占了代码量的 80%,核心逻辑只占 20%—— 完全是 “为了写类而写类”,毫无意义。
问题 3:灵活性极差 —— 无法实现 “临时混搭逻辑”
比如要做 “火腿 + 煎蛋双拼三明治”,用类的方式只能:
- 新建
MixProcess类; - 实现
prepare()(准备鸡蛋 + 火腿); - 实现
addIngredient()(加煎蛋 + 火腿);而用函数式接口 + Lambda,直接一行搞定:
// 无需新建类,直接混搭逻辑
SandwichTemplate.make(
() -> System.out.println("准备鸡蛋+火腿食材"),
() -> {
System.out.println("加煎蛋");
System.out.println("加火腿");
}
);
企业里经常有 “临时的个性化逻辑”(比如某个活动的特殊优惠、某个客户的特殊支付规则),用类的方式根本无法快速适配 —— 总不能为每个临时需求都新建一个类吧?
问题 4:无法和现代 Java 生态集成
Java 8 + 的核心特性(Stream、CompletableFuture、Optional)都是基于 “函数式接口 + Lambda” 设计的,比如:
// Stream过滤逻辑(函数式接口Predicate)
List<Order> validOrders = orders.stream()
.filter(order -> order.getAmount() > 100) // Lambda
.collect(Collectors.toList());
如果用 “类” 的方式实现过滤逻辑,需要:
// 新建过滤类
class AmountFilter implements Predicate<Order> {
@Override
public boolean test(Order order) {
return order.getAmount() > 100;
}
}
// 调用Stream
List<Order> validOrders = orders.stream()
.filter(new AmountFilter()) // 传类对象
.collect(Collectors.toList());
完全脱离了 Java 现代开发的主流,代码既臃肿又不符合规范。
2、函数式接口 + Lambda vs 定义类(核心对比表)
表格
| 维度 | 定义类的方式 | 函数式接口 + Lambda |
|---|---|---|
| 代码量 | 每个场景 1 个类,代码量爆炸 | 无类,Lambda 一行搞定,代码量极少 |
| 灵活性 | 只能固定场景,无法临时混搭 | 任意逻辑组合,无需新建类 |
| 维护成本 | 类太多,找 / 改都麻烦 | 无类,逻辑直接写在调用处,易维护 |
| 生态适配 | 无法集成 Stream / 异步编程等 | 完美适配 Java 8 + 所有新特性 |
| 可读性 | 核心逻辑藏在类里,需跳转查看 | 核心逻辑直接写在调用处,一目了然 |
3、什么时候才适合 “定义类”?(补充边界)
不是所有场景都不能定义类 —— 当 “可变细节” 满足以下条件时,才适合写类:
- 逻辑复杂:可变细节有几十行代码,不是一两行 Lambda 能搞定的;
- 复用频率高:这个细节逻辑会在 10 个以上地方复用,值得封装成类;
- 有状态需要维护:比如需要保存 “配料数量”“烤面包时间” 等状态(Lambda 是无状态的,类可以有成员变量)。
简单场景用lambda,复杂场景用类
比如:
// 复杂逻辑+有状态,适合写类
class ComplexCheeseProcess implements SandwichProcess {
private int cheeseNum; // 有状态:芝士片数量
public ComplexCheeseProcess(int cheeseNum) {
this.cheeseNum = cheeseNum;
}
@Override
public void prepare() {
// 复杂逻辑:校验芝士保质期、切芝士片等
System.out.println("准备" + cheeseNum + "片芝士,校验保质期");
}
@Override
public void addIngredient() {
System.out.println("分层加芝士片,共" + cheeseNum + "片");
}
}
但模板模式的核心场景是 “通用流程 + 简单可变细节”(比如三明治的 “加煎蛋 / 火腿”、支付的 “调支付宝 / 微信接口”),这些场景的可变细节都是 “一两行逻辑”,完全没必要写类。
总结:为什么不定义类,非要用函数式接口?
核心结论:在模板模式 “传递简单可变细节” 的场景下,定义类的方式 “成本高、灵活性差、代码冗余”,而函数式接口 + Lambda 恰好能解决这些问题—— 用最少的代码、最高的灵活性、最低的维护成本,实现 “传递可变细节” 的核心目标。
二、完整可运行代码(三明治场景)
步骤 1:定义 “可变细节” 的函数式接口
这个接口只定义 “需要子类自定义的步骤”,相当于原继承写法中的 “抽象方法”:
/**
* 函数式接口:定义三明治制作的可变细节
* 替代原继承写法中的抽象方法(prepareIngredients()、addCoreIngredient())
*/
@FunctionalInterface // 标记为函数式接口,支持Lambda
public interface SandwichProcess {
// 封装所有可变步骤(也可以拆成多个方法,看业务复杂度)
void process();
}
// 进阶:如果可变步骤多,可拆成多个函数式接口(更灵活)
// 准备食材的接口
@FunctionalInterface
interface PrepareStep {
void prepare();
}
// 加核心配料的接口
@FunctionalInterface
interface AddIngredientStep {
void add();
}
步骤 2:定义 “通用流程” 的工具类(核心模板)
这个类封装所有通用步骤,替代原继承写法中的 “抽象父类”,但不用继承,直接调用静态方法:
/**
* 三明治制作模板工具类(封装通用流程)
* 替代原继承写法的AbstractSandwich抽象父类
*/
public class SandwichTemplate {
// 核心模板方法:固定流程(和原makeSandwich()逻辑一致)
// 入参是“可变细节”的接口,支持Lambda传递
public static void makeSandwich(PrepareStep prepareStep, AddIngredientStep addStep) {
// 通用流程1:准备食材(可变,由Lambda传入)
prepareStep.prepare();
// 通用流程2:烤面包(固定,工具类内部实现)
toastBread();
// 通用流程3:加核心配料(可变,由Lambda传入)
addStep.add();
// 通用流程4:组装(固定)
assemble();
// 通用流程5:装盘(固定)
plate();
System.out.println("=== 三明治做好啦 ===");
}
// ---------------------- 以下是所有通用步骤(固定逻辑) ----------------------
private static void toastBread() {
System.out.println("2. 烤面包片(烤到两面全焦)");
}
private static void assemble() {
System.out.println("4. 抹沙拉酱,把核心配料夹进面包中间");
}
private static void plate() {
System.out.println("5. 装盘,放番茄片装饰");
}
}
步骤 3:业务调用(核心:Lambda 传递可变细节)
不用写子类,直接调用工具类,用 Lambda 传入 “煎蛋 / 火腿” 的特有逻辑:
/**
* 业务调用类(无子类,无继承)
*/
public class TestSandwich {
public static void main(String[] args) {
System.out.println("===== 制作煎蛋三明治 =====");
// 调用模板工具类,Lambda传递煎蛋的可变细节
SandwichTemplate.makeSandwich(
// 第一个Lambda:准备煎蛋食材(对应PrepareStep接口)
() -> System.out.println("1. 准备食材:面包片、鸡蛋、沙拉酱"),
// 第二个Lambda:加煎蛋(对应AddIngredientStep接口)
() -> System.out.println("3. 煎鸡蛋(单面煎,保留溏心)")
);
System.out.println("\n===== 制作火腿三明治 =====");
// 调用同一个模板,换Lambda就是火腿的逻辑
SandwichTemplate.makeSandwich(
() -> System.out.println("1. 准备食材:面包片、火腿、沙拉酱"),
() -> System.out.println("3. 煎火腿(煎到边缘微焦)")
);
}
}
运行结果
===== 制作煎蛋三明治 =====
1. 准备食材:面包片、鸡蛋、沙拉酱
2. 烤面包片(烤到两面全焦)
3. 煎鸡蛋(单面煎,保留溏心)
4. 抹沙拉酱,把核心配料夹进面包中间
5. 装盘,放番茄片装饰
=== 三明治做好啦 ===
===== 制作火腿三明治 =====
1. 准备食材:面包片、火腿、沙拉酱
2. 烤面包片(烤到两面全焦)
3. 煎火腿(煎到边缘微焦)
4. 抹沙拉酱,把核心配料夹进面包中间
5. 装盘,放番茄片装饰
=== 三明治做好啦 ===
三、和原继承写法的核心对比(一目了然)
表格
| 维度 | 继承式模板(教科书) | 组合式模板(企业实战) |
|---|---|---|
| 核心载体 | 抽象父类(AbstractSandwich) | 静态工具类(SandwichTemplate) |
| 可变细节传递方式 | 子类重写抽象方法 | Lambda 传递函数式接口 |
| 耦合度 | 子类和父类强耦合(继承) | 无耦合(工具类和业务逻辑分离) |
| 扩展性 | 子类只能继承一个父类,扩展受限 | 可同时调用多个模板工具类,灵活 |
| 代码量 | 每个业务场景要写一个子类 | 无子类,直接 Lambda 调用,代码更少 |
| Spring 适配性 | 子类要加 @Component,管理复杂 | 工具类可加 @Component,或直接静态调用 |