23种设计模式:模板方法模式与策略模式
摘要:本文先讲解模板方法模式(定义通用流程、抽象差异步骤,以炒菜为例),再阐述策略模式(封装算法为策略类、通过环境角色调用,以促销活动为例),含定义、结构等。
思维导图
1 模板方法模式
1.1 定义
模板方法:定义一套通用的执行流程,让子类负责每个执行步骤的具体实现
模板方法的适用场景:适用于有规范的流程,且执行流程可以复用
作用:大幅节省重复代码量,便于项目扩展、更好维护
1.2 结构
模板方法模式 包含以下主要角色:
抽象类:负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。
具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
具体子类:实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
1.3 案例实现
【例】炒菜
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
1.4 实现思路
目前我们的 “炒菜功能” 代码里,写了两种不同的炒菜方法(炒手撕包菜、炒蒜蓉菜心),但是会发现,这两种方法的流程完全一致、而且大多数代码都是相同的(比如都需要倒油、热油、翻炒,只有放什么菜、放什么调料不一样)。
这种情况下,我们就要运用设计模式 —— 模板方法模式 对代码进行优化。
模板方法模式是行为型设计模式,适用于具有通用处理流程、但处理细节不同的情况。通过定义一个抽象模板类,提供通用业务流程处理逻辑,并将不同的部分定义为抽象方法,由子类具体实现。
在我们的 “炒菜” 场景中,两种炒菜方法的流程都是:
- 倒油
- 热油
- 倒蔬菜
- 倒调味料
- 翻炒
可以将这些流程抽象为一套模板(抽象类),将每个实现不一样的步骤都定义为抽象方法,比如:
- 倒蔬菜(一种放包菜、一种放菜心)
- 倒调味料(一种放辣椒、一种放蒜蓉)
1.5 代码实现
1)定义抽象模板类—— 固定炒菜通用流程
抽象模板类的核心是 “模板方法”(用final
修饰,防止子类修改流程顺序),同时区分 “通用步骤” 和 “差异步骤”:
- 通用步骤:直接在抽象类中实现(所有炒菜方法都一样的操作,比如倒油、热油、翻炒)
- 差异步骤:定义为抽象方法(由子类实现具体细节,比如放什么菜、放什么调料)
我们创建抽象类AbstractClass
,其中cookProcess()
是模板方法,固定 5 步炒菜流程;“倒油、热油、翻炒” 是通用步骤直接实现;“倒蔬菜、倒调味料” 是差异步骤定义为抽象方法,代码如下:
package cook.dish;// 抽象模板类:定义炒菜的通用流程
public abstract class AbstractClass {/*** 模板方法:炒菜的完整流程(核心)* 用final修饰 → 子类不能修改流程顺序,保证所有炒菜方法都遵循“倒油→热油→放菜→放调料→翻炒”的顺序*/public final void cookProcess() {this.pourOil();this.heatOil();this.pourVegetable();this.pourSauce();this.fry();}// 第一步:倒油(通用步骤,具体实现固定)public void pourOil() {System.out.println("倒油");}// 第二步:热油(通用步骤,具体实现固定)public void heatOil() {System.out.println("热油");}// 第三步:倒蔬菜(差异步骤,抽象方法,由子类具体实现)public abstract void pourVegetable();// 第四步:倒调味料(差异步骤,抽象方法,由子类具体实现)public abstract void pourSauce();// 第五步:翻炒(通用步骤,具体实现固定)public void fry(){System.out.println("炒啊炒啊炒到熟啊");}
}
2)定义子类的具体实现
我们创建ConcreteClass_BaoCai
,继承抽象类AbstractClass
,并实现所有抽象方法(也就是 “倒蔬菜” 和 “倒调味料” 的差异步骤),专注于 “炒手撕包菜” 的特有逻辑,代码如下:
package cook.dish;// 具体实现类:对应“炒手撕包菜”(类似文件上传场景的第一种上传方式)
public class ConcreteClass_BaoCai extends AbstractClass {// 实现抽象方法:倒蔬菜(手撕包菜的特有逻辑:放包菜)@Overridepublic void pourVegetable() {System.out.println("下锅的蔬菜是包菜");}// 实现抽象方法:倒调味料(手撕包菜的特有逻辑:放辣椒)@Overridepublic void pourSauce() {System.out.println("下锅的酱料是辣椒");}
}
创建ConcreteClass_CaiXin
,继承AbstractClass
,实现抽象方法,专注于 “炒蒜蓉菜心” 的特有逻辑
package cook.dish;// 具体实现类:对应“炒蒜蓉菜心”(类似文件上传场景的第二种上传方式)
public class ConcreteClass_CaiXin extends AbstractClass {// 实现抽象方法:倒蔬菜(蒜蓉菜心的特有逻辑:放菜心)@Overridepublic void pourVegetable() {System.out.println("下锅的蔬菜是菜心");}// 实现抽象方法:倒调味料(蒜蓉菜心的特有逻辑:放蒜蓉)@Overridepublic void pourSauce() {System.out.println("下锅的酱料是蒜蓉");}
}
3)创建客户端类—— 调用炒菜功能
package cook.dish;// 客户端类:调用具体的炒菜实现(类似文件上传场景的FileManagerClient)
public class Client {public static void main(String[] args) {// 1. 炒手撕包菜:创建ConcreteClass_BaoCai实例,调用模板方法ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();baoCai.cookProcess(); // 会自动按“倒油→热油→放包菜→放辣椒→翻炒”执行// 2. 炒蒜蓉菜心:创建ConcreteClass_CaiXin实例,调用模板方法ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();caiXin.cookProcess(); // 会自动按“倒油→热油→放菜心→放蒜蓉→翻炒”执行}
}
注意:为防止恶意操作,一般模板方法都加上 final 关键词。
1.6 优缺点
优点
- 封装不变部分:算法的不变部分被封装在父类中。
- 扩展可变部分:子类可以扩展或修改算法的可变部分。
- 提取公共代码:减少代码重复,便于维护。
缺点
- 类数目增加:每个不同的实现都需要一个子类,可能导致系统庞大。
1.7 适用场景
算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
2 策略模式
2.1 定义
策略模式是一种对象行为型设计模式,其核心思想是:针对某一业务场景下的一系列相似算法(或逻辑),先将每个算法(或逻辑)单独封装成独立的 “策略类”,再通过统一的接口让这些策略类可以相互替换。
在实际开发中,当业务场景存在大量 “if...else” 或 “switch” 判断逻辑时,采用策略模式能有效优化代码结构:无需将所有判断逻辑与业务实现混在一起,而是把每种判断对应的逻辑封装成独立策略,既便于后续对单个策略的单独维护与修改,也让整体代码更清晰、可扩展性更强。
2.2 结构
策略模式包含以下几个核心角色:
- 环境(Context):维护一个对策略对象的引用,负责将客户端请求委派给具体的策略对象执行。环境类可以通过依赖注入、简单工厂等方式来获取具体策略对象。
- 抽象策略(Abstract Strategy):定义了策略对象的公共接口或抽象类,规定了具体策略类必须实现的方法。
- 具体策略(Concrete Strategy):实现抽象策略定义的接口或抽象类,包含具体的算法实现。
2.3 案例实现
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:
2.4 实现思路
1. 第一步:定义 “策略标准”
先明确所有促销策略的 “统一行为”—— 向客户展示促销内容,因此定义show()方法,让所有具体策略都遵循这个标准,避免出现 “有的策略用show(),有的用display()” 的混乱。
2. 第二步:封装 “具体策略”
将每个节日的促销逻辑单独抽成类,分别实现Strategy接口:
- 春节→StrategyA(买一送一)
- 中秋→StrategyB(满 200 减 50)
- 圣诞→StrategyC(满 1000 加 1 元换购)
这样做的好处是:修改某节日策略时,不会影响其他策略;新增节日(如国庆)时,只需新建StrategyD实现Strategy接口,无需修改现有代码(符合 “开闭原则”)。
3. 第三步:设计 “桥梁角色”
用SalesMan类承担 “使用策略” 的责任:
- 通过构造方法接收具体策略(如new SalesMan(new StrategyA())表示 “销售员要推广春节策略”);
- 提供salesManShow()方法,内部调用strategy.show()—— 无论传入的是哪个策略,都能通过统一接口触发促销展示,客户端无需关心策略细节。
2.5 代码实现
1. 抽象策略接口(Strategy)—— 定义策略统一标准
public interface Strategy {void show();
}
2. 具体策略类(StrategyA/B/C)—— 实现具体策略逻辑
这三个类是 “抽象策略角色” 的子类,对应不同的促销规则,属于策略模式的 “具体策略角色”。
//为春节准备的促销活动A
public class StrategyA implements Strategy {
public void show() {System.out.println("买一送一");}
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
public void show() {System.out.println("满200元减50元");}
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
public void show() {System.out.println("满1000元加一元换购任意200元以下商品");}
}
3. 环境角色类(SalesMan)—— 连接策略与客户端,统一调用
public class SalesMan { // 持有抽象策略角色的引用(关键:面向接口,不面向具体实现) private Strategy strategy; // 构造方法:接收具体策略,初始化时确定要使用的促销策略 public SalesMan(Strategy strategy) { this.strategy = strategy; } // 对外提供的统一方法:向客户展示促销活动 public void salesManShow(){ strategy.show(); // 调用具体策略的show()方法,无需关心是哪个策略 }
}
2.6 优缺点
优点
- 算法切换自由:可以在运行时根据需要切换算法。
- 避免多重条件判断:消除了复杂的条件语句。
- 扩展性好:新增算法只需新增一个策略类,无需修改现有代码。
缺点
- 策略类数量增多:每增加一个算法,就需要增加一个策略类。
- 所有策略类都需要暴露:策略类需要对外公开,以便可以被选择和使用。
2.7 使用场景
一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
系统要求使用算法的客户不应该知道其操作的数据时,可用策略模式来隐藏相关的数据结构。
多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。