【深入理解设计模式】模板方法模式

模板方法模式

在这里插入图片描述
模板方法模式是一种行为设计模式,它定义了一个操作中的算法骨架,将某些步骤延迟到子类中实现。模板方法模式使得子类可以不改变算法结构的情况下,重新定义算法的某些特定步骤。

概述

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。

例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

定义:

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

结构

模板方法模式由两部分组成:模板方法和基本方法。模板方法是算法的骨架,它定义了算法的结构,包括算法的步骤和执行顺序。基本方法是算法的具体步骤,它由子类实现,实现具体的算法逻辑。

模板方法(Template Method)模式包含以下主要角色:

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

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

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

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

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

      • 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

        一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。

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

模板方法模式的实现方式包括:

  1. 定义一个抽象类,其中包含模板方法。
  2. 在抽象类中定义基本方法,并使用抽象方法标记它们。
  3. 在子类中实现基本方法,以完成具体的算法逻辑。
  4. 在模板方法中调用基本方法,以完成算法的结构。

下面是一个简单的模板方法模式的例子:

abstract class AbstractClass {// 模板方法public void templateMethod() {// 步骤1System.out.println("Step 1");// 步骤2System.out.println("Step 2");// 调用基本方法concreteMethod();// 步骤3System.out.println("Step 3");}// 基本方法public abstract void concreteMethod();
}class ConcreteClass extends AbstractClass {// 实现基本方法public void concreteMethod() {System.out.println("Concrete Method");}
}public class TemplateMethodDemo {public static void main(String[] args) {AbstractClass obj = new ConcreteClass();obj.templateMethod();}
}

在这个例子中,AbstractClass 是一个抽象类,其中包含一个模板方法 templateMethod 和一个抽象方法 concreteMethodConcreteClassAbstractClass 的子类,它实现了 concreteMethod 方法,以完成具体的算法逻辑。在 templateMethod 中,我们调用了 concreteMethod 方法,以完成算法的结构。

案例实现:

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

/*** @author OldGj 2024/03/04* @version v1.0* @apiNote 抽象父类*/
public abstract class AbstractClass {// 模板方法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("炒啊炒啊炒到熟啊");}}

在抽象父类中定义模板方法cookProcess为炒菜的过程,其中倒蔬菜和倒调味料对于炒不同的菜是不同的,因此定义为抽象方法,延迟到子类实现。

/*** @author OldGj 2024/03/04* @version v1.0* @apiNote 具体子类 - 炒包菜*/
public class ConcreteClass_BaoCai extends AbstractClass{@Overridepublic void pourVegetable() {System.out.println("炒的菜是包菜");}@Overridepublic void pourSauce() {System.out.println("调味品是:辣椒");}
}
/*** @author OldGj 2024/03/04* @version v1.0* @apiNote 具体子类 - 炒菜心*/
public class ConcreteClass_CaiXin extends AbstractClass{@Overridepublic void pourVegetable() {System.out.println("炒的菜品是菜心");}@Overridepublic void pourSauce() {System.out.println("调味料是:蒜蓉");}
}

定义两个具体子类,分别是炒包菜和炒菜心,他们可以公用一个模板方法,因此都继承了AbstractClass,只需要重写其中的抽象方法即可,其他方法的算法逻辑无论是炒包菜还是炒菜心都是一个逻辑,因此不需要改变。

注意:为防止恶意操作,一般模板方法都加上 final 关键词。

/*** @author OldGj 2024/03/04* @version v1.0* @apiNote 客户端类 - 测试*/
public class Client {public static void main(String[] args) {AbstractClass baoCai = new ConcreteClass_BaoCai();baoCai.cookProcess();System.out.println("-------------------");AbstractClass caiXin = new ConcreteClass_CaiXin();caiXin.cookProcess();}
}

输出结果:
在这里插入图片描述

优缺点

优点:

  • 提高代码复用性

    将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。

  • 实现了反向控制

    通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。

缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

适用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

JDK源码解析

在 JDK 中,InputStream 中的 read() 方法使用了模板方法模式。

InputStream 是一个抽象类,它有一个模板方法 read()。这个方法定义了读取数据的算法骨架,但是将具体的读取操作延迟到子类中实现。

InputStream 中的 read() 方法是一个模板方法,它定义了读取数据的算法骨架,包括读取数据的步骤和执行顺序。具体步骤如下:

  1. 如果已经到达文件末尾,则返回 -1。
  2. 否则,读取下一个字节,并返回该字节的 int 值。

InputStream 的子类(如 FileInputStreamByteArrayInputStream 等)实现了 read() 方法的具体步骤。例如,FileInputStream 通过文件系统读取文件中的数据;ByteArrayInputStream 从字节数组中读取数据。

这种设计模式使得 InputStream 能够定义一个通用的读取数据的方法,而子类可以根据自己的需求实现具体的读取操作。

在InputStream类中定义了多个 read() 方法,如下:

public abstract class InputStream implements Closeable {//抽象方法,要求子类必须重写public abstract int read() throws IOException;public int read(byte b[]) throws IOException {return read(b, 0, b.length);}public int read(byte b[], int off, int len) throws IOException {if (b == null) {throw new NullPointerException();} else if (off < 0 || len < 0 || len > b.length - off) {throw new IndexOutOfBoundsException();} else if (len == 0) {return 0;}int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据if (c == -1) {return -1;}b[off] = (byte)c;int i = 1;try {for (; i < len ; i++) {c = read();if (c == -1) {break;}b[off + i] = (byte)c;}} catch (IOException ee) {}return i;}
}

从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[]) 方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。

在该方法中第18行、27行,可以看到调用了无参的抽象的 read() 方法。

总结如下: 在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/528097.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第九个实验:一维数组和二维字符串数组的输入而输出

实验内容: 新建一维数组 新建二维字符串数组 输入内容,运行结果,在输出界面中显示输入的内容 第一步:新建项目 第二步:编程 添加一个INT数控件和字符串控件 修改控件: 复制前面板控件

以题为例 浅谈sql注入二次注入

什么是二次注入 二次注入可以理解为&#xff0c;攻击者构造的恶意数据存储在数据库后&#xff0c;恶意数据被读取并进入到SQL查询语句所导致的注入。防御者即使对用户输入的恶意数据进行转义&#xff0c;当数据插入到数据库中时被处理的数据又被还原&#xff0c;Web程序调用存…

Vue3中Vue Router的使用区别

在 Vue 3 中&#xff0c;useRouter 和 useRoute 是两个用于 Vue Router 的 Composition API 函数&#xff0c;它们的用途和返回的对象不同&#xff0c;接下来详细了解一下它们的区别以及如何正确使用它们。 useRouter useRouter 用于获取 router 实例&#xff0c;这个实例提供…

神经网络实战前言

应用广泛 从人脸识别到网约车&#xff0c;在生活中无处不在 未来可期 无人驾驶技术便利出行医疗健康改善民生 产业革命 第四次工业革命——人工智能 机器学习概念 机器学习不等价与人工智能20世纪50年代&#xff0c;人工智能是说机器模仿人类行为的能力 符号人工智能 …

LeetCode_Java_排序系列(1)(题目+思路+代码)

目录 349.两个数组的交集 350. 两个数组的交集 II 349.两个数组的交集 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2…

Leetcode刷题【每日n题】(5)

题目一 思路分析 二分查找法&#xff1a; 要查找目标数的算术平方根k&#xff0c;则K*k <x,则可以利用二分法查找0-x之间的数&#xff0c;看是否与其匹配。 代码实现 class Solution {public int mySqrt(int x) {//使用二分查找int left0,rightx,ans-1;while(left<righ…

稀碎从零算法笔记Day14-LeetCode:同构字符串

题型&#xff1a;字符串、哈希表 链接&#xff1a;205. 同构字符串 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那…

面试经典150题【71-80】

文章目录 面试经典150题【71-80】112.路径总和129.求根节点到叶子节点的数字之和124.二叉树中的最大路径和&#xff08;要思考&#xff09;173.二叉树迭代搜索器222.完全二叉树节点的个数236.二叉树的最近公共祖先199.二叉树的右视图637.二叉树的层平均值102.二叉树的层序遍历1…

企业战略管理 找准定位 方向 使命 边界 要干什么事 要做多大的生意 资源配置投入

AI突破千行百业&#xff0c;也难打破护城河 作为每个企业或个人的立命生存之本&#xff0c;有的企业在某个领域长期努力筑起了高高的护城河。 战略是什么&#xff1f;用处&#xff0c;具体内容 企业战略是指企业为了实现长期目标&#xff0c;制定的总体规划和长远发展方向。…

Qdrant 向量数据库的部署以及如何在 .NET 中使用 TLS 安全访问

本文介绍如何使用 Docker 部署 Qdrant 向量数据库&#xff0c;以及其相关的安全配置&#xff0c;并演示如何使用 .NET 通过 TLS 安全访问 Qdrant 向量数据库。 文章目录 1. 背景2. Qdrant 向量数据库的部署2.1 Qdrant 向量数据库的安全配置2.2 使用 Docker 部署安全的 Qdrant 向…

[QT]自定义的QtabWidget

需求 最近有一个需求就是一个QTabWidget要求有四个tab页在左侧用于显示主页面&#xff0c;在右侧有一个关于按钮&#xff0c;点击后用于弹出窗口显示一些程序相关信息。主要是怎么实现右侧按钮 相关代码 #ifndef MYTABWIDGET_H #define MYTABWIDGET_H#include <QWidget&g…

【联邦学习综述:概念、技术】

出自——联邦学习综述&#xff1a;概念、技术、应用与挑战。梁天恺 1*&#xff0c;曾 碧 2&#xff0c;陈 光 1 从两个方面保护隐私数据 硬件层面 可 信 执 行 环 境 &#xff08;Trusted Execution Environment&#xff0c;TEE&#xff09;边 缘 计 算&#xff08;Edge Com…