23种设计模式:模板方法模式与策略模式

摘要:本文先讲解模板方法模式定义通用流程抽象差异步骤,以炒菜为例),再阐述策略模式封装算法为策略类通过环境角色调用,以促销活动为例),含定义、结构等。

思维导图

1 模板方法模式

1.1 定义

模板方法:؜定义一套通用的执行⁠流程,让子类负责每‏个执行步骤的具体实‌现

模板方法的适用场景:适用于有规范的流程,且执行流程可以复用

作用:大幅节省重复代码量,便于项目扩展、更好维护

1.2 结构

模板方法模式 包含以下主要角色:

抽象类:负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

  • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。

  • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:

    • 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。

    • 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。

    • 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。

具体子类:实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

1.3 案例实现

【例】炒菜

炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:

1.4 实现思路

目前我们的 "炒菜功能" 代码里,写了两种不同的炒菜方法(炒手撕包菜、炒蒜蓉菜心),但是会发现,这两种方法的流程完全一致、而且大多数代码都是相同的(比如都需要倒油、热油、翻炒,只有放什么菜、放什么调料不一样)。

这种情况下,我们就要运用设计模式 ------ 模板方法模式 对代码进行优化。

模板方法模式是行为型设计模式,适用于具有通用处理流程、但处理细节不同的情况。通过定义一个抽象模板类,提供通用业务流程处理逻辑,并将不同的部分定义为抽象方法,由子类具体实现。

在我们的 "炒菜" 场景中,两种炒菜方法的流程都是:

  1. 倒油
  2. 热油
  3. 倒蔬菜
  4. 倒调味料
  5. 翻炒

可以将这些流程抽象为一套模板(抽象类),将每个实现不一样的步骤都定义为抽象方法,比如:

  • 倒蔬菜(一种放包菜、一种放菜心)
  • 倒调味料(一种放辣椒、一种放蒜蓉)

1.5 代码实现

**1)**定义抽象模板类------ 固定炒菜通用流程

抽象模板类的核心是 "模板方法"(用final修饰,防止子类修改流程顺序),同时区分 "通用步骤" 和 "差异步骤":

  • 通用步骤:直接在抽象类中实现(所有炒菜方法都一样的操作,比如倒油、热油、翻炒)
  • 差异步骤:定义为抽象方法(由子类实现具体细节,比如放什么菜、放什么调料)

我们创建抽象类AbstractClass,其中cookProcess()是模板方法,固定 5 步炒菜流程;"倒油、热油、翻炒" 是通用步骤直接实现;"倒蔬菜、倒调味料" 是差异步骤定义为抽象方法,代码如下:

java 复制代码
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,并实现所有抽象方法(也就是 "倒蔬菜" 和 "倒调味料" 的差异步骤),专注于 "炒手撕包菜" 的特有逻辑,代码如下:

java 复制代码
package cook.dish;

// 具体实现类:对应"炒手撕包菜"(类似文件上传场景的第一种上传方式)
public class ConcreteClass_BaoCai extends AbstractClass {

    // 实现抽象方法:倒蔬菜(手撕包菜的特有逻辑:放包菜)
    @Override
    public void pourVegetable() {
        System.out.println("下锅的蔬菜是包菜");
    }

    // 实现抽象方法:倒调味料(手撕包菜的特有逻辑:放辣椒)
    @Override
    public void pourSauce() {
        System.out.println("下锅的酱料是辣椒");
    }
}

创建ConcreteClass_CaiXin,继承AbstractClass,实现抽象方法,专注于 "炒蒜蓉菜心" 的特有逻辑

java 复制代码
package cook.dish;

// 具体实现类:对应"炒蒜蓉菜心"(类似文件上传场景的第二种上传方式)
public class ConcreteClass_CaiXin extends AbstractClass {
    // 实现抽象方法:倒蔬菜(蒜蓉菜心的特有逻辑:放菜心)
    @Override
    public void pourVegetable() {
        System.out.println("下锅的蔬菜是菜心");
    }

    // 实现抽象方法:倒调味料(蒜蓉菜心的特有逻辑:放蒜蓉)
    @Override
    public void pourSauce() {
        System.out.println("下锅的酱料是蒜蓉");
    }
}

3)创建客户端类------ 调用炒菜功能

java 复制代码
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)------ 定义策略统一标准

java 复制代码
public interface Strategy {
    void show();
}

2. 具体策略类(StrategyA/B/C)------ 实现具体策略逻辑

这三个类是 "抽象策略角色" 的子类,对应不同的促销规则,属于策略模式的 "具体策略角色"。

java 复制代码
//为春节准备的促销活动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)------ 连接策略与客户端,统一调用

java 复制代码
public class SalesMan {                
    // 持有抽象策略角色的引用(关键:面向接口,不面向具体实现)        
    private Strategy strategy;          
                                        
    // 构造方法:接收具体策略,初始化时确定要使用的促销策略        
    public SalesMan(Strategy strategy) { 
        this.strategy = strategy;       
    }                                  
                                        
    // 对外提供的统一方法:向客户展示促销活动        
    public void salesManShow(){        
        strategy.show(); // 调用具体策略的show()方法,无需关心是哪个策略        
    }                                  
}

2.6 优缺点

优点

  1. 算法切换自由:可以在运行时根据需要切换算法。
  2. 避免多重条件判断:消除了复杂的条件语句。
  3. 扩展性好:新增算法只需新增一个策略类,无需修改现有代码。

缺点

  1. 策略类数量增多:每增加一个算法,就需要增加一个策略类。
  2. 所有策略类都需要暴露:策略类需要对外公开,以便可以被选择和使用。

2.7 使用场景

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。

  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。

  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。

  • 系统要求使用算法的客户不应该知道其操作的数据时,可用策略模式来隐藏相关的数据结构。

  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。