设计模式——2_9 模版方法(Template Method)

人们往往把任性也叫做自由,但是任性只是非理性的自由,人性的选择和自决都不是出于意志的理性,而是出于偶然的动机以及这种动机对感性外在世界的依赖

——黑格尔

文章目录

  • 定义
  • 图纸
  • 一个例子:从文件中获取信息分几步?
    • Reader
          • Reader
    • 读取一个文件分几步?
          • Reader
  • 碎碎念
    • 模板方法和好莱坞原则
      • 好莱坞原则
      • 依赖腐败
    • 模板方法和钩子
    • 模板方法和框架
    • 模板方法和策略
          • Handler
    • 模板方法和生成器
    • 写在后面

定义

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


其实这个系列的文章本身就是一个模板方法的体现

您可能发现了,在这个系列里每篇文章都是以 定义-图纸-例子-碎碎念 这样的格式来编写的,只不过每一篇的各个模块的里面的内容有所不同

这就是模板方法,模板方法定义骨架,由子类填充血肉,从而变成不同的个体




图纸

在这里插入图片描述




一个例子:从文件中获取信息分几步?

假定在你们公司的对外网站上有一个允许用户上传文件的接口,你会通过这个接口解析用户上传的文件,并把解析到的数据存到数据库中,用于共享供应商和你们自己的信息。但是由于经理坚信客户就是上帝,于是乎诡异的需求出现了,你需要从 word、excel还有xml文件中读取数据

准备好了吗?这次的例子开始了:



Reader

看到这样的题目你肯定会说,那很简单啊。肯定是要建一个 Reader 类簇,搞一个 WordReaderExcelReaderXMLReader。然后根据需要解析哪种文件去调用对应的 Reader 不就万事大吉了吗?


非常好,赶紧去吧 Reader 建出来,就像这样:

在这里插入图片描述

Reader
/*** 读取器*/
public interface Reader<E> {List<E> read(File file) throws IOException;
}/*** word 文件的信息读取器*/
public class WordReader<E> implements Reader<E> {@Overridepublic List<E> read(File file) throws IOException {if (file.exists()) {//文件必须存在 打开流try (FileInputStream is = new FileInputStream(file)) {System.out.println("使用 is 进行word信息读取");return data;//返回最终用户要的数据}}return null;}
}/*** excel 文件的信息读取器*/
public class ExcelReader<E> implements Reader<E> {@Overridepublic List<E> read(File file) throws IOException {if (file.exists()) {//文件必须存在 打开流try (FileInputStream is = new FileInputStream(file)) {System.out.println("使用 is 进行excel信息读取");return data;//返回最终用户要的数据}}return null;}
}/*** xml 文件的信息读取器*/
public class XMLReader<E> implements Reader<E> {@Overridepublic List<E> read(File file) throws IOException {if (file.exists()) {//文件必须存在 打开流try (FileInputStream is = new FileInputStream(file)) {System.out.println("使用 is 进行xml信息读取");return data;//返回最终用户要的数据}}return null;}
}

然后我们的问题才刚刚开始

我们发现这个 Reader 里面大量的代码都是重复的,我们判断要读取的文件是否存在,然后需要开启一个文件流,并且保证无论如何他都会被正确关闭,而这些操作 无论我将来读取任何类型的文件,他们都应该是不变的


在我们的实现里出现了重复,那他们一定可以像被提取公因式一样被提取出来简化



读取一个文件分几步?

问:把大象塞冰箱分几步?

答:三步,打开冰箱门、把大象塞进去、关上冰箱门


其实这个脑筋急转弯是个标准的偷换概念,如果你不了解他的套路,一定会纠结答案里的第二步。正确答案其实并没有解决问题,而是给正确答案加了层包装,事实上无论你塞任何东西进冰箱,都需要打开和关上冰箱门

沿着这个思路,我们再来看看上面那个问题

读取一个文件分几步?

其实也就三步,打开文件流,读出数据,关闭文件流


也就是说我们可以考虑拆分刚刚的实现,就像这样:

在这里插入图片描述

Reader
/*** 读取器*/
public abstract class Reader<E> {public List<E> read(File file) throws IOException {if (haveFile(file)) {FileInputStream fis = getFileInStream(file);try {return resolution(fis);} catch (IOException e) {e.printStackTrace();} finally {endWork(fis);}}return null;}protected boolean haveFile(File file) {return file.exists();}protected FileInputStream getFileInStream(File file) throws FileNotFoundException {return new FileInputStream(file);}protected abstract List<E> resolution(FileInputStream fis) throws IOException;protected void endWork(FileInputStream fis) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}
}/*** word 文件的信息读取器*/
public class WordReader<E> extends Reader<E> {@Overridepublic List<E> resolution(FileInputStream fis) throws IOException {//word 文件的读取方式}
}/*** excel 文件的信息读取器*/
public class ExcelReader<E> extends Reader<E> {@Overridepublic List<E> resolution(FileInputStream fis) throws IOException {//excel 文件的读取方式}
}/*** xml 文件的信息读取器*/
public class XMLReader<E> extends Reader<E> {@Overridepublic List<E> resolution(FileInputStream fis) throws IOException {//xml 文件的读取方式}
}

在这个实现中,我们将读取文件的流程分为 getFileInStream (打开文件流) -> resolution (读出文件信息)-> endWork(关闭文件流),最后把他们集成到 read 方法中并提供给外部代码调用


那你会说,就这?这不就是继承的标准用法吗?

整个模式确实是通过继承来实现的,但是他的核心是定义了骨架的 read 方法。在顶层的 read 方法中,他定义了 Reader 的工作流程,而且他调用了尚未被实现的方法 resolution,而这个方法恰恰是整个 Reader 中最核心的方法,他决定了这个 Reader 的具体工作内容

也就是说,这个实现完成了这样一个壮举,即:由父类(上层)决定调用方式,让子类(下层)决定具体实现

而这正是一个标准的模板方法实现




碎碎念

模板方法和好莱坞原则

好莱坞原则

别调用我们,我们会调用你(单向依赖)


据说模板方法的诞生是受到了好莱坞的运作模式的启发(Head First 设计模式 里写的,不管你信不信,反正我信了 ),书里是这样说的:

好莱坞原则可以给我们一种防止 “依赖腐败” 的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,边侧组件又依赖低层组件的时候,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。

在好莱坞原则下,我们允许低层组件将自己挂钩到系统上,但高层组件会决定什么时候和怎么使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”

——《Head First 设计模式(第一版)》中文版 中国电力出版社版本 P296

依赖腐败

在书上他提出了一个新概念:依赖腐败。这种腐败可不是我们平时说的 权力导致腐败,绝对的权力导致绝对的腐败。恰恰相反,依赖腐败 是上下层之间过于“亲密”导致的,上下层互相依赖,最终导致整个系统纠缠在一起,就像一团打结的毛线球一样

为了解决这种 依赖腐败 ,我们考虑让依赖尽可能变成单向的,更具体一点,让下层组件挂载到上层组件的结构中(在上层组件提前预留出位置的情况下)。上层组件只会知道在某个位置一定有某个下层组件存在,在某时某刻我可以调用他,至于他具体是如何实现的,上层组件从来是漠不关心的。这就是好莱坞原则

就像开车,我只需要知道车有行驶的功能,踩了油门他会走,踩住刹车他要停。至于他烧的是92还是95,用的电池还是油箱,跟我没关系的



模板方法和钩子

钩子,英文名叫 hook

还有个东西叫 钩子方法,比如上例中的 resolution

简单来说他就是指那种 下层可以提供实现,而且一定会在上层实现某种条件的情况下被调用 的抽象方法(空实现也算)


在JavaScript中钩子方法更是随处可见,只不过在那边叫回调函数,其实本质上两者是一样的

更进一步,使用回调函数的 JavaScript 函数其实自身也是一个模板方法的实现

当我使用带有回调函数的函数时,这个具体函数的执行骨架我是无法修改的,我只需要关注于我传进去的回调函数会在符合什么状态下被调用,以及应该执行什么



模板方法和框架

理论上来说,在创建框架的时候,模板方法总是你的好帮手,典型的比如Android里面的几大组件,servlet里面的请求处理流程,甚至是古老的 applet


并不是说这些框架都肯定用了模板方法,模板方法提供的是一种思路,即 上层制定规则,下层具体实现。对于框架来说,其他程序员就是他的客户,他必须保证在每个客户都拥有足够高自由度的情况下,整个框架可以按照预设的方式运作

上层决定出入口,整个框架的起承转合,因为这个执行方式是永远不会变的,这是框架的灵魂。至于具体要怎么起,怎么承,你来决定框架的血肉



模板方法和策略

对模板方法来说,由于上层指定规则,下层具体实现。但是这个“下层”可没有人规定一定是子类来实现的


譬如说在 Reader 的实现中,我完全可以把 resolution 的实现委托出去,创建一个对应的类簇,比如说 Handler 吧,让这个类簇可以专注于根据不同的文件类型读取不同的信息。这时候 ReaderHandler 之间,就会形成一个类似策略模式的实现,Handler 是作为可插拔的 Reader 部分算法来实现的,就像这样:

在这里插入图片描述

Handler
/*** 读取器*/
public class Reader<E> {public List<E> read(File file,Handler<E> handler) throws IOException {if (haveFile(file)) {FileInputStream fis = getFileInStream(file);try {return handler.resolution(fis);} catch (IOException e) {e.printStackTrace();} finally {endWork(fis);}}return null;}protected boolean haveFile(File file) {return file.exists();}protected FileInputStream getFileInStream(File file) throws FileNotFoundException {return new FileInputStream(file);}protected void endWork(FileInputStream fis) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}
}public interface Handler<E> {List<E> resolution(FileInputStream fis) throws IOException;
}/*** word 文件的信息读取器*/
public class WordHandler<E> implements Handler<E> {@Overridepublic List<E> resolution(FileInputStream fis) throws IOException {//word 文件的读取方式}
}/*** excel 文件的信息读取器*/
public class ExcelHandler<E> implements Handler<E> {@Overridepublic List<E> resolution(FileInputStream fis) throws IOException {//excel 文件的读取方式}
}/*** xml 文件的信息读取器*/
public class XMLHandler<E> implements Handler<E> {@Overridepublic List<E> resolution(FileInputStream fis) throws IOException {//xml 文件的读取方式}
}

那你会说,不对啊,这种写法可不就是把变化的部分提取出来的策略模式吗?模板方式的优良传统呢?通过继承修改原有部分实现呢?想了这么久想出来一个违背祖宗的决定是吧?

首先我承认,这就是策略的实现,但不妨碍他也用了模板方法呀。我们把一定会发生变化的部分独立出来,形成策略簇。但是我们原有的 Reader 依然保留拓展的能力呀,假设以后我要包装我的流,或者在读取前或读取后做一些操作,完全可以通过创建 Reader 子类的方式来实现


这种写法在实战中很常见,比如说在 迭代器 一章中就有一个标准的例子,我们讲的外部迭代,其实就是通过这种方式来实现的



模板方法和生成器

你发现了吗?模板方法模式和生成器模式他们的思路是一样的,只是最终的目的不同而已。


模板方法 关注行为,他讲究某个行为必须执行的步骤和顺序,把这些不变的内容固定好后,由子类去确定具体的算法,从而实现算法和执行流程之间的解耦

生成器 就像流水线,流水线上的各个工位要做什么事情在最开始就设计好了,你只需要提供物料,不同的工位就会根据自己的职能对物料进行组装或加工,这是对一个对象不同创建流程的抽象。本质上讲这也是一种对算法的抽象——对一个对象的创建流程的抽象



写在后面

总是有人在纠结,自由到底有没有边界。窃以为,自由一定是有限度的

自由不是为所欲为,而是在一定限制内随心所欲。真正自由是需要对自己所做的事情负责的,只有一个人在有担当所做的事情造成的后果的能力后,才有权利去讲自己的自由

这就像模板方法委托给子类进行实现的那个钩子一样,我给你自由,但是你需要在我制定的框架下。就像做饭,模板方法不管你是做佛跳墙还是蛋炒饭,这是你的自由,但是做完都得把火关上,否则家里会着火,那是你承担不起的后果





万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

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

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

相关文章

Adobe AE(After Effects)2017下载地址及安装教程

Adobe After Effects是一款专业级别的视觉效果和动态图形处理软件&#xff0c;由Adobe Systems开发。它被广泛用于电影、电视节目、广告和其他多媒体项目的制作。 After Effects提供了强大的合成和特效功能&#xff0c;可以让用户创建出令人惊艳的动态图形和视觉效果。用户可以…

MapReduce 机理

1.hadoop 平台进程 Namenode进程: 管理者文件系统的Namespace。它维护着文件系统树(filesystem tree)以及文件树中所有的文件和文件夹的元数据(metadata)。管理这些信息的文件有两个&#xff0c;分别是Namespace 镜像文件(Namespace image)和操作日志文件(edit log)&#xff…

vscode编译c++报错解决方案

1&#xff0c;xxxx cl.exe 一大串什么非程序员的(应该是这些&#xff09;,就是看一些谁的&#xff0c;调用了Visual Studio的编译软件去运行。建议&#xff0c;不要这样搞。 解决方案1&#xff1a;每次用就看这个文章&#xff08;个人觉得很麻烦&#xff09;&#xff1a;仅当…

java正则表达式教程

什么是正则表达式&#xff1a; 正则表达式是一种用来描述字符串模式的语法。在 Java 中&#xff0c;正则表达式通常是一个字符串&#xff0c;它由普通字符&#xff08;例如字母、数字、标点符号等&#xff09;和特殊字符&#xff08;称为元字符&#xff09;组成。这些特殊字符可…

NLP基础—jieba分词

jieba分词 支持四种分词模式 精确模式 试图将句子最精确地切开,适合文本分析;全模式 把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;搜索引擎模式 在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。paddle模式 利用Paddle…

WARNING: No swap limit support——查看docker状态时提示警告

环境&#xff1a;Ubuntu 20.04 1、警告详情 执行命令 service docker status如下图 2、解决办法 2.1 修改文件 执行命令 vim /etc/default/grub在GRUB_CMDLINE_LINUX中追加cgroup_enablememory swapaccount1&#xff0c;如下&#xff1a; # If you change this file…

elmentui树形表格使用Sortable拖拽展开行时拖拽bug

1、使用elemntui的el-table使用Sortable进行拖拽&#xff0c;如下 const el this.$el.querySelector(.el-table__body-wrapper tbody) Sortable.create(el, {onEnd: (event) > {const { oldIndex, newIndex } event//拿到更新前后的下标即可完成数据的更新} })2、但是我这…

分析ARP解析过程

1、实验环境 主机A和主机B连接到交换机&#xff0c;并与一台路由器互连&#xff0c;如图7.17所示&#xff0c;路由器充当网关。 图7.17 实验案例一示意图 2、需求描述 查看 ARP 相关信息,熟悉在PC 和 Cisco 设备上的常用命令,设置主机A和主机B为同一个网段网关设置为路由接…

2024-14.python前端+Django

第四篇 web前端 第1章 、Web的基本概念 前端基础总共分为三部分&#xff1a;html、css和js。 1.3、HTTP协议 1.3.1 、http协议简介 HTTP协议是Hyper Text Transfer Protocol&#xff08;超文本传输协议&#xff09;的缩写,是用于万维网&#xff08;WWW:World Wide Web &am…

故障转移-redis

4.4.故障转移 集群初识状态是这样的&#xff1a; 其中7001、7002、7003都是master&#xff0c;我们计划让7002宕机。 4.4.1.自动故障转移 当集群中有一个master宕机会发生什么呢&#xff1f; 直接停止一个redis实例&#xff0c;例如7002&#xff1a; redis-cli -p 7002 sh…

LVM和磁盘配额

目录 1、LVM &#xff08;1&#xff09;LVM机制 &#xff08;2&#xff09;LVM的管理命令 &#xff08;3&#xff09;创建并使用LVM &#xff08;4&#xff09;扩容 2、磁盘配额 &#xff08;1&#xff09;什么叫磁盘配额 &#xff08;2&#xff09;磁盘配额的条件和特点…

OpenHarmony、HarmonyOS和Harmony NEXT 《我们不一样》

1. OpenHarmony 定义与地位&#xff1a;OpenHarmony是鸿蒙系统的底层内核系统&#xff0c;集成了Linux内核和LiteOS&#xff0c;为各种设备提供统一的操作系统解决方案。 开源与商用&#xff1a;OpenHarmony是一个开源项目&#xff0c;允许开发者自由访问和使用其源代码&#…