Java设计模式及应用场景之《模板方法模式》

文章目录

      • 一、模板方法模式定义
      • 二、模板方法模式的结构和说明
      • 三、模板方法模式示例
      • 四、钩子方法
      • 五、模板方法模式的优缺点
      • 六、模板方法模式的应用场景及案例

一、模板方法模式定义

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses.
Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
(定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。)

二、模板方法模式的结构和说明

在这里插入图片描述

  • AbstractClass: 抽象模板类,用来定义模板方法和基本方法。模板方法内部定义的是一个框架,实现对基本方法的调用,完成固定格式的逻辑操作。基本方法包括抽象方法和普通方法,抽象方法需要延迟到子类中实现。
  • ConcreteClass: 具体实现类,用来实现父类中定义的一个或多个基本方法,完成子类相关的特定功能。

三、模板方法模式示例

假设我们有个制作饮品的功能,我们可以制作茶和咖啡。

制作茶的步骤如下:

  1. 将水烧开
  2. 将茶叶放入杯中
  3. 将烧好的水倒入杯中
  4. 放入柠檬

制作咖啡的步骤如下:

  1. 将水烧开
  2. 将咖啡粉倒入杯中
  3. 将烧好的水倒入杯中
  4. 放入咖啡伴侣

由上可以看到,制作茶和咖啡的步骤基本相同,并且其中第一步和第三部处理过程完全一样,如果逻辑都各自实现的话,是不是就会造成代码重复呢。

我们可以使用模板方法模式来避免这种情况。

首先定义一个抽象模板类,模板类中有一个模板方法,模板方法依次调用烧水、将主材料放入杯中、将烧好的水倒入杯中、放入调味品这四个处理步骤方法。

/**
 * 制作饮品(抽象模板类)
 */
public abstract class Drinks {

    /**
     * 模板方法:制作饮品
     * 定义成final,防止子类重写
     */
    public final void makeDrinks(){

        // 1、将水烧开
        boilWater();

        // 2、将主材料放入杯中
        drinksIntoCup();

        // 3、将烧好的水倒入杯中
        waterIntoCup();

        // 4、放入调味品
        condimentIntoCup();

    }

    /**
     * 烧水为通用逻辑,没必要在子类中实现
     */
    private void boilWater() {
        System.out.println("烧水中...");
    }

    /**
     * 放主材料的抽象方法,具体处理逻辑需要延迟到子类中实现
     */
    protected abstract void drinksIntoCup();

    /**
     * 倒水为通用逻辑,没必要在子类中实现
     */
    private void waterIntoCup() {
        System.out.println("将烧好的水倒入杯中");
    }

    /**
     * 放调味品的抽象方法,具体处理逻辑需要延迟到子类中实现
     */
    protected abstract void condimentIntoCup();

}

那么,制作茶和咖啡的子实现类就可以这样来实现:

/**
 * 泡茶
 */
public class Tea extends Drinks{

    /**
     * 实现放主材料的处理逻辑
     */
    @Override
    protected void drinksIntoCup() {
        System.out.println("将适量茶叶放入杯中");
    }

    /**
     * 实现加调味品的处理逻辑
     */
    @Override
    protected void condimentIntoCup() {
        System.out.println("放入适量柠檬片");
    }

}
/**
 * 冲咖啡
 */
public class Coffee extends Drinks{

    /**
     * 实现放主材料的处理逻辑
     */
    @Override
    protected void drinksIntoCup() {
        System.out.println("将咖啡粉倒入杯中");
    }

    /**
     * 实现加调味品的处理逻辑
     */
    @Override
    protected void condimentIntoCup() {
        System.out.println("放入咖啡伴侣");
    }

}

最后,我们来模拟一下,制作茶和制作咖啡的调用方式。

public static void main(String[] args) {
	Drinks tea = new Tea();
	tea.makeDrinks();
	
	System.out.println("==================");

	Drinks coffee = new Coffee();
	coffee.makeDrinks();
}

输出如下:

烧水中…
将适量茶叶放入杯中
将烧好的水倒入杯中
放入适量柠檬片
==================
烧水中…
将咖啡粉倒入杯中
将烧好的水倒入杯中
放入咖啡伴侣

四、钩子方法

我们在抽象模板类中,也可以定义一些空实现或者有默认实现的方法,子类中可以选择重写也可以不重写这些方法。这样的方法,我们称为钩子方法。钩子方法为你在实现某一个抽象类的时候提供了可选项,相当于预先提供了一个默认配置。钩子方法的引入使得子类可以控制父类的行为。

举个例子,制作咖啡时,当我们倒入咖啡伴侣后,一般需要搅拌一下,而制作茶的过程中就不需要这个环节。

我们在抽象模板类中加入搅拌的方法。

/**
 * 制作饮品(抽象模板类)
 */
public abstract class Drinks {

    /**
     * 模板方法:制作饮品
     * 定义成final,防止子类重写
     */
    public final void makeDrinks(){

        // 1、将水烧开
        boilWater();

        // 2、将主材料放入杯中
        drinksIntoCup();

        // 3、将烧好的水倒入杯中
        waterIntoCup();

        // 4、放入调味品
        condimentIntoCup();

        // 5、搅拌均匀
        stir();

    }

    /**
     * 烧水为通用逻辑,没必要在子类中实现
     */
    private void boilWater() {
        System.out.println("烧水中...");
    }

    /**
     * 放主材料的抽象方法,具体处理逻辑需要延迟到子类中实现
     */
    protected abstract void drinksIntoCup();

    /**
     * 倒水为通用逻辑,没必要在子类中实现
     */
    private void waterIntoCup() {
        System.out.println("将烧好的水倒入杯中");
    }

    /**
     * 放调味品的抽象方法,具体处理逻辑需要延迟到子类中实现
     */
    protected abstract void condimentIntoCup();

    /**
     * 搅拌均匀。
     * 并不是所有饮品都需要搅拌,所以这里给一个空实现。
     * 这个是个钩子方法,意思是,子类中谁想用这个方法,谁就重写一下
     */
    protected void stir(){}

}

咖啡需要搅拌,所以我们在咖啡类中,重写实现一下这个搅拌方法。

/**
 * 冲咖啡
 */
public class Coffee extends Drinks{

    /**
     * 实现放主材料的处理逻辑
     */
    @Override
    protected void drinksIntoCup() {
        System.out.println("将咖啡粉倒入杯中");
    }

    /**
     * 实现加调味品的处理逻辑
     */
    @Override
    protected void condimentIntoCup() {
        System.out.println("放入咖啡伴侣");
    }

    /**
     * 重写搅拌方法
     */
    @Override
    protected void stir() {
        System.out.println("将咖啡和咖啡伴侣搅拌均匀");
    }
}

重新执行我们的测试方法后,得到下边的输出:

烧水中…
将适量茶叶放入杯中
将烧好的水倒入杯中
放入适量柠檬片
==================
烧水中…
将咖啡粉倒入杯中
将烧好的水倒入杯中
放入咖啡伴侣
将咖啡和咖啡伴侣搅拌均匀

五、模板方法模式的优缺点

优点:

  • 实现代码复用。

缺点:

  • 算法骨架不容易升级。 模板类跟子类非常耦合,如果要对模板方法进行变更,可能就会要求所有子类进行相应的变化。所以,在抽取算法骨架的时候需要特别小心
    ,尽量确保抽取的部分是不会变化的。

六、模板方法模式的应用场景及案例

  • 各个子类中有公共行为,应该抽取出来,从而避免代码重复。
  • 需要固定算法骨架,实现一个算法的不可变部分,并把可变部分留给子类来实现。
  • AQS(AbstractQueuedSynchronizer) 就是模板方法模式的一个典型应用案例。

热门文章

暂无图片
编程学习 ·

浅析原型模式中的clone()

更多精彩文章请访问我的个人博客(zhuoerhuobi.cn)最近学习到设计模式中的原型模式,在学习过程中,产生了对clone()实现的原理和效率的兴趣。原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的…
暂无图片
编程学习 ·

开挂的印度裔00后:7岁“出道”教编程,12岁成为IBM荣誉顾问

9年教学经验,400万播放量,还出了3本编程教学书。解锁如此成就的,正是在油管(Youtube)爆火的一位up主,印度裔加拿大籍程序员——Tanmay Bakshi。他的课程覆盖主流操作系统、编程语言,以及基础科学。其深入浅出、通俗易懂的讲解方式,大获网友们的好评。有中国网友,还将他与…
暂无图片
编程学习 ·

线程开多少合适?

简单来说 CPU 密集型: 最佳线程数 = CPU 核数(逻辑)+ 1 注:计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。 I/O密集型: 最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU…
暂无图片
编程学习 ·

为什么 SELECT * 效率低?

面试官:“小程,说一下你常用的SQL优化方式吧。” 程序猿小一:“那很多啊,比如不要用SELECT *,查询效率低。巴拉巴拉…” 面试官:“为什么不要用SELECT * ?它在哪些情况下效率低呢?” 程序猿小一:“SELECT * 它好像比写指定列名多一次全表查询吧,还多查了一些无用的字…
暂无图片
编程学习 ·

Ubuntu 18.04安装docker-compose

安装docker-compose之前先安装docker环境可以参照我的文章https://blog.csdn.net/weixin_42608885/article/details/106859553#安装依赖工具 $ sudo apt-get install python-pip -y #安装编排工具 $ sudo pip install docker-compose #查看版本 $ sudo docker-compose version
暂无图片
编程学习 ·

电商新手做亚马逊要怎样开始?

"说到互联网创业,很多人的第一个想到的是淘宝,但是很多人并不清楚,经过十几年的发展淘宝已经很难再进入了,利润也是下降到了最低,很多的卖家都在寻找机会做转型,而你一个毫无经验的小白现在进入,基本可以说很难生存,近年来,我国的跨境电子商务进入迅猛的发展阶段,…
暂无图片
编程学习 ·

MYSQL字符类型字段判断是否包含某个字符N种方法

方法一:使用LIKE关键字SELECT * FROM 表名 WHERE 字段名 LIKE "%字符%";方法二:使用INSTR函数SELECT * FROM 表名 WHERE INSTR(字段,字符);方法三:使用FIND_IN_SET函数SELECT * FROM 表名 WHERE FIND_IN_SET(字符, 字段名);方法四:使用LOCATE函数SELECT * FROM 表…
暂无图片
编程学习 ·

Tomcat 启动控制台乱码

Tomca 启动控制台乱码将tomcat用作web应用服务器,在tomcat的服务迭代中,服务漏洞是不可避免需要升级的来修复漏洞的。在修复漏洞的时候通常需要将原来的webapps下的文件复制到新tomcat中,并替换tomcat/conf/server.xml文件(小版本升级都可以用这种方式)。解决乱码 在tomca…
暂无图片
编程学习 ·

mybati中动态标签「if」没有生效的原因

一、问题: <if test="carrier != null and carrier != and carrier !=0">AND CARRIER = #{carrier} </if>我们在接口设置传入的字段类型为String,要在carrier字段不为null,空字符串,和”0“的时候增加以上条件,但是以上当carrier等于"0"时…
暂无图片
编程学习 ·

kuangbin专题8 生成树 次小生成树部分 HDU4081/UVA10600/UVA10462

前言 本来壮志凌云的想都做完 发现我在做梦。。。 朱刘算法太难了(自己太懒发现性价比比较低之后就没做而且算法介绍也太难懂了好几个关键词含义都不给简直简直太难了我枯 HDU4081 Qin Shi Huang’s National Road System 题意:给你一个图的各个点的坐标 再给你每个点的权值…
暂无图片
编程学习 ·

一起Talk Android吧(第二百五十五回:Android中的Toolbar标题一)

各位看官们大家好,上一回中咱们说的是Android中Toolbar的例子,这一回咱们继续说该例子。闲话休提,言归正转。让我们一起Talk Android吧! 看官们,我们在前面章回中介绍完了Toolbar的导航,本章回中将介绍Toolbar的标题。标题位于导航右侧,用来提示程序的内容或者当前页面的…
暂无图片
编程学习 ·

Java 为什么Set元素是不重复的?是如何去重复的?

为什么Set元素是不重复的?是如何去重复的?对于有值特性的,Set集合可以直接判断进行去重复。例如数字1,2,本身就有值特性。 对于引用数据类型的类对象: set集合会让两两对象,先调用自己的hashCode()方法得到彼此的哈希值(所谓的内存地址) 然后比较两个对象的哈希值是否相…
暂无图片
编程学习 ·

二进制与十进制转换工具类

package util;/*** 二进制工具类* * @author 谢辉* @time 2020.07.01**/ public class BinaryUtil {/*** 十进制数字转二进制* * @param num 十进制数字* @param strResult 结果容器,追加结果用,* @return 返回结果字符串*/public static String DecimalToBinary(Integ…
暂无图片
编程学习 ·

GIS开发:如何开发一个MBTiles Server

MBTiles是一个存储地图切片的数据库,以SQLite数据为基础,将地图切片按照缩放级别、横行和纵行的顺序,存储在其中。 常见的Geoserver可以加载插件,对MBTiles进行发布,github上也有开源的MBTiles Server,也可以进行MBTiles发布。 在只需要地图的切片情况下,如何进行一个MB…
暂无图片
编程学习 ·

排序算法总结

排序算法总结选择排序插入排序希尔排序归并排序自顶向下自底向上快速排序堆排序选择排序 算法流程:首先找到数组中最小的元素 将这个最小的元素与数组首位元素交换位置 在剩下的元素中找到最小的元素,并与数组第二个元素交换位置 如此往复直到整个数组排序算法分析:时间复杂度…
暂无图片
编程学习 ·

对于IIC发送数据的理解

主机发送数据到从机 👀1.数据是从低位开始到高位传输的。 👀2.接受数据是从高位到低位的。 //IIC发送一个字节 void IIC_Send_Byte(u8 txd) { u8 t; SDA_OUT(); IIC_SCL=0;for(t=0;t<8;t++){ IIC_SDA=(txd&0x80)>>…