热门关键字:
jquery > jquery教程 > html5 > 九:模板方法模式

九:模板方法模式

199
作者:管理员
发布时间:2020/3/24 10:58:09
评论数:0
转载请自觉注明原文:http://www.jq-school.com/Show.aspx?id=1093

  九:模板方法模式

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

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

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

  publicinterfacePageBuilder{

  StringbulidHtml();

  }

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

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

  复制代码

  publicabstractclassAbstractPageBuilderimplementsPageBuilder{

  privateStringBufferstringBuffer=newStringBuffer();

  publicStringbulidHtml(){

  //首先加入doctype,因为都是html页面,所以我们父类不需要推迟给子类实现,直接在父类实现

  stringBuffer.append("<!DOCTYPEhtmlPUBLIC\"-//W3C//DTDXHTML1.0Transitional//EN\"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");

  //页面下面就是成对的一个HTML标签,我们也在父类加入,不需要给子类实现

  stringBuffer.append("<htmlxmlns=\"http://www.w3.org/1999/xhtml\">");

  //下面就应该是head标签里的内容了,这个我们父类做不了主了,推迟到子类实现,所以我们定义一个抽象方法,让子类必须实现

  appendHead(stringBuffer);

  //下面是body的内容了,我们父类依然无法做主,仍然推迟到子类实现

  appendBody(stringBuffer);

  //html标签的关闭

  stringBuffer.append("</html>");

  returnstringBuffer.toString();

  }

  //第一个模板方法

  protectedabstractvoidappendHead(StringBufferstringBuffer);

  //第二个模板方法

  protectedabstractvoidappendBody(StringBufferstringBuffer);

  }

  复制代码

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

  复制代码

  publicclassMyPageBuilderextendsAbstractPageBuilder{

  @Override

  protectedvoidappendHead(StringBufferstringBuffer){

  stringBuffer.append("<head><title>你好</title></head>");

  }

  @Override

  protectedvoidappendBody(StringBufferstringBuffer){

  stringBuffer.append("<body><h1>你好,世界!</h1></body>");

  }

  publicstaticvoidmain(String[]args){

  PageBuilderpageBuilder=newMyPageBuilder();

  System.out.println(pageBuilder.bulidHtml());

  }

  }

  复制代码

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

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

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

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

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

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

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

  复制代码

  publicabstractclassAbstractPageBuilderimplementsPageBuilder{

  privatestaticfinalStringDEFAULT_DOCTYPE="<!DOCTYPEhtmlPUBLIC\"-//W3C//DTDXHTML1.0Transitional//EN\"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">";

  privatestaticfinalStringDEFAULT_XMLNS="http://www.w3.org/1999/xhtml";

  privateStringBufferstringBuffer=newStringBuffer();

  publicStringbulidHtml(){

  stringBuffer.append(DEFAULT_DOCTYPE);

  stringBuffer.append("<htmlxmlns=\""+DEFAULT_XMLNS+"\">");

  stringBuffer.append("<head>");

  appendTitle(stringBuffer);

  appendMeta(stringBuffer);

  appendLink(stringBuffer);

  appendScript(stringBuffer);

  stringBuffer.append("</head>");

  appendBody(stringBuffer);

  stringBuffer.append("</html>");

  returnstringBuffer.toString();

  }

  protectedvoidappendMeta(StringBufferstringBuffer){

  }

  protectedvoidappendLink(StringBufferstringBuffer){

  }

  protectedvoidappendScript(StringBufferstringBuffer){

  }

  protectedabstractvoidappendTitle(StringBufferstringBuffer);

  protectedabstractvoidappendBody(StringBufferstringBuffer);

  }

  复制代码

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

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

  复制代码

  publicclassMyPageBuilderextendsAbstractPageBuilder{

  protectedvoidappendMeta(StringBufferstringBuffer){

  stringBuffer.append("<metahttp-equiv=\"Content-Type\"content=\"text/html;charset=utf-8\"/>");

  }

  protectedvoidappendTitle(StringBufferstringBuffer){

  stringBuffer.append("<title>你好</title>");

  }

  protectedvoidappendBody(StringBufferstringBuffer){

  stringBuffer.append("<body>你好,世界!</body>");

  }

  publicstaticvoidmain(String[]args){

  PageBuilderpageBuilder=newMyPageBuilder();

  System.out.println(pageBuilder.bulidHtml());

  }

  }

  复制代码

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

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

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

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

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

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

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

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

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

  复制代码

  publicabstractclassClassLoader{

  //这是一个重载方法

  publicClass<?>loadClass(Stringname)throwsClassNotFoundException{

  returnloadClass(name,false);

  }

  //这里就是父类算法的定义

  protectedsynchronizedClass<?>loadClass(Stringname,booleanresolve)

  throwsClassNotFoundException

  {

  Classc=findLoadedClass(name);

  if(c==null){

  try{

  if(parent!=null){

  c=parent.loadClass(name,false);

  }else{

  c=findBootstrapClass0(name);

  }

  }catch(ClassNotFoundExceptione){

  c=findClass(name);

  }

  }

  if(resolve){

  resolveClass(c);

  }

  returnc;

  }

  //这里留了一个方法给子类选择性覆盖

  protectedClass<?>findClass(Stringname)throwsClassNotFoundException{

  thrownewClassNotFoundException(name);

  }

  }

  复制代码

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

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

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

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

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

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

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

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

  复制代码

  packagecom.classloader;

  publicclassClassLoaderTest{

  publicstaticvoidmain(String[]args)throwsException{

  Class<?>clazz=ClassLoader.getSystemClassLoader().loadClass("com.classloader.ClassLoaderTest");

  Objectentity=clazz.newInstance();

  System.out.println(entityinstanceofClassLoaderTest);

  }

  }

  复制代码

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

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

  复制代码

  packagecom.classloader;

  importjava.io.IOException;

  importjava.io.InputStream;

  classMyClassLoaderextendsClassLoader{

  publicClass<?>loadClass(Stringname)throwsClassNotFoundException{

  StringfileName=name.substring(name.lastIndexOf(".")+1)+".class";

  InputStreamis=getClass().getResourceAsStream(fileName);

  if(is==null){

  returnsuper.loadClass(name);

  }

  try{

  byte[]b=newbyte[is.available()];

  is.read(b);

  returndefineClass(name,b,0,b.length);

  }catch(IOExceptione){

  thrownewClassNotFoundException();

  }

  }

  }

  publicclassClassLoaderTest{

  publicstaticvoidmain(String[]args)throwsInstantiationException,IllegalAccessException,ClassNotFoundException{

  ClassLoaderclassLoader=newMyClassLoader();

  Class<?>clazz=classLoader.loadClass("com.classloader.ClassLoaderTest");

  Objectentity=clazz.newInstance();

  System.out.println(entityinstanceofClassLoaderTest);

  }

  }

  复制代码

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

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

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

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

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

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





如果您觉得本文的内容对您的学习有所帮助:支付鼓励



关键字:html
友荐云推荐