设计模式——模板方法模式

模板方法模式

1、定义

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

这个定义还是比较好理解的,由父类定义定义一个模板,这里的模板就是一个方法,它将算法定义成一组步骤,其中任何步骤都可以是抽象的,由子类负责实现。通过模板方法模式,可以确保算法的结构保持不变,同时由子类提供部分实现。下面我将通过具体实例来讲解一下模板方法模式。

2、模板方法模式

2.1、需求

假设我们需要模拟饮品店准备饮品的过程,例如在准备咖啡时,分为将水煮沸、用沸水冲泡咖啡、将咖啡倒入杯子、添加糖和牛奶四个步骤;准备茶时,分为将水煮沸、用沸水浸泡茶叶、把茶倒入杯子、添加柠檬四个步骤。

2.2、第一种实现方式

第一眼看到需求,我们很容易就可以想到,要模拟准备饮品的过程,我们只需要针对每种饮品定义一个类,在每个类中编写准备的过程即可,所以我们可以分别定义咖啡类Coffee和茶类Tea,它们的实现方式如下:

/**咖啡类*/
public class Coffee {
	public void prepareRecipe() {
		boilWater();
		brewCoffeeGrinds();
		pourInCup();
		addSugarAndMilk();
	}
	
	public void boilWater() {
		System.out.println("把水煮沸");
	}
	
	public void brewCoffeeGrinds() {
		System.out.println("用沸水冲泡咖啡");
	}
	
	public void pourInCup() {
		System.out.println("把咖啡倒进杯子");
	}
	
	public void addSugarAndMilk() {
		System.out.println("加糖和牛奶");
	}	
}
//-------------------------------------------
/**茶类*/
public class Tea {
	public void prepareRecipe() {
		boilWater();
		steepTeaBag();
		pourInCup();
		addLemon();
	}
	
	public void boilWater() {
		System.out.println("把水煮沸");
	}
	
	public void steepTeaBag() {
		System.out.println("用沸水浸泡茶叶");
	}
	
	public void pourInCup() {
		System.out.println("把茶倒进杯子");
	}
	
	public void addLemon() {
		System.out.println("加柠檬");
	}
}

在这两个类中我们分别定义了一个准备饮品的方法,这个方法中包含了准备饮品的每个步骤,并各自实现了所有的步骤,按照这个思路,以后如果有新的饮品出现,我们只需要定义新的饮品类即可。但是仔细观察我们定义的两个类可以发现,这两个类都有准备饮品的方法,我们是否可以把它抽象出来?另外,准备饮品的步骤中,咖啡和茶的第一步均为“将水煮沸”、第三步均为“将XX倒进杯子”,这两个步骤是一样的,我们是否也能将它们抽出来呢?

2.3、改进后的实现方式

依照面向对象的编程思想,我们可以将饮品类抽象出一个父类,这个父类包含一个准备饮品的抽象方法(因为所有饮品都必须实现这个方法,但是实现方式不一定相同)和所有饮品的公共方法,我们可以按照如下的方式进行定义:

/**所有咖啡因饮料的公共父类*/
public abstract class CaffeineBeverage {
	public abstract void prepareRecipe();
	
	public void boilWater() {
		System.out.println("把水煮沸");
	}
		
	public void pourInCup() {
		System.out.println("倒进杯子");
	}		
}

有了这个父类之后我们可以定义它的子类Coffee和Tea:

/**咖啡类*/
public class Coffee extends CaffeineBeverage{
	public void prepareRecipe() {
		boilWater();
		brewCoffeeGrinds();
		pourInCup();
		addSugarAndMilk();
	}
	
	public void brewCoffeeGrinds() {
		System.out.println("用沸水冲泡咖啡");
	}
	
	public void addSugarAndMilk() {
		System.out.println("加糖和牛奶");
	}
}
//-------------------------------------------
/**茶类*/
public class Tea extends CaffeineBeverage{
	public void prepareRecipe() {
		boilWater();
		steepTeaBag();
		pourInCup();
		addLemon();
	}
	
	public void steepTeaBag() {
		System.out.println("用沸水浸泡茶叶");
	}
	
	public void addLemon() {
		System.out.println("加柠檬");
	}
}

这两个类分别实现了准备饮品方法,同时还自己定义了两个独有的方法,在准备饮品方法中,它们同时调用了父类和子类的步骤。

通过抽象和继承能够我们很好的实现需求,但是有没有办法再进行优化呢?我们继续观察咖啡类和茶类,这两个类自行定义了两个方法,这两个方法是相互不同的,但是通过观察我们发现,冲泡咖啡和浸泡茶叶是不是可以合并成一个方法,例如加入原料这样的方法呢?最后一个方法是加糖和牛奶或者加柠檬,是否也可以合并成一个方法,例如加入辅料这样的方法呢?这样我们可以将准备饮品的所有的步骤都抽象到父类中了,父类可以直接定义一个不可变的准备饮品方法,在这个方法中定义一系列的步骤,子类无需实现这个方法,需要准备饮品时直接调用父类的方法即可,但是子类可以自行实现其中的一些步骤,这样可以满足自己的特殊需求。我们可以通过模板方法模式来实现需求。

2.4、模板方法模式的实现方式

我们修改父类的定义:

public abstract class CaffeineBeverage {
	public final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
		if(customerWantsCondiments()) {
			addCondiments();
		}
	}
	
	public void boilWater() {
		System.out.println("把水煮沸");
	}
		
	public void pourInCup() {
		System.out.println("倒进杯子");
	}
	
	public abstract void brew();
	
	public abstract void addCondiments();
	
	public boolean customerWantsCondiments() {
		return true;
	}	
}

我们先看一下这个类的定义,我们首先定义了一个准备饮品的方法prepareRecipe(),这个方法中调用了准备饮品的一系列步骤,同时我们将prepareRecipe()方法定义为final,这样子类就不能覆盖这个方法了。然后我们在父类中定义了所有准备饮品的步骤,这些步骤有些是已经实现了的,有些步骤未实现,未实现的步骤可由子类自行实现。另外,我们还定义了一个方法customerWantsCondiments(),这个方法返回的是一个布尔值,在这个类中我们将这个方法的返回定义为恒定的值true,这个方法的使用我们会在后续进行详细说明。

接下来我们定义咖啡类和茶类:

/**咖啡类*/
public class Coffee extends CaffeineBeverage {
	@Override
	public void brew() {
		System.out.println("用沸水冲泡咖啡");
	}

	@Override
	public void addCondiments() {
		System.out.println("加糖和牛奶");
	}
	
	public boolean customerWantsCondiments() {
		String answer = getUserInput();
		
		if(answer.toLowerCase().startsWith("y")) {
			return true;
		} else {
			return false;
		}
	}
	
	private String getUserInput() {
		String answer = null;
		
		System.out.println("请问是否需要添加糖和牛奶?(y/n)");
		BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		
		try {
			answer = in.readLine();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		if(answer == null) {
			return "no";
		}
		
		return answer;
	}
}
//---------------------------------
/**茶类*/
public class Tea extends CaffeineBeverage{
	@Override
	public void brew() {
		System.out.println("用沸水浸泡茶叶");		
	}

	@Override
	public void addCondiments() {
		System.out.println("加柠檬");		
	}	
}

可以看到,咖啡类和茶类都实现了brew()方法和addCondiments()方法,但是咖啡类还重写了customerWantsCondiments()方法,这个方法用于根据用户输入判断是否需要添加辅料,根据父类中的步骤定义,只有用户需要添加辅料的时候才会调用添加辅料方法,但是茶类中并没有重写这个方法,也就是说,茶类使用的是父类定义的customerWantsCondiments()方法,也就是永远返回true,这样一来,茶类在准备饮品的时候会一直添加柠檬。在模板方法模式中,customerWantsCondiments()这样的方法被称为钩子,它在父类定义中通常什么都不做,或者有一个默认的实现,子类可以覆盖这个方法,也可以不覆盖。

在模板方法模式中,我们将需要子类实现的步骤定义为抽象方法,钩子也是需要子类实现的,那么我们什么时候使用抽象方法什么时候使用钩子呢?当子类必须要实现算法中某个步骤的时候,就可以使用抽象方法,当某个步骤是可选的,那么就可以使用钩子。

那么我们为什么要使用钩子呢?钩子可以让子类实现算法中的可选部分,同时钩子能让子类有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做出反应,另外,钩子也让子类有能力为其抽象类做出一些决定,就像我们上面示例中,我们通过钩子来让用户决定是否需要添加辅料。

最后,我们来看一下模板方法模式的类图(类图摘自《Head First设计模式》):

设计模式——模板方法模式

父类中包含了一个模板方法,这个模板方法实现了算法的过程,但是算法的步骤父类并没有全部实现,可以由子类来实现一个或多个步骤。

3、总结

模板方法模式定义了算法的步骤,并将这些步骤的实现延迟到了子类,它为我们提供了一个代码复用的重要技巧。在模板方法模式的抽象类中,我们可以定义具体方法、抽象方法和钩子,其中抽象方法需要由子类实现,子类可以选择修改或者不修改钩子。模板方法模式在实际中应用很多,而且很多时候我们会遇到模板方法的变体,这就需要我们自己有能力进行分析辨别。