模板方法模式
在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这个模式是用来创建一个算法的模板。什么是模板?如你所见,模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
//未改造前:public class Coffee{ void prepareRecipe(){ boilWater(); brewCoffeeGrinds(); pourInCup(); addSugarAndMilk(); } public void boilWater(){ System.out.println("Boiling water"); } public void brewCoffeeGrinds(){ System.out.println("Dripping Coffee through filter"); } public void pourInCup(){ System.out.println("Pouring into cup"); } public void addSugarAndMilk(){ System.out.println("Adding Sugar and Milk"); }}//未改造前public class Tea { void prepareRecipe(){ boilWater(); steepTeaBag(); pourInCup(); addLemon(); } public void boilWater(){ System.out.println("Boiling water"); } public void steepTeaBag(){ System.out.println("Steeping the tea"); } public void addLemon(){ System.out.println("Adding Lemon"); } public void pourInCup(){ System.out.println("Pouring into cup"); }}//改造后//咖啡因饮料抽象类public abstract class CaffeineBeverage { final void prepareRecipe(){ boilWater(); pourInCup(); brew(); addCondiments(); } void boilWater(){ System.out.println("Boiling water"); } void pourInCup(){ System.out.println("Pouring into cup"); } abstract void brew(); abstract void addCondiments();}//改造后public class Coffee extends CaffeineBeverage{ [@Override](https://my.oschina.net/u/1162528) void brew() { System.out.println("Dripping Coffee through filter"); } [@Override](https://my.oschina.net/u/1162528) void addCondiments() { System.out.println("Add Sugar and Milk"); }}//改造后public class Tea extends CaffeineBeverage{ [@Override](https://my.oschina.net/u/1162528) void brew() { System.out.println("Steeping the tea"); } [@Override](https://my.oschina.net/u/1162528) void addCondiments() { System.out.println("Adding Lemon"); }}
模板方法被声明为final,以免子类改变这个算法的顺序
对模板方法更进一步:
public abstract class AbstractClass { final void templateMethod(){ //模板方法 primitiveOperation1(); primitiveOperation2(); concreteOperation(); hook(); } abstract void primitiveOperation1(); abstract void primitiveOperation2(); final void concreteOperation(){ //实现,这个具体的方法被定义在抽象类中。将它声明为final,这样一来子类就无法覆盖它。它可以被模板方法直接使用 //或者被子类使用 } void hook(){ //我们也可以有"默认不做事的方法",我们称这种方法为"hook"(钩子)。子类可以视情况决定要不要覆盖它们。 //钩子的存在,可以让子类有能力对算法的不同点进行挂钩,由子类自行决定 }}//对模板方法进行挂钩...public abstract class CaffeineBeverageWithHook { void prepareRecipe(){ //模板方法 boilWater(); brew(); pourInCup(); if (customerWantsCondiments()){ addCondiments(); } } abstract void brew(); abstract void addCondiments(); String getUserInput(){ return "no"; } void boilWater(){ System.out.println("Boiling water"); } void pourInCup(){ System.out.println("Pouring into cup"); } boolean customerWantsCondiments(){ //如果实现后面的算法,直接重写改为false return true; }}//使用钩子public class CoffeeWithHook extends CaffeineBeverageWithHook { [@Override](https://my.oschina.net/u/1162528) void brew() { System.out.println("Dripping Coffee through filter"); } @Override void addCondiments() { System.out.println("Adding Sugar and Milk"); } @Override boolean customerWantsCondiments() { String answer = getUserInput(); if (answer.toLowerCase().startsWith("y")){ return true; }else{ return false; } } private String getUserInput() { String answer = null; System.out.println("Would you like milk and sugar with your coffee (y/n)?"); BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); try { answer = in.readLine(); } catch (IOException ioe) { System.out.println("IO error trying to read your answer"); } if (answer == null) { return "no"; } return answer; }}//测试结果public class BeverageTestDrive { public static void main(String[] args) { TeaWithHook teaHook = new TeaWithHook(); CoffeeWithHook coffeeHook = new CoffeeWithHook(); System.out.println("\nMaking tea..."); teaHook.prepareRecipe(); System.out.println("\nMaking coffee..."); coffeeHook.prepareRecipe(); }}
问1:似乎我应该保持抽象方法的数目越少越好,否则,在子类中实现这些方法将会很麻烦。
答案:
- 当你在写模板方法的时候,心里要随时记得这一点。想要做到这一点,可以让算法内的步骤不要切割得太细,但是如果步骤太少的话,会比较没有弹性,所以要看情况折衷。
- 也请记住,某些步骤是可选的,所以你可以将这些步骤实现成钩子,而不是实现成抽象方法,这样就可以让抽象类的子类的负荷减轻。
好莱坞原则:
别调用(打电话给)我们,我们会调用(打电话给)你。
好莱坞原则可以给我们一种防止"依赖腐败"的方法。当高层组件依赖底层组件,而底层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖底层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是"别调用我们,我们会调用你"。
重点:高层组件控制何时以及如何让低层组件参与,低层组件可以参与计算,低层组件绝对不可以直接调用高层组件。
问:好莱坞原则和依赖倒置原则之间的关系如何?
答:依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩进计算中,而且又不会让高层组件依赖低层组件。两者的目标都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。好莱坞原则教我们一个技巧,创建一个有弹性的设计,允许低层结构能够互相操作,而又防止其他类太过依赖它们
API中的模板方法:
Java数组类的设计者提供给我们一个方便的模板方法用来排序。让我们看看这个方法如何运行:
public static void sort(Object[] a){ //Arrays.sort(Object[] objs) Object aux[] = (Object[])a.clone(); mergeSort(aux,a,0,a.length,0);}private static void mergeSort(Object src[],Object dest[],int low,int high,int off){ for(int i=low;ilow &&((Comparable)dest[j-i]).compareTo((Comparable)dest[j])>0;j--){ swap(dest,j,j-1); } } return;}
我们需要实现compareTo()方法,sort()是静态的,所以使用它和他被定义在超类中是一样的。因为sort()并不是真正定义在超类中, 所以sort()方法需要知道已经实现了这个compartTo()方法,否则就无法进行排序。
//排序鸭子,实现compareTo()方法public class Duck implements Comparable{ String name; int weight; public String toString(){ return name + " weighs "+weight; } public int compareTo(Object object){ //compareTo()需要被传入另一个鸭子,和本身这只鸭子做比较 Duck otherDuck = (Duck)object; if(this.weightotherDuck.weight return 1; } }}//鸭子排序public class DuckSortTestDrive { public static void main(String[] args) { Duck[] ducks = { new Duck("Daffy", 15), new Duck("Mikey", 18), new Duck("TangLaoYa", 17), new Duck("MiNi", 16) }; display(ducks); Arrays.sort(ducks); System.out.println("\n"); display(ducks); } public static void display(Duck[] ducks) { for (Duck duck : ducks) { System.out.println("name:" + duck.getName() + "," + "weight:" + duck.getWeight()); } }}
要点:
- "模板方法"定义了算法的步骤,把这些步骤的实现延迟到子类。
- 模板方法模式为我们提供了一种代码复用的重要技巧。
- 模板方法的抽象类可以定义具体方法、抽象方法和钩子。
- 抽象方法由子类实现。
- 钩子是一种方法,它在抽象类中不做事,或者只做默认的事,子类可以选择要不要去覆盖它。
- 为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
- 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用低层模块。
- 你将在真实世界代码中看到模板方法模式的许多变体,不要期待它们全都是一眼就可以被你认出的。
- 策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
- 工厂方法是模板方法的一种特殊版本。