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

在我们实际开发中,如果一个方法极其复杂时,如果我们将所有的逻辑写在一个方法中,那维护起来就很困难,要替换某些步骤时都要重新写,这样代码的扩展性就很差,当遇到这种情况就要考虑今天的主角——模板方法模式

概述

模板方法

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

  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 关键字, 防止子类重写模板方法。

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

到此这篇关于Java设计模式之模板方法模式Template Method Pattern详解的文章就介绍到这了,更多相关Java模板方法模式内容请搜索编程学习网以前的文章希望大家以后多多支持编程学习网!

本文标题为:Java设计模式之模板方法模式Template Method Pattern详解

基础教程推荐