模板方法模式
冲泡咖啡
有些人没有咖啡就活不下去,有的人则离不开茶,两者的共同成分是什么?当然是咖啡因了。不仅仅如此,他们的冲泡方法都一样。咖啡的冲泡步骤是:
- 把水煮沸
- 用沸水冲泡咖啡
- 倒进杯子
- 加糖和牛奶
而冲茶的步骤是:
-
把水煮沸
-
用沸水冲泡茶叶
-
倒进杯子
-
加柠檬
V1版
让我们快速搞定V1版本的咖啡和茶制作。
咖啡类
/**
* 咖啡
*/
public class CoffeeV1 {
public void prepareRecipe(){
boilWater(); //把水煮沸
brewCoffeeGrinds(); //用沸水冲泡咖啡
pourInCup(); //把咖啡到入杯子
addSugarAndMilk(); //加糖和加奶
}
private void boilWater() {
System.out.println("把水煮沸");
}
private void brewCoffeeGrinds() {
System.out.println("用沸水冲泡咖啡");
}
private void pourInCup() {
System.out.println("到入杯子");
}
private void addSugarAndMilk() {
System.out.println("加糖和加奶");
}
}
茶类
/**
* 茶
*/
public class TeaV1 {
public void prepareRecipe(){
boilWater(); //把水煮沸
steepTeaBag(); //用沸水冲泡茶
pourInCup(); //把茶到入杯子
addLemon(); //加柠檬
}
private void boilWater() {
System.out.println("把水煮沸");
}
private void steepTeaBag() {
System.out.println("用沸水冲泡茶");
}
private void pourInCup() {
System.out.println("到入杯子");
}
private void addLemon() {
System.out.println("加柠檬");
}
}
测试
/**
* 测试
*/
public class BeverageMainV1 {
public static void main(String[] args) {
CoffeeV1 coffee = new CoffeeV1();
coffee.prepareRecipe();
System.out.println();
TeaV1 tea = new TeaV1();
tea.prepareRecipe();
}
}
在咖啡类和茶类中,明显有代码重复,当你发觉有重复的代码出现时,就是给你一个信号,要重构你的代码了。设计模式通常都是在重构代码的时候再引入的。
咖啡类和茶类的重复代码需要抽取出来放到一个地方,这儿我们从咖啡类和茶类抽象出饮料基类,将公共的代码放到饮料基类中。
你可以想象下,如果不能抽象出基类时,又该怎么消除重复的代码?
V2版
基于这个抽象思路,我们来实现V2版
公共的饮料基类,为了不让直接实例化,定义为abstract的。
/**
* 饮料基类
*/
public abstract class BaseBeverageV2 {
protected void boilWater() {
System.out.println("把水煮沸");
}
protected void pourInCup() {
System.out.println("到入杯子");
}
}
V2版咖啡类,继承饮料基类
/**
* 咖啡
*/
public class CoffeeV2 extends BaseBeverageV2{
public void prepareRecipe(){
boilWater(); //把水煮沸
brewCoffeeGrinds(); //用沸水冲泡咖啡
pourInCup(); //把咖啡到入杯子
addSugarAndMilk(); //加糖和加奶
}
private void brewCoffeeGrinds() {
System.out.println("用沸水冲泡咖啡");
}
private void addSugarAndMilk() {
System.out.println("加糖和加奶");
}
}
同样,V2版茶类继承饮料基类
/**
* 茶
*/
public class TeaV2 extends BaseBeverageV2{
public void prepareRecipe(){
boilWater(); //把水煮沸
steepTeaBag(); //用沸水冲泡茶
pourInCup(); //把茶到入杯子
addLemon(); //加柠檬
}
private void steepTeaBag() {
System.out.println("用沸水冲泡茶");
}
private void addLemon() {
System.out.println("加柠檬");
}
}
测试
public class BeverageMainV2 {
public static void main(String[] args) {
CoffeeV2 coffee = new CoffeeV2();
coffee.prepareRecipe();
System.out.println();
TeaV2 tea = new TeaV2();
tea.prepareRecipe();
}
}
嗯,再看一眼,发现咖啡和饮料还有共同点,他们都有prepareRecipe方法,并且步骤差不多,能不能将prepareRecipe方法也放到基类中,我们合并下V2版本中两个子类的prepareRecipe依赖的方法,一共有6个,其中brewCoffeeGrinds()和steepTeaBag()两个方法都是冲泡含义,重命名为brew(),同样addLemon()和addSugarAndMilk()类似,都是加调料,重名为一个addCondiments().
这样prepareRecipe方法中会依赖4个方法,有两个方法是基类已经实现了的,brew()和ddCondiments()因为只有子类知道如何处理,定义为抽象方法。
V3版
看看V3版实现,饮料基类中定义了prepareRecipe方法,并且定义了它依赖的4个步骤,有的步骤基类中就可以实现,而有的步骤需要子类实现。
/**
* 饮料基类
*/
public abstract class BaseBeverageV3 {
/**
* 模板方法
*/
public void prepareRecipe(){
boilWater(); //把水煮沸
brew(); //用沸水冲泡
pourInCup(); //到入杯子
addCondiments(); //加调料
}
protected abstract void brew();
protected abstract void addCondiments();
protected void boilWater() {
System.out.println("把水煮沸");
}
protected void pourInCup() {
System.out.println("到入杯子");
}
}
咖啡子类,实现基类中的抽象方法
/**
* 咖啡
*/
public class CoffeeV3 extends BaseBeverageV3{
@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡");
}
@Override
protected void addCondiments() {
System.out.println("加糖和加奶");
}
}
同理,茶子类,也实现基类中的抽象方法
/**
* 茶
*/
public class TeaV3 extends BaseBeverageV3{
protected void brew() {
System.out.println("用沸水冲泡茶");
}
protected void addCondiments() {
System.out.println("加柠檬");
}
}
测试
public class BeverageMainV3 {
public static void main(String[] args) {
CoffeeV3 coffee = new CoffeeV3();
coffee.prepareRecipe();
System.out.println();
TeaV3 tea = new TeaV3();
tea.prepareRecipe();
}
}
在V3实现中,我们就基本上使用模板方法模式来重构了我们的代码,消除代码重复。
V3代码的UML图
定义
好了,真是认识下模板方法设计模式,模板方法定义了(实现某个功能所需要的)算法的步骤,允许子类提供其中某些步骤的实现。
通常为了不让子类有机会覆盖基类的算法骨架,模板方法定义为final的。
用代码实现下定义吧
抽象类
/**
* 包含了模板方法的抽象类
*/
public abstract class AbstractClass {
/**
* final的模板方法
*/
public final void templateMethod(){
primitiveOperation1(); //原语操作1
primitiveOperation2(); //原语操作2
concreteOperation(); //具体操作
}
protected abstract void primitiveOperation1();
protected abstract void primitiveOperation2();
private void concreteOperation() {
System.out.println("抽象类中,具体的操作。。。");
}
}
具体类
/**
* 具体类 - 实现算法某些步骤
*/
public class ConcreteClass extends AbstractClass {
@Override
protected void primitiveOperation1() {
System.out.println("实现原语操作1....");
}
@Override
protected void primitiveOperation2() {
System.out.println("实现原语操作2....");
}
}
测试
public class TemplateMethodMain {
public static void main(String[] args) {
AbstractClass abstractClass = new ConcreteClass();
abstractClass.templateMethod();
}
}
看看UML图
在上面模板方法定义中,模板方法中包含两种方法,一种是抽象的原语方法,其需要子类提供实现的;还要一类是基类中提供实现的方法,可直接使用,当然,如果不希望子类覆盖,也可以将这类方法定义为final的。第二类方法,有一些特例,比如空实现、默认实现的方法,子类可以选择覆盖或者不覆盖,区别于抽象方法,子类必须实现,这种用作特殊用途的方法,我们称之为钩子方法。
钩子方法
比如,在我们的饮料示例中,我们可以在模板方法中应用钩子,由子类来决定是否要加调料,来看看效果。
/**
* 饮料基类
*/
public abstract class BaseBeverageV4 {
/**
* 模板方法
*/
public final void prepareRecipe(){
boilWater(); //把水煮沸
brew(); //用沸水冲泡
pourInCup(); //到入杯子
if(wantsCondiments()) {
addCondiments(); //加调料
}
}
/**
* 子类可以选择是否覆盖该方法,控制模板方法中是否要执行加调料步骤
* @return
*/
protected boolean wantsCondiments() {
return true;
}
protected abstract void brew();
protected abstract void addCondiments();
protected final void boilWater() {
System.out.println("把水煮沸");
}
protected final void pourInCup() {
System.out.println("到入杯子");
}
}
wantsCondiments方法,就是我们说的钩子方法。当然你没必要记住叫什么方法,可以感受和理解这种使用方式即可。
接下来,咖啡类,不覆盖钩子方法,即使用默认实现
/**
* 咖啡
*/
public class CoffeeV4 extends BaseBeverageV4{
@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡");
}
@Override
protected void addCondiments() {
System.out.println("加糖和加奶");
}
}
而茶覆盖了钩子方法,不加入调料
public class TeaV4 extends BaseBeverageV4{
@Override
protected boolean wantsCondiments() {
return false;
}
protected void brew() {
System.out.println("用沸水冲泡茶");
}
protected void addCondiments() {
System.out.println("加柠檬");
}
}
测试
public class BeverageMainV4 {
public static void main(String[] args) {
CoffeeV4 coffee = new CoffeeV4();
coffee.prepareRecipe();
System.out.println();
TeaV4 tea = new TeaV4();
tea.prepareRecipe();
}
}
输出
把水煮沸
用沸水冲泡咖啡
到入杯子
加糖和加奶
把水煮沸
用沸水冲泡茶
到入杯子
可以看出,模板方法模式在实际使用中,是非常灵活的,怎么顺手怎么来。
扩展示例
在看一个数据处理示例,示例参考: https://java2blog.com/template-method-design-pattern-in-java/
通常数据处理的整体步骤是,读取数据,处理数据,写数据。这个示例中,演示从两种数据源,数据库和csv文件读取数据,然后处理数据,最后写CSV文件。
抽象的数据处理器,里面定义了数据处理的模板方法:
/**
* 抽象的数据处理器,定义数据处理的模板方法
*/
public abstract class DataParser {
//Template method
//This method defines a generic structure for parsing data
public void parseDataAndGenerateOutput() {
readData();
processData();
writeData();
}
//This methods will be implemented by its subclass
abstract void readData();
abstract void processData();
//We have to write output in a CSV file so this step will be same for all subclasses
public void writeData() {
System.out.println("Output generated,writing to CSV");
}
}
CSVDataParser实现
public class CSVDataParser extends DataParser {
@Override
void readData() {
System.out.println("Reading data from csv file");
}
@Override
void processData() {
System.out.println("Looping through loaded csv file");
}
}
DatabaseDataParser实现
public class DatabaseDataParser extends DataParser {
@Override
void readData() {
System.out.println("Reading data from database");
}
@Override
void processData() {
System.out.println("Looping through datasets");
}
}
测试
public class DataParserMain {
public static void main(String[] args) {
CSVDataParser csvDataParser=new CSVDataParser();
csvDataParser.parseDataAndGenerateOutput();
System.out.println("**********************");
DatabaseDataParser databaseDataParser=new DatabaseDataParser();
databaseDataParser.parseDataAndGenerateOutput();
}
}
数据处理的UML图
模板方法在通常框架中使用较多,比如HttpServlet。
说到这里,从代码调用的角度,框架和库的区别是?框架调用你的代码,你的代码调用库API。