品设计模式 - (行为型) 模板方法模式 Template Method

128 阅读9分钟
这里写图片替代文字

简介

模板方法模式定义了一种 算法框架,并允许子类在不改变算法结构的前提下,重新定义算法中的某些步骤。

核心思想: 通过 ⌈抽象类⌋ 定义算法的骨架,并将部分实现延迟到子类,从而实现 ⌈代码复用⌋ 和 ⌈反向控制⌋

模板方法模式定义

定义一个操作的算法骨架,将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构接口重新定义该算法的某些特定步骤。

解决的问题

  1. 提高代码的复用性: 将不变的部分提取到抽象父类中实现代码共享,避免代码重复,并且将不可变的部分延迟到子类中,支持子类具体实现
  2. 实现反向控制: 父类调用子类的方法,而不是传统的子类调用父类方法。子类可以通过 ⌈继承和方法重写⌋ 来改变算法的部分行为,而不用修改整体流程。

本质

固定算法骨架,允许子类扩展特定步骤以实现不同的具体行为

入门案例 - 不同样式的相同循环打印字符

将字符和字符串循环显示 5 次的

出现的类列表一览

类名作用
AbstractDisplay只实现了 display 方法的抽象类
CharDisplay实现了 open、print、close 方法的类
StringDisplay实现了 open、print、close 方法的类
Main测试程序的类

类图示意

画板

AbstractDisplay 类

display() 方法中会执行以下处理:

  1. 调用 open() 方法
  2. 调用 5 次 print() 方法
  3. 调用 close() 方法
public abstract class AbstractDisplay {     // 抽象类 AbstractDisplay

    // 给子类实现的抽象方法 open
    public abstract void open();

    // 给子类实现的抽象方法 print
    public abstract void print();

    // 给子类实现的抽象方法 close
    public abstract void close();

    public final void display() {
        open();     // 先打开
        for (int i = 0; i < 5; i++) {   // 循环调用 5 次
            print();
        }
        close();    // 最后关闭
    }
}

实现子类一:CharDisplay

CharDisplay 类分别实现抽象方法如下:

  1. open(): 显示字符串 "<<"
  2. print(): 显示构造函数的一个字符
  3. close(): 显示字符串 ">>"
public class CharDisplay extends AbstractDisplay {

    // 待显示的字符
    private char c;

    // 构造器
    public CharDisplay(char c) {
        this.c = c;
    }

    @Override
    public void open() {        // 重写父类中的抽象方法,显示起始字符
        System.out.print("<<");
    }

    @Override
    public void print() {   // 重写父类中的 print 方法,显示保存在字段 c 中的字符
        System.out.println(this.getClass().getSimpleName() + " print:" + c);
    }

    @Override
    public void close() {   // 重写父类中的 close 方法并且显示结束字符
        System.out.println(">>");
    }
}

实现子类二:StringDisplay

实现抽象方法的作用:

  1. open(): 显示字符串 "+-----+"
  2. print(): 在构造函数接收的字符串前后分别加上 "|"
  3. close(): 显示字符串 "+------+"
public class StringDisplay extends AbstractDisplay {    // 抽象类的子类

    private String str;     // 待显示的字符串 
    private int width;     // 计算出待输出字符串的长度,按照字节计算

    public StringDisplay(String str, int width) {
        this.str = str;
        this.width = str.getBytes().length;
    }

    @Override
    public void open() {
        buildBound();
    }

    @Override
    public void print() {
        System.out.println("|" + str + "|");
    }

    @Override
    public void close() {
        buildBound();
    }

    // 构建边界
    private void buildBound() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < width + 2; i++) {
            if (i == 0 || i == width + 1) {
                sb.append("+");
            } else {
                sb.append("-");
            }
        }
        System.out.println(sb);
    }
}

Main 类测试

public class Main {

    public static void main(String[] args) {
        // 生成一个持有 'H' 的 CharDisplay 类的实例
        AbstractDisplay d1 = new CharDisplay('H');
        // 生成一个持有 "Hello, world." 的 StringDisplay 类的实例
        AbstractDisplay d2 = new StringDisplay("Hello, world.", 15);
        // 生成一个持有 "你好,世界。" 的 StringDisplay 类的实例
        AbstractDisplay d3 = new StringDisplay("!!Hello, Jools wakoo!!", 15);
        d1.display();   // 测试输出字符
        d2.display();   // 测试输出字符串
        d3.display();

        /*
         输出结果:
            <<CharDisplay print:H
            CharDisplay print:H
            CharDisplay print:H
            CharDisplay print:H
            CharDisplay print:H
            >>
            +-------------+
            |Hello, world.|
            |Hello, world.|
            |Hello, world.|
            |Hello, world.|
            |Hello, world.|
            +-------------+
            +----------------------+
            |!!Hello, Jools wakoo!!|
            |!!Hello, Jools wakoo!!|
            |!!Hello, Jools wakoo!!|
            |!!Hello, Jools wakoo!!|
            |!!Hello, Jools wakoo!!|
            +----------------------+
        */
    }
}

Template 模式中登场的角色

角色名称简介
AbstractClass (抽象类)负责实现模板方法,声明在模板方法中所使用到的抽象方法
ConcreteClass (具体类)负责具体实现 AbstractClass 角色中定义的抽象方法

Template Method 模式的类图

画板

模板定义和写法

通常在模板里面包含:

  1. 模板方法:定义算法骨架的方法
  2. 具体的操作:模板中直接实现某些步骤的方法
  3. 具体的 AbstractClass 操作:实现某些公共功能,可以提供给子类使用
  4. 原语操作:模板中定义的抽象操作,通常是 ⌈模板方法⌋ 需要调用的操作,是必须操作
  5. 钩子操作:模板中定义,提供默认实现的操作

示例: 一个较完整的模板定义示例

public abstract class AbstractTemplate {

    /*
     模板方法,定义算法骨架
    */
    public final void templateMethod() {
        this.operation1();	// 第一步
        this.operation2();	// 第二步
        this.doPrimitiveOperation1();	// 原语操作
        this.doPrimitiveOperation2();	// 原语操作
        this.hookOperation();	// 钩子函数操作
    }

    // 具体操作 01,算法中的步骤,固定实现
    private void operation1() {
        // do something
    }

    // 具体操作 02,算法中的步骤,固定实现
    private void operation2() {
        // do something
    }

    // 原语操作 1, 算法中的必要操作,父类无法确定如何实现,需要子类实现
    protected abstract void doPrimitiveOperation1();

    // 原语操作 2, 需要子类实现
    protected abstract void doPrimitiveOperation2();

    // 钩子函数, 可以由子类选择并实现
    protected void hookOperation1() {
        // 提供实现
    }
}

模板方法模式优缺点

优点

  1. 提高代码复用性: 通过 抽象类 统一 算法骨架,避免代码重复。
  2. 增强代码扩展性: 允许子类 灵活扩展 具体步骤,而无需修改整体结构(符合 开闭原则)。
  3. 实现反向控制 (IoC): 由 父类调用子类的方法,而不是传统的 子类调用父类方法,提供更好的可维护性。

缺点

  1. 增加类的数量: 引入了抽象类,不同的实现都需要一个子类来实现,导致类的个数增加,进而增加了系统的复杂度
  2. 对子类的要求比较高: 算法骨架不容易升级。模板和子类非常耦合,如果需要对模板中的算法骨架进行变更,则会导致所有相关子类进行相应的变化

适用场景

  1. 需要定义算法的 骨架,但允许子类扩展某些步骤。
  2. 需要提取 公共代码,避免代码重复,提高复用性。
  3. 需要 控制子类的扩展,确保算法结构稳定。

扩展:类的层次思考,子类与父类

一般来说,站在子类的角度思考,子类的特点:

  1. 在子类中可以使用父类中定义的方法
  2. 并且可以通过子类中增加方法以实现新的功能
  3. 子类中重写父类的方法可以改变程序的行为

而站在父类的角度,如果声明了一个抽象方法,期望子类:

  1. 去实现该抽象方法
  2. 要求子类去实现该抽象方法

为什么选用抽象类而不是接口?

抽象类的意义: 可以由具体的实现方法,而接口中所有的方法都是没有具体的实现的。可以既约束子类的行为,又要为子类提供公共功能。

而由于模板方法模式需要固定定义算法的骨架,所以这个骨架只有一份,算是一个公共的行为。而将模板实现为抽象类可以实现 ⌈为子类提供公共功能⌋,因为抽象类中已经定义具体算法骨架;在模板里把需子类扩展的具体步骤算法定义为抽象方法,⌈要求子类实现是 ⌈约束子类行为 的体现。

入门案例 - 冲茶饮

需求分析

冲咖啡和冲茶都有类似的流程,但是某些步骤会有一些不一样,要求复用哪些相同重复的步骤

假设冲咖啡的步骤如下: prepare()

  1. boilWater()
  2. brewCoffeeGrinds()
  3. pourInCup()
  4. addSugarAndMilke()

冲茶步骤如下:prepare()

  1. boilWater()
  2. steepTeaBag()
  3. pourInCup()
  4. addLemon()

类图设计

画板

抽象类 - CaffeineBeverage

public abstract class CaffineBeverage {

    // 模板方法 prepare 定义了算法的骨架
    public void prepare() {
        boilWater();        // 1. 烧水
        brew();             // 2. 冲泡
        pourInCup();        // 3. 倒进杯子
        addCondiments();    // 4. 加调料
    }

    // 抽象方法 - 烘培
    protected abstract void brew();

    // 抽象方法 - 加调料
    protected abstract void addCondiments();

    // 公共方法 - 倒进杯子
    protected void pourInCup() {
        System.out.println("pour in cup");
    }

    // 公共方法 - 烧水
    protected void boilWater() {
        System.out.println("boil water");
    }
}

具体类 01:咖啡 Coffee

public class Coffee extends CaffineBeverage{
    @Override
    protected void brew() {
        System.out.println(this.getClass().getSimpleName() + " brew!!!!");
    }

    @Override
    protected void addCondiments() {
        System.out.println(this.getClass().getSimpleName() + " add Coffee-Mate and Sugar");
    }
}

具体类 02: 茶 Tea

public class Tea extends CaffineBeverage{
    @Override
    protected void brew() {
        System.out.println(this.getClass().getSimpleName() + " brew!!!!");
    }

    @Override
    protected void addCondiments() {
        System.out.println(this.getClass().getSimpleName() + " add Lemon");
    }
}

测试

public class Main {

    public static void main(String[] args) {
        CaffineBeverage makeCoffee = new Coffee();
        CaffineBeverage makeTea = new Tea();

        makeCoffee.prepare();
        System.out.println("===========");
        makeTea.prepare();

        /*
            boil water
            Coffee brew!!!!
            pour in cup
            Coffee add Coffee-Mate and Sugar
            ===========
            boil water
            Tea brew!!!!
            pour in cup
            Tea add Lemon
        */
    }
}

巩固案例 - 咖啡馆

原题来源:卡码网 - 设计模式之模板方法模式

原题描述

小明喜欢品尝不同类型的咖啡,她发现每种咖啡的制作过程有一些相同的步骤,他决定设计一个简单的咖啡制作系统,使用模板方法模式定义咖啡的制作过程。系统支持两种咖啡类型:美式咖啡(American Coffee)和拿铁(Latte)。

咖啡制作过程包括以下步骤:

  1. 研磨咖啡豆 Grinding coffee beans
  2. 冲泡咖啡 Brewing coffee
  3. 添加调料 Adding condiments

其中,美式咖啡和拿铁的调料添加方式略有不同, 拿铁在添加调料时需要添加牛奶Adding milk

分析

设计模板类:CoffeeMaker

  1. 模板方法:prepare() 具体制作咖啡的流程定义
  2. 具体的 AbstractClass 操作:步骤为 selectCoffee -> grind -> brew -> addCondiments
  3. 原语操作
    1. selectCoffee():选取咖啡类型,子类必须实现
    2. addCondiments():该咖啡类型具体添加的调料,子类必须实现

代码实现

import java.util.*;

public class Main {
    
    
    public static void main(String[] args) {
        
        Scanner s = new Scanner(System.in);
        
        CoffeeMaker coffeeMaker;
        while(s.hasNextLine()) {
            String type = s.nextLine();
            if(type.equals("1")) {
                coffeeMaker = new AmericanMaker();
                
            } else if (type.equals("2")) {
                coffeeMaker = new LatteMaker();
            } else {
                throw new RuntimeException("Unsupported coffee type!");
            }
            coffeeMaker.prepare();
            System.out.println();
        }
        
        s.close();  // 关闭流
    }
}

abstract class CoffeeMaker {
    
    protected void prepare() {
        this.selectCoffee();  // 步骤一: 选取咖啡豆
        this.grind();         // 步骤二:研磨咖啡豆
        this.brew();          // 步骤三: 煮
        this.addCondiments(); // 步骤四:煮完后添加调料
    }
    
    // 具体操作 
    void brew() {
        System.out.println("Brewing coffee");
    }
    
    // 具体操作
    void grind() {
        System.out.println("Grinding coffee beans");
    }
    
    // 原语操作,子类实现
    protected abstract void selectCoffee();
    
    //  原语操作,子类实现
    protected abstract void addCondiments();

}

// 具体实现子类: 美式咖啡
class AmericanMaker extends CoffeeMaker {
    
    @Override
    public final void selectCoffee() {
        System.out.println("Making American Coffee:");
    }
    
    @Override
    public final void addCondiments() {
        System.out.println("Adding condiments");
    }
}

// 具体实现子类:拿铁咖啡
class LatteMaker extends CoffeeMaker {
    
    @Override
    public final void selectCoffee() {
        System.out.println("Making Latte:");
    }
    
    @Override
    public final void addCondiments() {
        System.out.println("Adding milk");
        System.out.println("Adding condiments");
    }
}

参考

  1. 个人回答 - 面试鸭(模板设计模式)
  2. 《图解设计模式》 - 模板方法模式
  3. Carson 带你学设计模式 - 《模板方法模式》
  4. 七寸知架构 - 模板方法模式
  5. pdai-行为型模板方法模式