小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
大家好,
我是方圆,第一次在掘金上发文章,就以此来纪念,也要求自己保证每篇文章的质量吧!
1. 茶和咖啡的秘方公布给大家
public class Coffee {
/**
* 准备饮品的方法,分以下四步
*/
public void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugar();
}
public void boilWater() {
System.out.println("把水烧开");
}
public void brewCoffeeGrinds() {
System.out.println("用热水冲咖啡");
}
public void pourInCup() {
System.out.println("倒入杯中");
}
public void addSugar() {
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("加点儿柠檬");
}
}
- 我们从这个
秘方中发现,都有把水烧开和倒入杯中的步骤,而另外两个方法则是根据饮料不同,而自己特有的,我们可以把重复的代码抽取出来,而特有的归自己保管。通过前两句我的描述,你能想到什么?

- 我们看下面这张图,就明白了

1.1 还没理解?很正常,我们用代码实现一下
我们先看基类,其中有一个特别的点,我先不说,看您能不能发现。
public abstract class HotDrink {
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
// 以下两个方法,对于茶和咖啡都是相同的
public void boilWater() {
System.out.println("把水烧开");
}
public void pourInCup() {
System.out.println("倒入杯中");
}
// 以下两个方法子类各不相同
public abstract void brew();
public abstract void addCondiments();
}
我们再看看两个实现类,仅是实现了各自所需,没有什么特别
public class Coffee extends HotDrink {
@Override
public void brew() {
System.out.println("用水冲咖啡");
}
@Override
public void addCondiments() {
System.out.println("加点儿糖");
}
}
public class Tea extends HotDrink {
@Override
public void brew() {
System.out.println("热水泡茶");
}
@Override
public void addCondiments() {
System.out.println("加点儿柠檬");
}
}
1.2 揪一揪其中的小细节
- 基类中,prepareRecipe()方法,用了
final修饰符,这个方法不能被子类修改,做饮料,就按照这个模板来(关键字出现了),这就是模板方法模式,其中具体的步骤,根据具体实现类的不同而有具体的差异。
我们写个测试方法测试一下
public class Test {
public static void main(String[] args) {
Tea tea = new Tea();
Coffee coffee = new Coffee();
tea.prepareRecipe();
System.out.println("---------");
coffee.prepareRecipe();
}
}
结果如下

好了,我们大致的了解了,我们把它用官方话说的定义看看,我们能不能从中悟出东西。
- 模板方法模式:在一个方法中定义一个算法的
骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤

通俗了说,模板就是一个方法,这个方法将算法定义成一组步骤,其中任何步骤都可以是抽象的,具体的实现由子类来负责。这样可以保证方法的结构不变,同时子类之间又各有千秋
1.3 这个模板方法模式,有啥优点呐?
- 减少了重复的代码,实现了
代码的复用,如我们的例子中,泡茶和冲咖啡都有一样的步骤 引入新的子类容易,子类只需实现自己特有的步骤就好- 模板基类专注于
方法本身,子类专注于具体的实现步骤
2. 你听说过吗?代码里会有钩子!
- 钩子是啥?钩子是一种被声明在
抽象类中的方法,可以为空实现或有默认实现。
我们在基类里加个钩子看看
public abstract class HotDrink {
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if(customerWantsCondiments()) {
addCondiments();
}
}
...
// 这个就是钩子方法...
public boolean customerWantsCondiments() {
return true;
}
}
这个钩子方法起到的是询问顾客是否需要加配料,现在的默认实现都是需要加
2.1 基类,选择性对钩子进行实现
- 但是我们想知道顾客到底想不想加,所以直接
问他!我们在下面添加了一个询问的方法
public class Coffee extends HotDrink {
@Override
public void brew() {
System.out.println("用水冲咖啡");
}
@Override
public void addCondiments() {
System.out.println("加点儿糖");
}
@Override
public boolean customerWantsCondiments() {
String ans = getCustomerAnswer();
if ("y".equals(ans)) {
return true;
} else if ("n".equals(ans)) {
return false;
} else {
return false;
}
}
/**
* 询问顾客是否需要加糖
*/
private String getCustomerAnswer() {
String ans = null;
System.out.println("你需要加糖吗?(y/n)");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
ans = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
if (ans == null) {
return "n";
}
return ans;
}
}
基类重写了钩子方法,实现了询问的功能,简单测试一下
public class Test {
public static void main(String[] args) {
Coffee coffee = new Coffee();
coffee.prepareRecipe();
}
}
结果如下

2.2 钩子是个什么样儿的存在?
- 钩子的存在
能够作为条件控制,影响抽象类中的算法流程 - 钩子在子类中的实现是可选的,它对于子类来说,多数情况下并不重要
2.3 好莱坞原则
- 基类对实现类之间的原则:基类能对子类进行调用,子类不能调用基类
3. 我们见过的模板方法模式
在AbstractList中有一个get的抽象方法,只是没有定义实现,不过也相当于模板方法中的模板,定义了算法的步骤,具体的实现延迟到子类中

子类ArrayList对此做了实现,如下

注:这个模式的侧重点在于,提供一个
算法模板,我们在开发中遇到的模板方法模式,不一定非要沿袭继承的方法,像java.io中InputStream类的read()方法,也是需要子类来实现的,但是没有用到继承。
4. 总结
- 模板方法模式,定义了算法的步骤,而具体的实现延迟到了子类中
- 模板方法模式,让我们实现了代码复用
- 模板方法的抽象类可以定义具体方法,抽象方法和钩子
- 钩子是一种方法,空实现或默认实现,在子类中可以选择性的重写
- 模板的方法声明为final,防止被子类修改
无限进步