8.模板方法模式

1.引入

本节将要深入封装算法块,好让子类可以在任何时候都可以将自己挂接进运算里。

1.1 场景引入

茶和咖啡的冲泡方式非常相似:

快速搞定几个咖啡和茶的类:

咖啡代码:

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(){

    }

    public void steepTeaBag(){

    }

    public  void pourInCup(){

    }

    public void addLemon(){
        
    }


}

发现了重复的代码,这表示我们需要清理一下没计了。在这里,既然茶和咖啡是如此地相似,似乎我们应该将共同的部分抽取出来,放进一个基类中。

注意两份冲泡法都采用了相同的算法

 注意步骤2和步骤4的执行逻辑是一样的,只是应用在不同的饮料上。

第一版的设计

1.2 抽象prepareRecipe()

对第一版继续进行改进,将prepareRecipe()方法进行抽象

从每一个子类(茶和咖啡)中逐步抽象prepareRecipe().

1.遇到的第一个问题,是咖啡使用brewCoffeeGrinds()和addSugerAndMilk()方法,而茶使用steepTeaBag()和addLemon()方法。

 让我们来思考这一点:浸泡(steep)和冲泡(brew)差异其实不大。所以我们给它一个新的方法名称,比方说brew(),然后不管是泡茶或冲泡咖啡我们都用这个名称。类似地,加糖和牛奶也和加柠檬很相似:都是在饮料中加入调料。让我们也给它一个新的方法名称来解决这个向题,就叫做addCondiments()好了。这样一来,新的prepareRecipe()方法看起来就像这样:

2.现在有了新的prepareRecipe()方法,但是需要让他能够符合代码。那就要先设计CaffeineBeverage(咖啡因饮料)超类开始:

public abstract class CaffeineBeverage {
  
	final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
		addCondiments();
	}
 
	abstract void brew();
  
	abstract void addCondiments();
 
	void boilWater() {
		System.out.println("Boiling water");
	}
  
	void pourInCup() {
		System.out.println("Pouring into cup");
	}
}

 对代码中的几个点进行分析:

  1. 用一个prepareRecipe()方法来处理茶和咖啡。prepareRecipe()被声明为final,因为不希望子类覆盖这个方法,同时将步骤2和步骤4泛化为brew()和addCondiments()。
  2. 因为咖啡和茶处理这些方法的做法不同,因此这两个方法必须被声明为抽象,剩余的东西留给子类去操心

3.最后,来处理咖啡和茶类。这两个类是依赖超类来处理冲泡法,因此只需要自行处理冲泡和添加调料部分:

public class Coffee extends CaffeineBeverage {
	@Override
	public void brew() {
		System.out.println("Dripping Coffee through filter");
	}
	@Override
	public void addCondiments() {
		System.out.println("Adding Sugar and Milk");
	}
}


public class Tea extends CaffeineBeverage {
	@Override
	public void brew() {
		System.out.println("Steeping the tea");
	}
	@Override
	public void addCondiments() {
		System.out.println("Adding Lemon");
	}
}

 通过对咖啡和茶 这两类的制作步骤,就已经将模板方法体现出来。

1.3 认识模板方法

来看看咖啡因饮料类的结构,其中就包含了实际的“模板方法”。

prepareRecipe()是我们的模板方法,原因:

1.它是一个方法

2.用作一个算法的模板,在这个例子中,算法是用来制作咖啡因饮料的

在这个模板中,算法内的每一个步骤都被一个方法代表了。

某些方法是由这个类(超类)处理的。

 模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现

1.4 追踪模板方法的工作方式

通过逐步地泡茶,追踪这个模板方法是如何工作的。将会得知在算法内的某些地方,该模板方法控制了算法。让子类能够提供某些步骤的实现:

 模板方法有利于,将代码的复用最大化。

2.模板方法

2.1 定义模板方法模式

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

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

2.2 模板方法类图与相关代码

抽象类被声明成 抽象的,用来作为基类,其子类必须实现其操作。

模板方法templateMethod(),被声明为final,以免子类改变这个算法的顺序。

模板方法定义了一连串的步骤,每个步骤由一个方法代表。

在这个范例中,有两个原语操作,具体子类必须实现它们。

 2.3 钩子方法的定义与使用

钩子是一种被声明在抽象类中的方法,但只有空的或默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定

有了钩子,开发者就能够觉得要不要覆盖方法。如果子类不提供自己的方法,抽象类也会提供一个默认的实现。

public abstract class CaffeineBeverageWithHook {
 
	final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
		//加上一个小小的条件语句,而该条件是否成立,是由一个具体方法customerWantsCondiments()决定的
		//如果客户“想要”调料,只有这时我们才调用addCondiments()
		if (customerWantsCondiments()) {
			addCondiments();
		}
	}
 
	abstract void brew();
 
	abstract void addCondiments();
 
	void boilWater() {
		System.out.println("Boiling water");
	}
 
	void pourInCup() {
		System.out.println("Pouring into cup");
	}

	//定义了一个方法,(通常)是空的缺省实现,这个方法只会返回true,不做别的事
	//这就是一个钩子,子类可以覆盖这个方法,但是不是一定要这么做
	boolean customerWantsCondiments() {
		return true;
	}
}

使用钩子

为了使用钩子,在子类中覆盖它。此处,钩子控制了咖啡因饮料是否执行某部分算法,即饮料中是否需要加进饮料。

在子类中覆盖这个钩子,来获取顾客是否想要调料。

public class CoffeeWithHook extends CaffeineBeverageWithHook {
 
	@Override
	public void brew() {
		System.out.println("Dripping Coffee through filter");
	}
 
	@Override
	public void addCondiments() {
		System.out.println("Adding Sugar and Milk");
	}
 
	@Override
	public boolean customerWantsCondiments() {

		String answer = getUserInput();

		if (answer.toLowerCase().startsWith("y")) {
			return true;
		} else {
			return false;
		}
	}
 
	private String getUserInput() {
		String answer = null;

		System.out.print("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.err.println("IO error trying to read your answer");
		}
		if (answer == null) {
			return "no";
		}
		return answer;
	}
}

执行测试代码:

来制造热茶和热咖啡:

public class BeverageTestDrive {
	public static void main(String[] args) {
 
		Tea tea = new Tea();
		Coffee coffee = new Coffee();
 
		System.out.println("\nMaking tea...");
		tea.prepareRecipe();
 
		System.out.println("\nMaking coffee...");
		coffee.prepareRecipe();

 
		TeaWithHook teaHook = new TeaWithHook();
		CoffeeWithHook coffeeHook = new CoffeeWithHook();
 
		System.out.println("\nMaking tea...");
		teaHook.prepareRecipe();
 
		System.out.println("\nMaking coffee...");
		coffeeHook.prepareRecipe();
	}
}

测试结果:

 在这个例子中,钩子能够作为条件控制,影响抽象类中的算法流程

:当我创建一个模板方法时,怎么才能知道什么时候该使用抽象方法,什么时候使用钩子呢?

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

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

:钩子有几种用法:

1.钩子可以让子类实现算法中可选的部分,或者在钩子对子类的实现并不重要的时候,子类可以对此钩子置之不理。

2.让子类能够有机会对模板方法汇总某些即将发生的(或刚刚发生的)步骤做出反应。

2.4 好莱坞原则

好莱坞原则:别调用我们,我们会调用你。

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

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

2.5 好莱坞原则和模板方法

好菜坞原则和模板方法之间的连接其实还算明显:当我们设计模板方法模式时,我们告诉子类,“不要调用我们,我们会调用你”。怎样才能办到呢?让我们再看一次咖啡因饮料的设计:

 当需要底层组件Tea或者Coffee时,高层组件CaffeineBeverage才会去调用Coffee或者Tea这俩子类的具体方法。

问:好莱坞原则和依赖倒置原则(第四章)之间的关系如何

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

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

3.模板方法模式练习

这个模式很常见是因为对创建框架来说,这个模式简直棒极了。由框架控制如何做事情,而由你(使用这个框架的人)指定框架算法中每个步骤的细节
3.1 用模板方法排序

需要对数组进行排序,Java数组类的设计者就提供了一个方便的模板方法用来排序。

下面的sort()方法,是Arrays.sort()方法

3.2 对鸭子进行排序

假如我们有一个鸭子的数组需要排序,你要怎么做?数组的排序模板方法已经提供了算法,但是你必须让这个模板方法知道如何比较鸭子。你所要做的事情就是,实现一个compareTo()方法。

sort()的设计者希望这个方法能使用于所有的数组,所以他们把sort()变成是静态的方法,这样一来,任何数组都可以使用这个方法。但是没关系,它使用起来和它被定义在超类中是一样的。现在,还有一个细节要告诉你:因为sort()并不是真正定义在超类中,所以sort()方法需要知道你已经实现了这个compartTo()方法,否则就无法进行排序。

要达到这一点,设计者利用了Comparable接口。你须实现这个接口,提供这个接口所声明的方法,也就是compareTo()。

这个compareTo()方法将比较两个对象,然后返回其中一个是大于、等于还是小于另一个。sort()只要能够知道两个对象的大小,当然就可以进行排序。

比较鸭子

如果要排序鸭子,就必须实现compareTo()方法,然后,数组就可以被正常排序了。

Duck类为:

public class Duck implements Comparable<Duck> {
	String name;
	int weight;
  
	public Duck(String name, int weight) {
		this.name = name;
		this.weight = weight;
	}
 
	@Override
	public String toString() {
		return name + " weighs " + weight;
	}
  
	@Override
	public int compareTo(Duck otherDuck) {
 
  
		if (this.weight < otherDuck.weight) {
			return -1;
		} else if (this.weight == otherDuck.weight) {
			return 0;
		} else { // this.weight > otherDuck.weight
			return 1;
		}
	}
}

测试一些鸭子:

public class DuckSortTestDrive {

	public static void main(String[] args) {
		Duck[] ducks = { 
						new Duck("Daffy", 8), 
						new Duck("Dewey", 2),
						new Duck("Howard", 7),
						new Duck("Louie", 2),
						new Duck("Donald", 10), 
						new Duck("Huey", 2)
		 };

		System.out.println("Before sorting:");
		display(ducks);

		Arrays.sort(ducks);
 
		System.out.println("\nAfter sorting:");
		display(ducks);
	}

	public static void display(Duck[] ducks) {
		for (Duck d : ducks) {
			System.out.println(d);
		}
	}
}

测试输出:

 3.3 总结

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

问:在Java API中,还有其他模板方法的例子吗?
答:是的,你可以在一些地方看到它们。比方说,java.io的InputStream类有一个read()方法,是由子类实现的,而这个方法又会被read(byte b[], int off,int len)模板方法使用。

4.总结

  1. “模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类。
  2. 模板方法模式为我们提供了一种代码复用的重要技巧。
  3. 模板方法的抽象类可以定义具体方法、抽象方法和钩子。
  4. 抽象方法由子类实现。钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它。
  5. 为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
  6. 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用低层模块。
  7. 你将在真实世界代码中看到模板方法模式的许多变体,不要期待它们全都是一眼就可以被你认出的。
  8. 策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
  9. 工厂方法是模板方法的一种特殊版本。
     
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值