设计模式——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> {

    @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 子类的方式来实现


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



模板方法和生成器

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


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

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



写在后面

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

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

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





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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Spingboot+vue协同过滤音乐推荐管理系统

项目演示视频效果&#xff1a; 基于Spingbootvue协同过滤音乐推荐管理系统 基于Spingbootvue协同过滤音乐推荐管理系统 1、项目介绍 基于Springboot的音乐播放管理系统总共两个角色&#xff0c;用户和管理员。用户使用前端前台界面&#xff0c;管理员使用前端后台界面。 有推荐…

Golang内存、指针逃逸、垃圾回收机制概览

最近看到了一篇文章是关于go的内存、指针逃逸和垃圾回收机制的&#xff0c;发现自己并未很细致的了解过这方面的内容&#xff0c;于是在翻阅各种文章的情况下&#xff0c;写出了这篇总结&#xff0c;参考文章放在文末&#xff0c;可自取 内存 Go 语言使用一个自带的垃圾收集器…

【S32K3 入门系列】- ADC 模块简介(上)

一、 前言 对于 S32K3 系列的初学者来说&#xff0c;S32K3 系列的参考手册阅读难度是让人望而却步的&#xff0c;本系列将对 S32K3 系列的外设进行逐一介绍&#xff0c;对参考手册一些要点进行解析。本文旨在介绍 S32K3 系列的 ADC 模块&#xff0c; ADC&#xff08;Analog to…

node端导出excel-用请求排队来限流

需求 有一个会执行luckySheet脚本并且导出excel的node接口&#xff0c;会在每天凌晨执行&#xff0c;但是文件过大时会内存溢出 之前有用worker来实现多线程&#xff08;主要是避免变量污染&#xff09;&#xff0c;但这样只能保证主线程不卡死&#xff0c;几个子线程合起来占用…

MDC搭配ttl使用!!!

一、简介 MDC 介绍​ MDC&#xff08;Mapped Diagnostic Context&#xff0c;映射调试上下文&#xff09;是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map&#xff0c;可以往其中添加键值对。MDC 中包含的内容可以被…

使用yolov8 进行实例分割训练

1、基于windows 的ISAM标注 直接下载安装包&#xff0c;解压后即可使用 链接&#xff1a;https://pan.baidu.com/s/1u_6jk-7sj4CUK1DC0fDEXQ 提取码&#xff1a;c780 2、标注结果转yolo格式 通过ISAM标注后的json文件路径 原始json格式如下&#xff1a; ISAM.json 转 yolo.…

牛客2024 【牛客赛文X】春招冲刺 ONT34 加油站【中等 贪心 C++、Java、Go、PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/a013a0691a0343aeb262ca1450d2fe4e 思路 贪心&#xff1a; 如果总的gas小于走完全程的cost&#xff0c;直接返回-1不需要再找了 如果确保了可以走完一圈之后&#xff0c;那么从index 0开始找&#xff0c; 当g…

【cygwin】工具安装apt-cyg

目录 下载安装查看是否安装成功安装软件 下载 git clone https://github.com/transcode-open/apt-cyg.git安装 cd apt-cyg mv apt-cyg /usr/local/bin/ 查看是否安装成功 apt-cyg --help安装软件 apt-cyg install nano

视频号小店怎么做?新手开店必备运营攻略,看这一篇就够了

大家好&#xff0c;我是电商笨笨熊 作为腾讯推出的电商项目&#xff0c;视频号小店在推出到现在一直都备受关注&#xff0c;同时也吸引了不少玩家入驻&#xff1b; 毕竟作为一个新平台、新市场&#xff0c;一个适合跑马圈地的红利平台&#xff0c;谁都想在这里分的一杯羹。 …

Linux debian gdb dump

1.开发背景 记录 debian 下应用程序崩溃调试方法 2.开发需求 程序越界可以定位到越界的位置附近 3.开发环境 debian 操作系统&#xff0c;如果不支持需要查看是否存在对应的可执行文件 4.实现步骤 4.1 设置 dump 输出大小 ulimit -c unlimited # 设置输出大小 生成core 文…

一个文生视频MoneyPrinterTurbo项目解析

最近抖音剪映发布了图文生成视频功能&#xff0c;同时百家号也有这个功能&#xff0c;这个可以看做是一个开源的实现&#xff0c;一起看看它的原理吧~ 一句话提示词 大模型生成文案 百家号生成视频效果 MoneyPrinterTurbo生成视频效果 天空为什么是蓝色的&#xff1f; 天空…

上位机图像处理和嵌入式模块部署(智能硬件的介绍)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 目前&#xff0c;用上位机软件虽然可以部署项目&#xff0c;但是它本身有自己的缺点&#xff0c;那就是稳定性差、价格贵。稳定性这部分&#xff0…

深度剖析扫雷游戏的各个知识点(2)

小伙伴们&#xff0c;大家好。这次继续上次的剖析扫雷游戏的知识点。 那么本次咱们主要是讲扫雷中的宏定义&#xff0c;也就是#define这些 首先#define是用来定义一个宏&#xff0c;后面就是类似于和变量一样的常量名&#xff0c;以及最后的数字就是它的值。 定义规则 #def…

数据结构——树和二叉树

目录 前言 一、树概念及结构 1.1树的概念 1.2 树的相关概念 ​编辑 1.3 树的表示 1.4 树的应用 2.二叉树概念及结构 2.1 二叉树概念 2.2 现实中的二叉树 2.3 特殊的二叉树 2.4 二叉树的性质 2.5 二叉树的存储结构 总结 前言 之前我们学习到的数据结构都是线性的…

Linux Makefile

1.开发背景 linux 下编译程序需要用到对应的 Makefile&#xff0c;用于编译应用程序。 2.开发需求 编写 Makefile 编译应用程序 1&#xff09;支持多个源文件 2&#xff09;支持多个头文件 3&#xff09;支持只编译修改的文件&#xff0c;包括源文件和头文件 4&#xff09;支持…

【Android Studio报错】:* What went wrong:Out of memory. Java heap space

项目场景&#xff1a; 今天&#xff0c;刚打开自己的安卓项目发现报错&#xff1a; 报错&#xff1a; * What went wrong: Out of memory. Java heap space Possible solution: - Check the JVM memory arguments defined for the gradle process in: gradle.properties in…

STM32G030F6P6TR ST意法

STM32G030F6P6TR是ST(意法半导体)一款基于高性能ArmCortex-M032位RISC内核&#xff0c;工作频率高达64MHz的32位MCU微控制器。代理销售ST(意法半导体)全系列IC电子元器件-中芯巨能为您提供STM32G030F6P6TR(ST 32位MCU)引脚图及中文参数介绍等内容。 STM32G030F6P6TR的中文参数 …

Python多态

1.多态 多态定义&#xff1a;多态&#xff08;polymorphism&#xff09;是指同一个方法调用由于对象不同可能会产生不同的行为 注意以下2点&#xff1a; 1.多态是方法的多态&#xff0c;属性没有多态。 2.多态的存在有2个必要条件&#xff1a;继承、方法重写 class Animal:de…

RabbitMQ入门实战

文章目录 RabbitMQ入门实战基本概念安装快速入门单向发送多消费者 RabbitMQ入门实战 官方&#xff1a;https://www.rabbitmq.com 基本概念 AMQP协议&#xff1a;https://www.rabbitmq.com/tutorials/amqp-concepts.html 定义&#xff1a;高级信息队列协议&#xff08;Advanc…

ORA-600 ktsiseginfo1故障---惜分飞

oracle 9i的库在运行途中突然报ORA-600 kcbnew_3错误 Sun Mar 31 14:25:11 2024 Undo Segment 69 Onlined Sun Mar 31 14:25:11 2024 Created Undo Segment _SYSSMU69$ Sun Mar 31 14:25:11 2024 Created Undo Segment _SYSSMU70$ Undo Segment 70 Onlined Sun Mar 31 14:28:41…