Java设计模式之模板方法模式Template Method Pattern详解

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Java设计模式之模板方法模式Template Method Pattern详解

流烟默   2022-11-02 我要评论

概述

模板方法

模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。那么什么是模板方法呢?我们看下模板方法的定义。

  1. 一个具体方法而非抽象方法,其用作一个算法的模板;
  2. 在模板方法中,算法内的大多数步骤都被某个方法代表;
  3. 模板方法中某些方法是子类处理
  4. 需要由子类提供的方法,必须在超类中声明为抽象方法;
  5. 模板方法通常不能被覆盖,也就是使用final修饰;

模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),指在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行简单说,模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤。

这种类型的设计模式属于行为型模式。

如下实例,prepareRecipe就是一个模板方法。

public abstract class CaffeineBeverage {
   final void  prepareRecipe(){
       boilWater();
       brew();
       pourInCup();
       addCondiments();
   }
   protected abstract void addCondiments();
   protected abstract void pourInCup();
   protected abstract void brew();
   protected abstract void boilWater();
}

模板方法模式

在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

这个模式是用来创建一个算法的模板。什么是模板?如你所见的,模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。

对上面实例进一步扩展,我们看下抽象类内可以有哪些类型的方法。

public abstract class CaffeineBeverage {
   final void  prepareRecipe(){
       boilWater();
       brew();
       pourInCup();
       addCondiments();
       concreteOperation();
       hook();
   }
   protected abstract void addCondiments();
   protected abstract void pourInCup();
   protected abstract void brew();
   protected abstract void boilWater();
      final void concreteOperation(){
	// 这里是实现
   }
	//空方法
   void hook(){};
}

可以看到有一个具体的concreteOperation方法,final表示其不可以被子类覆盖。我们也可以由“默认不做事的方法”,我们称这种方法为“hook”(钩子)。子类可以视情况决定要不要覆盖他们。

钩子是一种被声明在 抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类决定。每一个具体的子类都必须定义所有的抽象方法,并为模板方法算法中未定义步骤提供完整的实现。

那么什么时候使用抽象方法什么时候使用钩子呢?

答,当你的子类必须提供算法中某个算法或步骤的实现时,就是用抽象方法。如果算法的这个部分是可选的,就用钩子。如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做。

使用钩子的真正目的是什么?

钩子有几种用法。如我们之前所说的,钩子可以让子类实现算法中可选的部分,或者在钩子对于子类的实现并不重要的时候,子类可以对此钩子置之不理。钩子的另一个用法,是让子类能够有机会对模板方法中某些即将发生的(或刚刚发生的)步骤作出反应。比方说,名为justReOrderedList()的钩子方法允许子类在内部列表重新组织后执行某些动作(例如在屏幕上重新显示数据)。正如前面提到的,钩子也可以让子类有能力为其抽象类作一些决定。

好莱坞原则

好莱坞原则简单来讲就是:别调用我们,我们会调用你。

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

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

模板方法模式就契合该原则。当我们设计模板方法模式时,我们告诉子类,“不要调用我们,我们会调用你”。

那么低层组件完全不可以调用高层组件的方法吗?并不尽然!

事实上,低层组件在结束时,常常调用从超类中继承来的方法。我们所要做的是,避免让高层和低层组件之间有明显的环状依赖。

好莱坞原则与依赖倒置原则

依赖倒置原则教我们尽量避免使用具体类,而多使用抽象类。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。

好莱坞原则教我们一个技巧,创建一个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过依赖它们。

真实案例

数组类Arrays的mergeSort方法就是一个模板方法。

 private static void mergeSort(Object[] src,
                                  Object[] dest,
                                  int low,
                                  int high,
                                  int off) {
        int length = high - low;
        // Insertion sort on smallest arrays
        if (length < INSERTIONSORT_THRESHOLD) {
            for (int i=low; i<high; i++)
                for (int j=i; j>low &&
                         ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }
        // Recursively sort halves of dest into src
        int destLow  = low;
        int destHigh = high;
        low  += off;
        high += off;
        int mid = (low + high) >>> 1;
        mergeSort(dest, src, low, mid, -off);
        mergeSort(dest, src, mid, high, -off);
        // If list is already sorted, just copy from src to dest.  This is an
        // optimization that results in faster sorts for nearly ordered lists.
        if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
            System.arraycopy(src, low, dest, destLow, length);
            return;
        }
        // Merge sorted halves (now in src) into dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }
    }

这里方法中,swap方法是一个具体方法。每一个元素需要实现compareTo方法,“填补”模板方法的缺憾。

可能会有疑问,上面这个案例真的是一个模板方法吗?

答,其符合模板方法的精神。这个模式的重点在于提供一个算法,并让子类实现某些步骤。而数组的排序做法很明显地并非如此!但是,我们都知道。Java api中的模式并非总是如同教科书例子一般地中规中矩,为了符合当前的环境和实现的约束,它们总是要被适当地修改。这个Arrays类sort方法的设计者受到一些约束。通常我们无法设计一个类继承Java数组,而sort()方法希望能够适用于所有的数组(每个数组都是不同的类)。所以它们定义了一个静态方法,而由被排序的对象内的每个元素自行提供比较大小的算法部分。所以,这虽然不是教科书上的模板方法,但它的实现仍然符合模板方法模式的精神。再者,由于不需要基础数组就可以使用这个方法,这样使得排序变得更有弹性、更有用。

另外一个案例是java.io的InputStream类有一个read()方法,是由子类实现的,而这个方法又会被read(byte b[], int off, int len)模板方法使用。

模板方法模式的注意事项和细节

基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改。

实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。

该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大。一般模板方法都加上final 关键字, 防止子类重写模板方法。

模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们