(九)模板方法模式详解(包含与类加载器不得不说的故事)

 作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可。

                  模板方法模式,这是一个在许多优秀的开源项目中LZ见的最多的一个设计模式,也是LZ觉得最为优秀的一个设计模式,所以这一章LZ会尽自己所能的去尽量将这个设计模式解释清楚。

                  模板方法模式,一般是为了统一子类的算法实现步骤,所使用的一种手段或者说是方式。它在父类中定义一系列算法的步骤,而将具体的实现都推迟到子类。

                  最典型的形式就是一个接口,一个抽象父类,父类中会有一系列的抽象方法,而在子类中去一一实现这些方法。

                  下面LZ给举一个例子,比如我们有一个接口,里面就一个方法,是用来制造一个HTML页面,如下。

public interface PageBuilder {String bulidHtml();}

                 这个接口很简单,就是直接制造一个Html页面的内容,假设我们不使用模板方法模式,直接让各个子类去直接实现这个接口,那么肯定实现的方式千奇百怪,而且步骤也乱七八糟的,这样实在不利于维护和扩展。所以我们可以使用模板方法模式,将这个过程给制定好,然后把具体的内容填充交给子类就好,这样这些子类生成的HTML页面就会非常一致。

                 基于这个目的,我们定义如下抽象类,去实现这个接口,并且我们定义好步骤。

复制代码
public abstract class AbstractPageBuilder implements PageBuilder{private StringBuffer stringBuffer = new StringBuffer();public String bulidHtml() {//首先加入doctype,因为都是html页面,所以我们父类不需要推迟给子类实现,直接在父类实现stringBuffer.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");//页面下面就是成对的一个HTML标签,我们也在父类加入,不需要给子类实现stringBuffer.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">");//下面就应该是head标签里的内容了,这个我们父类做不了主了,推迟到子类实现,所以我们定义一个抽象方法,让子类必须实现
        appendHead(stringBuffer);//下面是body的内容了,我们父类依然无法做主,仍然推迟到子类实现
        appendBody(stringBuffer);//html标签的关闭stringBuffer.append("</html>");return stringBuffer.toString();}//第一个模板方法protected abstract void appendHead(StringBuffer stringBuffer);//第二个模板方法protected abstract void appendBody(StringBuffer stringBuffer);}
复制代码

              上面LZ已经加了注释,这下我们如果要制作一个html页面,就直接继承我们的抽象父类就可以了,而我们的子类只需要实现两个模板方法,就可以成功完成html页面的创建,下面LZ给出一个子类,我们随意制造一个html页面。

复制代码
public class MyPageBuilder extends AbstractPageBuilder{@Overrideprotected void appendHead(StringBuffer stringBuffer) {stringBuffer.append("<head><title>你好</title></head>");}@Overrideprotected void appendBody(StringBuffer stringBuffer) {stringBuffer.append("<body><h1>你好,世界!</h1></body>");}public static void main(String[] args) {PageBuilder pageBuilder = new MyPageBuilder();System.out.println(pageBuilder.bulidHtml());}}
复制代码

               我们简单的加入一个head和body标签,然后创建测试类运行一下,就会发现,我们按照父类给的标准模板,生成了一个html页面。

               这样做的方式的好处是,父类可以规范子类的创建过程,便于我们维护,而且子类也更省事,因为像doctype包括html标签都是一样的,所以子类不再需要关心这些。当然上述LZ写的有点粗糙,其实我们可以定义的更仔细一点,比如head标签里,第一个是title,然后是meta等等。但作为例子,我们还是遵循简单的原则,主要还是想给各位传达模板方法模式的思想。

               模板方法模式是所有设计模式当中,LZ觉得最无侵入性的模式,因为它的好处实在是太明显了。模板方法模式并不强制接口的实现类必须继承,所以不会对子类造成任何影响,而如果子类的实现可以配得上模板类的模板,那么就可以享受模板方法模式带来的好处。

               通常情况下,模板方法模式用于定义构建某个对象的步骤与顺序,或者定义一个算法的骨架。

               我们刚才的示例明显就是构建一个String对象的过程,在这里要声明一点,对于模板方法模式,父类提供的构建步骤和顺序或者算法骨架,通常是不希望甚至是不允许子类去覆盖的,所以在某些场景中,可以直接将父类中提供骨架的方法声明为final类型。

               模板方法模式还有一种使用的方式,为了给子类足够的自由度,可以提供一些方法供子类覆盖,去实现一些骨架中不是必须但却可以有自定义实现的步骤。

               比如上述的例子当中,我们应该都知道,HTML页面中有一些标签是可有可无的。比如meta标签,link标签,script标签等。那么我们可以将刚才的例子细化一下,去看一下上面说的供子类覆盖的方法是什么。我们将刚才的抽象父类细化成如下形式。

复制代码
public abstract class AbstractPageBuilder implements PageBuilder{private static final String DEFAULT_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">";private static final String DEFAULT_XMLNS = "http://www.w3.org/1999/xhtml";private StringBuffer stringBuffer = new StringBuffer();public String bulidHtml() {stringBuffer.append(DEFAULT_DOCTYPE);stringBuffer.append("<html xmlns=\"" + DEFAULT_XMLNS + "\">");stringBuffer.append("<head>");appendTitle(stringBuffer);appendMeta(stringBuffer);appendLink(stringBuffer);appendScript(stringBuffer);stringBuffer.append("</head>");appendBody(stringBuffer);stringBuffer.append("</html>");return stringBuffer.toString();}protected void appendMeta(StringBuffer stringBuffer){}protected void appendLink(StringBuffer stringBuffer){}protected void appendScript(StringBuffer stringBuffer){}protected abstract void appendTitle(StringBuffer stringBuffer);protected abstract void appendBody(StringBuffer stringBuffer);}
复制代码

                可以看到,我们将head标签的生成过程更加细化了,分成四个方法,title,meta,link和script。但是这四个里面appendTitle是模板方法,子类必须实现,而其它三个则是普通的空方法。

                那么上述三个方法,就是留给子类覆盖的,当然子类可以选择不覆盖,那么生成的HTML就没有meta,link和script这三种标签,如果想有的话,就可以覆盖其中任意一个,比如下面这样。

复制代码
public class MyPageBuilder extends AbstractPageBuilder{protected void appendMeta(StringBuffer stringBuffer) {stringBuffer.append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />");}protected void appendTitle(StringBuffer stringBuffer) {stringBuffer.append("<title>你好</title>");}protected void appendBody(StringBuffer stringBuffer) {stringBuffer.append("<body>你好,世界!</body>");}public static void main(String[] args) {PageBuilder pageBuilder = new MyPageBuilder();System.out.println(pageBuilder.bulidHtml());}}
复制代码

                我们覆盖了appendMeta方法,所以我们就可以在head标签中生成一个meta标签。如果各位看过上章的适配器模式,其实这里和缺省适配很像,目的都是一样的,因为如果把appendMeta也写成抽象方法,那么子类就必须实现,但是meta标签又不是必须的,所以子类就有可能把appendMeta,appendLink,appendScript方法全空着了。

                所以为了不强制子类实现不必要的抽象方法,但又不剥夺子类自由选择的权利,我们在父类提供一个默认的空实现,来让子类自由选择是否要覆盖掉这些方法。

                说到模板方法模式,我们JDK当中有一个类与它还有一个不得不说的故事,那就是类加载器。

                JDK类加载器可以大致分为三类,分别是启动类加载器,扩展类加载器,以及应用程序加载器。

                这三者加载类的路径分别为如下:

                启动类加载器:JAVA_HOME/lib目录下,以及被-Xbootcalsspath参数设定的路径,不过启动类加载器加载的类是有限制的,如果JVM不认识的话,你放在这些目录下也没用。

                扩展类加载器:JAVA_HOME/lib/ext目录下,以及被java.ext.dirs系统变量指定的路径。

                应用程序类加载器:用户自己的类路径(classpath),这个类加载器就是我们经常使用的系统类加载器,并且JDK中的抽象类ClassLoader的默认父类加载器就是它。

                在这里为什么说类加载器和模板方法模式有关呢,是因为ClassLoader类就使用了模板模式,去保证类加载过程中的唯一性。LZ先给各位看下这个类当中的模板模式的应用。

复制代码
public abstract class ClassLoader {//这是一个重载方法public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}//这里就是父类算法的定义protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{Class c = findLoadedClass(name);if (c == null) {try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClass0(name);}} catch (ClassNotFoundException e) {c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}//这里留了一个方法给子类选择性覆盖protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}}
复制代码

              LZ截取了主要的部分,为了突出这三个方法。在上面LZ加了简单的注释,相信经过刚才的介绍,各位应该能看出来这是一个模板方法模式,只是它没有定义抽象方法,因为findClass这个方法,并不是必须实现的,所以JDK选择留给程序员们自己选择是否要覆盖。

              从代码上我们可以看出,在ClassLoader中定义的算法顺序是。

              1,首先看是否有已经加载好的类。

              2,如果父类加载器不为空,则首先从父类类加载器加载。

              3,如果父类加载器为空,则尝试从启动加载器加载。

              4,如果两者都失败,才尝试从findClass方法加载。

              这是JDK类加载器的双亲委派模型,即先从父类加载器加载,直到继承体系的顶层,否则才会采用当前的类加载器加载。这样做的目的刚才已经说了,是为了JVM中类的一致性。                            

              如果有读者第一次接触这方面的知识,估计会比较迷茫,下面LZ给出一个例子。各位猜测下下面程序的运行结果会是什么?

复制代码
package com.classloader;public class ClassLoaderTest {public static void main(String[] args) throws Exception {Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("com.classloader.ClassLoaderTest");Object entity = clazz.newInstance();System.out.println(entity instanceof ClassLoaderTest);}
}
复制代码

              相信各位都可以毫无疑问的猜测出来,结果应该是true,这是因为entity是ClassLoaderTest类的一个实例,instanceof关键字用来判断一个实例是否属于一个特定的类型,所以结果就是true。

              那么各位再来猜猜下面这段代码的运行结果会是什么?

复制代码
package com.classloader;import java.io.IOException;
import java.io.InputStream;class MyClassLoader extends ClassLoader{public Class<?> loadClass(String name) throws ClassNotFoundException {String fileName = name.substring(name.lastIndexOf(".")+1) + ".class";InputStream is = getClass().getResourceAsStream(fileName);if (is == null) {return super.loadClass(name);}try {byte[] b = new byte[is.available()];is.read(b);return defineClass(name, b, 0, b.length);} catch (IOException e) {throw new ClassNotFoundException();}}}public class ClassLoaderTest {public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {ClassLoader classLoader = new MyClassLoader();Class<?> clazz = classLoader.loadClass("com.classloader.ClassLoaderTest");Object entity = clazz.newInstance();System.out.println(entity instanceof ClassLoaderTest);}}
复制代码

              对于类加载器比较熟悉的读者们可能觉得这个结果并不出乎意料,可是或许还是有人会比较意外,为什么结果会是false呢?

              这是因为如果没有按照ClassLoader中提供的骨架算法去加载类的话,可能会造成JVM中有两个一模一样的类信息,他们是来自一个类文件,但却不是一个加载器加载的,所以这两个类不相等。

              这也是类加载器为何要使用模板模式给我们定义好查找的算法,是为了保证我们加载的每一个类在虚拟机当中都有且仅有一个。

              不过你可能会想,既然如此,为何不把loadClass方法写成final类型的,这样不是更安全吗?

              这是因为有的时候我们希望JVM当中每一个类有且仅有一个,但有的时候我们希望有两个,甚至N个,就比如我们的tomcat,你可以想象下,你每一个项目假设都有com.xxx.xxxx.BaseDao等等,如果这些类都是一个的话,你的tomcat还能同时启动多个WEB服务吗?虽说tomcat也是遵循的双亲委派模型,但是从此也可以看出来,我们并不是在所有时候都希望同一个全限定名的类在整个JVM里面只有一个。

              这里提到类加载器,是为了给模板方法一个现有的现实中的例子,以便于有些看多了自己制造的例子的读者可以换个口味,如果有机会,LZ会在这个系列完结以后,专门开一个系列来和各位分享学习虚拟机过程中的感悟,本次不再过多介绍类加载器的相关内容。

              另外,如果多掌握一些类加载器的知识,还是对平时的工作和学习有很大帮助的,各位也可以私下去搜索下相关资料。

              好了,模板方法模式就介绍到这里吧,希望各位都有自己的收获。

              谢谢观看。

              下期预告,装饰器模式。
 

                      

                     


版权声明


作者:zuoxiaolong(左潇龙)

出处:博客园左潇龙的技术博客--http://www.cnblogs.com/zuoxiaolong

您的支持是对博主最大的鼓励,感谢您的认真阅读。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

转载于:https://www.cnblogs.com/Zyf2016/p/6337740.html

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

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

相关文章

阿里云openapi接口使用,PHP,视频直播

1.下载sdk放入项目文件夹中 核心就是aliyun-php-sdk-core&#xff0c;它的配置文件会自动加载相应的类 2.引入文件 include_once LIB_PATH . ORG/aliyun-openapi/aliyun-php-sdk-core/Config.php; 3.配置客户端对象,需要Access Key ID,Access Key Secret $iClientProfile Defa…

Android之网络编程利用PHP操作MySql插入数据(四)

因为最近在更新我的项目&#xff0c;就想着把自己在项目中用到的一些的简单的与网络交互的方法总结一下&#xff0c;所以最近Android网络编程方面的博文会比较多一些&#xff0c;我尽量以最简单的方法给大家分享&#xff0c;让大家明白易懂。如果有什么不对的地方&#xff0c;还…

【常用网址】——opencv等

opencv官网Releases - OpenCVhttps://opencv.org/releases/

(五):C++分布式实时应用框架——微服务架构的演进

C分布式实时应用框架——微服务架构的演进 技术交流合作QQ群&#xff1a;436466587 欢迎讨论交流 上一篇&#xff1a;(四)&#xff1a;C分布式实时应用框架——状态中心模块 版权声明:本文版权及所用技术归属smartguys团队所有&#xff0c;对于抄袭&#xff0c;非经同意转载等…

Robot Application Builder

软件开发工具包 Robot Application Builder是安装在PC机&#xff08;Windows 2000或Windows XP操作系统&#xff09;上的一种独立开发工具&#xff0c;可用于创建运行于ABB FlexPendant示教器或PC机上的定制化操作界面。为此&#xff0c;该软件包由以下两部分组成&#xff1a;…

asp.net model 验证和取出 ErrorMessage 信息

为什么80%的码农都做不了架构师&#xff1f;>>> public class Users{public int Id { get; set; }public string Name { get; set; }[Required(ErrorMessage "邮箱不能为空")][EmailAddressAttribute(ErrorMessage "邮箱格式不正确")]public…

【pyqt5学习——信号与槽】实例计时器(解决界面卡顿问题)

目录 一、方法一&#xff1a;另开线程 1、什么是信号与槽 1&#xff09;GUI控件&#xff08;信号&#xff09;与槽 2&#xff09;自定义信号与槽 2、实战1&#xff1a;计时器&#xff08;不自定义信号槽和不使用多线程&#xff09; 1&#xff09;界面设计——利用qt-desi…

【数据库学习笔记】——创建数据库连接对象connection

目录 connect函数的参数 创建连接对象连接MySQL代码 连接对象常见属性与方法 事务名词解释 课程视频链接&#xff1a; 第14节 Python操作数据库_哔哩哔哩_bilibili666https://www.bilibili.com/video/BV1q54y147KX?fromsearch&seid968950907021994347&spm_id_from3…

【数据库学习笔记】——cursor游标对象

目录 1、创建cursor对象 2、cursor对象常用方法 3、操作数据库的常见流程&#xff08;五部曲&#xff09; 课程视频链接&#xff1a; 第14节 Python操作数据库_哔哩哔哩_bilibili666https://www.bilibili.com/video/BV1q54y147KX?p2&spm_id_frompageDriver 1、创建cu…

基于ARM核AT75C220在指纹识别系统中应用

纹识别技术近年来逐渐成熟&#xff0c;在门禁、安防和金融等方面得到了越来越广泛的应用。典型的指纹识别系统是以指纹传感器和DSP处理器为核心构成。指纹传感器采集指纹图像&#xff0c;DSP处理器实时实现指纹识别算法。同时&#xff0c;通常的指纹识别系统还具有较强的通信能…

【数据库学习笔记】——创建数据库文件

目录 1、数据库基础知识 2、创建数据库文件 1、数据库基础知识 什么是数据库&#xff1f;——数据库是存放多个数据表的仓库&#xff0c;数据表可以理解为是二维数组 如上表所示&#xff0c;每个表由行列组成&#xff1a; 字段&#xff1a;每一列称之为一个字段&#xff0c;每…

浅谈工业机器人的运动停止

德系的工业机器人系统中&#xff0c;对于机器人停止运动&#xff0c;定义了3种模式&#xff0c;比如 KUKA 的工业机器人分别定义了 Stop 0 &#xff0c;Stop 1&#xff0c;Stop 2 (*注1)。这种定义模式是与机器人的机械结构和电气结构相关联的。 对于此&#xff0c;读者应该先了…

【数据库学习】——数据库可视化--Navicat下载安装连接教程

目录 进入网站 下载可视化软件 安装 双击桌面图标&#xff0c;选择试用 连接数据库 查看数据库中的表&#xff0c;如下所示 常用数据库有&#xff1a; MySQL、sqlite等 进入网站 Navicat | 产品https://www.navicat.com.cn/products 下载可视化软件 这里以数据库sql…

C# 格式化字符串

原文地址&#xff1a;http://www.cnblogs.com/zyh-nhy/archive/2007/10/11/921240.html 1 前言如果你熟悉Microsoft Foundation Classes&#xff08;MFC&#xff09;的CString&#xff0c;Windows Template Library&#xff08;WTL&#xff09;的CString或者Standard Template …

【数据库学习笔记】——操作sqlite(增删改查)以及cursor的方法介绍

目录 1、sqlite数据库介绍 1&#xff09;常见的数据库操作 2&#xff09;数据操作常见步骤 2、向数据表中增加数据&#xff08;insert into&#xff09; 1&#xff09;向数据表中添加一条记录 2&#xff09;向数据表中一次性添加多条记录 ​ 3、修改数据表中已有的数据 1)…

【sqlite常用操作SQL语句】

目录 1、创建一个新的数据表 2、在已有的数据表中增加一个新的字段&#xff08;列&#xff09; 3、 在已有的数据表中增加一条新的记录&#xff08;行&#xff09; 1、创建一个新的数据表 "create table user(id int(11) primary key, name varchar(20))" 含义&…

centos6虚拟机复制后修改网卡

方法1&#xff1a; 使用vmware创建centos6.4虚拟机&#xff0c; 创建完成后复制该虚拟机&#xff0c; 打开复制的虚拟机发现网卡名字是eth1&#xff0c;而网卡配置文件为eth0&#xff0c;mac地址变了 这时修改网卡配置文件&#xff0c; 删除uuid&#xff0c;修改deivce为eth1&a…

【pyinstaller打包pyqt5编写的项目为exe(脱离环境可运行)】

目录 下载pyinstaller库 0、pyinstaller语句介绍 1、单个py文件打包成exe 1)只有py文件 假设只有一个py文件&#xff1a;pyinstaller -F xxx.py 加上图标&#xff1a;pyinstaller -F xxx.py -i xxx.ico 取消命令行窗口:pyinstaller -F -w xxx.py -i xxx.ico 2) 不但有py…

python 中cPickle学习二

写入&#xff1a; import cPickle as p shoplistfile data.data shoplist [meili,[current_account,[100000,1222],basis_account,[5555555,888]],qinshan,[current_account,[1089000,12292],basis_account,[55555955,888]],jiayou,[current_account,[10000,12292],basis_acc…

4.0 多线程基础篇

本文并非最终版本&#xff0c;如有更新或更正会第一时间置顶&#xff0c;联系方式详见文末如果觉得本文内容过长&#xff0c;请前往本人 “简书”4.0-1.1 进程 概念 : 进程是指在系统中正在运行的一个应用程序 (操作系统中每一个 APP 就是一个进程)  性质 : 每个进程之间是独…