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

news2025/9/18 9:46:37

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

——黑格尔

文章目录

  • 定义
  • 图纸
  • 一个例子:从文件中获取信息分几步?
    • 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> {

    @Override
    public 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> {

    @Override
    public 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> {

    @Override
    public 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> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //word 文件的读取方式
    }
}

/**
 * excel 文件的信息读取器
 */
public class ExcelReader<E> extends Reader<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //excel 文件的读取方式
    }
}

/**
 * xml 文件的信息读取器
 */
public class XMLReader<E> extends Reader<E> {

    @Override
    public 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> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //word 文件的读取方式
    }
}

/**
 * excel 文件的信息读取器
 */
public class ExcelHandler<E> implements Handler<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //excel 文件的读取方式
    }
}

/**
 * xml 文件的信息读取器
 */
public class XMLHandler<E> implements Handler<E> {

    @Override
    public List<E> resolution(FileInputStream fis) throws IOException {
        //xml 文件的读取方式
    }
}

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

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


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



模板方法和生成器

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


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

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



写在后面

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

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

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





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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Hive概述与基本操作

一、Hive基本概念 1.什么是hive? &#xff08;1&#xff09;hive是数据仓库建模的工具之一 &#xff08;2&#xff09;可以向hive传入一条交互式的sql,在海量数据中查询分析得到结果的平台 2.Hive简介 Hive本质是将SQL转换为MapReduce的任务进行运算&#xff0c;底层由HDFS…

经典文献阅读之--A Survey on Generative Diffusion Models(扩散模型最新综述)

0. 简介 本文综述了深度生成模型&#xff0c;特别是扩散模型(Diffusion model)&#xff0c;如何赋予机器类似人类的想象力。扩散模型在生成逼真样本方面显示出巨大潜力&#xff0c;克服了变分自编码器中的后分布对齐障碍&#xff0c;缓解了生成对抗网络中的对抗性目标不稳定性…

深度挖掘响应式模式的潜力,从而精准优化AI与机器学习项目的运行效能,引领技术革新潮流

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 &#x1f525; 转载自热榜文章&#xff1a;探索设计模式的魅力&#xff1a;深度挖掘响应式模式的…

uni-starter的微信登录拿不到登录者的昵称,头像,手机号问题记录

uni-starter的微信登录竟然拿不到登录者的昵称&#xff0c;头像&#xff0c;手机号 获取手机号的方法在另外一篇文章中&#xff0c;需要认证&#xff0c;需要有营业执照 uni.login({"provider": type,"onlyAuthorize": true,// #ifdef APP"univerif…

RabbitMQ - Spring boot 整合 RabbitMQ

一、RabbitMQ 1、RabbitMQ 使用场景 1.1、服务解耦 假设有这样一个场景, 服务A产生数据, 而服务B,C,D需要这些数据, 那么我们可以在A服务中直接调用B,C,D服务,把数据传递到下游服务即可 但是,随着我们的应用规模不断扩大,会有更多的服务需要A的数据,如果有几十甚至几百个下…

Docker Desktop修改镜像存储路径 Docker Desktop Start ... 卡死

1、CMD执行wsl -l -v --all 2、Clean / Purge data 3、导出wsl子系统镜像: wsl --export docker-desktop D:\docker\wsl\distro\docker-desktop.tar wsl --export docker-desktop-data D:\docker\wsl\data\docker-desktop-data.tar4、删除现有的wsl子系统&#xff1a; wsl -…

智游剪辑网页版发布了!

你是否因为软件安装麻烦而不愿意尝试本软件&#xff1f;那么可以试试网页版&#xff01;只需要一个浏览器&#xff0c;就可以直接访问了&#xff0c;网页版免安装&#xff0c;无广告&#xff0c;大部分功能都可以免费使用&#xff01; 网页版地址&#xff1a;app.zyjj.cc 界面…

汇编基础-----通过x64dbg了解什么是堆栈

汇编基础-----通过x64dbg了解什么是堆栈 什么是堆栈 在汇编语言中&#xff0c;堆栈&#xff08;stack&#xff09;是一种用于存储临时数据和执行函数调用的内存结构。堆栈是一种后进先出&#xff08;Last-In-First-Out, LIFO&#xff09;的数据结构&#xff0c;通常用于保存函…

ssm042在线云音乐系统的设计与实现+jsp

在线云音乐系统的设计与实现 摘 要 随着移动互联网时代的发展&#xff0c;网络的使用越来越普及&#xff0c;用户在获取和存储信息方面也会有激动人心的时刻。音乐也将慢慢融入人们的生活中。影响和改变我们的生活。随着当今各种流行音乐的流行&#xff0c;人们在日常生活中经…

08_ADC轮询方式读取电压值/DMA方式多通道采集/DAC数模转换

ADC轮询方式读取电压值/DMA方式多通道采集/DAC数模转换 ADC轮询方式读取电压值DMA方式多通道采集DAC数模转换 ADC轮询方式读取电压值 while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */HAL_ADC_Start(&hadc1);//启动ADC装换HAL_ADC_PollForConversion(&hadc…

多维时序 | Matlab实现TCN-LSTM时间卷积长短期记忆神经网络多变量时间序列预测

多维时序 | Matlab实现TCN-LSTM时间卷积长短期记忆神经网络多变量时间序列预测 目录 多维时序 | Matlab实现TCN-LSTM时间卷积长短期记忆神经网络多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.【Matlab实现TCN-LSTM时间卷积长短期记忆神经网络多变量…

【C++ 继承】关于多继承、菱形继承你了解多少?

文章目录 1. 为什么 C 有多继承&#xff0c;Java 没有多继承呢&#xff1f;2. 什么是菱形继承呢&#xff1f;3. 菱形继承产生数据冗余和二义性问题 1. 为什么 C 有多继承&#xff0c;Java 没有多继承呢&#xff1f; C 实现多继承是为了满足某些场景的需要&#xff0c;但是有多继…

leetcode:739.每日温度/496.下一个更大元素

单调栈的应用&#xff1a; 求解当前元素右边比该元素大的第一个元素&#xff08;左右、大小都可以&#xff09;。 单调栈的构成&#xff1a; 单调栈里存储数组的下标&#xff1b; 单调栈里的元素递增&#xff0c;求解当前元素右边比该元素大的第一个元素&#xff1b;元素递…

【附gpt4.0升级秘笈】百度智能云万源全新一代智能计算操作系统发布:引领AI新纪元

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;作为引领未来发展的关键技术&#xff0c;正逐步渗透到社会的每一个角落。百度&#xff0c;作为中国AI领域的领军企业&#xff0c;始终站在技术创新的前沿&#xff0c;不断推出引领行业的重磅产品。今日&…

【小贴士|Unity】华佗热更版本控制配置

现在越来越多的新项目选择使用HybridCLR&#xff0c;而不是以前的Lua。也不妨有的项目会配置打包机器人以及版本控制&#xff0c;但是这个版本控制的配置还真需要注意一些。&#xff08;因为我就踩坑了&#xff09; 如图所示&#xff0c;当你第一次执行HybridCLR/Generate/All后…

渲染农场怎么收费?渲染100邀请码1a12

随着互联网的发展&#xff0c;越来越多的影视、动画和广告项目涌现出来&#xff0c;为了加快渲染&#xff0c;很多视觉工作者都会使用渲染农场&#xff0c;现在市面上的农场那么多&#xff0c;它们有什么收费模式&#xff1f;性价比如何&#xff1f;这篇文章我们就来看下吧。 1…

leetcode199 二叉树的右视图

题目 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 示例 输入: [1,2,3,null,5,null,4] 输出: [1,3,4] 解析 这道题首先能想到的办法&#xff0c;就是使用迭代法层次遍历&…

《手把手教你》系列基础篇(九十)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-中篇(详解教程)

1.简介 上一篇宏哥介绍是如何使用logback将日志输出到控制台中&#xff0c;但是如果需要发给相关人需要你拷贝出来&#xff0c;有时候由于控制台窗口的限制&#xff0c;有部分日志将会无法查看&#xff0c;因此我们还是需要将日志输出到文件中&#xff0c;因此今天主要介绍和分…

视频号小店被严重低估,私域电商再次崛起,这次真的不一样!

大家好&#xff0c;我是电商笨笨熊 在视频号正式推出电商之后&#xff0c;我听到过很多贬低的话&#xff1b; 有人觉得腾讯没有做电商的基因&#xff0c; 有人觉得这不过是资本打一枪换一个地方罢了&#xff0c;最终都是普通人承担后果&#xff0c; 总之&#xff0c;这样的…

华为海思数字芯片设计笔试第五套

声明 下面的题目作答都是自己认为正确的答案&#xff0c;并非官方答案&#xff0c;如果有不同的意见&#xff0c;可以评论区交流。 这些题目也是笔者从各个地方收集的&#xff0c;感觉有些题目答案并不正确&#xff0c;所以在个别题目会给出自己的见解&#xff0c;欢迎大家讨论…