模板方法模式:由子类决定如何实现父类算法中的哪一步。
例子:咖啡机自动冲咖啡,但是我们可能做卡布奇诺(Cappuccino),也可能做焦糖玛奇朵(CaramelMacchiato),但是总体来说步骤一致,只是最后加入的其他材料不太一样。
冲咖啡的步骤:获得咖啡豆(getBeans()),放入咖啡机(putIntoCoffeemaker()),磨碎咖啡豆(grindBeans()),冲咖啡(makeCoffee())。
假设有这么一个抽象类,AutoCoffee,它的代码如下:
public abstract class AutoCoffee {
//获得咖啡豆
protected void getBeans(){
System.out.println("获得咖啡豆");
}
//放入咖啡机
protected void putIntoCoffeemaker(){
System.out.println("放入咖啡机");
}
//磨碎咖啡豆
protected void grindBeans(){
System.out.println("磨碎咖啡豆");
}
//冲咖啡
protected void makeCoffee(){
System.out.println("冲咖啡");
}
//自动冲咖啡
final public void autoMake(){
this.getBeans();
this.putIntoCoffeemaker();
this.grindBeans();
this.makeCoffee();
}
}
客户端不用关心自动冲咖啡内部的步骤如何实现,只需要调用autoMake()方法就够了,既然这样,我们的步骤方法,都设为protected。
然后,我们需要一个设定,让咖啡机自动冲卡布奇诺,只需要在最后加入牛奶和奶泡就好了:
Cappuccino.java
public class Cappuccino extends AutoCoffee {
@Override
public void makeCoffee() {
super.makeCoffee();
System.out.println("加入牛奶和奶泡");
}
}
再来一个设定,自动冲焦糖玛奇朵,只需要在最后加入牛奶和焦糖:
CaramelMacchiato.java
public class CaramelMacchiato extends AutoCoffee {
@Override
public void makeCoffee() {
super.makeCoffee();
System.out.println("加入牛奶和焦糖");
}
}
OK,这样,来一个客户端:
Clinet.java
public class Clinet {
public static void main(String[] args) {
AutoCoffee cappuccino = new Cappuccino();
AutoCoffee caramelMacchiato = new CaramelMacchiato();
cappuccino.autoMake();
caramelMacchiato.autoMake();
}
}
输出结果:
获得咖啡豆
放入咖啡机
磨碎咖啡豆
冲咖啡
加入牛奶和奶泡
---------------------
获得咖啡豆
放入咖啡机
磨碎咖啡豆
冲咖啡
加入牛奶和焦糖
以上就是模板方法。是的,就是这么简单,或许你有疑问:如果子类需要更改冲咖啡的过程,或者需要稍微不同怎么办?
针对第一种情况,如果真的很不一样,需要大改,几近完全不同,那么,你不应该继承AutoCoffee这个抽象类,而是写一个类。但是如果只是稍微不同,那么我们就引用钩子(hook)的概念。
假设,我们的磨碎咖啡豆这一步骤,有手摇和电动两种选择,无论是卡布奇诺还是焦糖玛奇朵,我们都采用手动,但是实现方式不一样,我们可以这样编写抽象类AutoCoffee
public abstract class AutoCoffee {
private boolean flag = true;
//获得咖啡豆
protected void getBeans(){
System.out.println("获得咖啡豆");
}
//放入咖啡机
protected void putIntoCoffeemaker(){
System.out.println("放入咖啡机");
}
//钩子方法
protected boolean isMotorDriven(){
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
//磨碎咖啡豆
protected void grindBeansByMotorDriven(){
System.out.println("电动磨碎咖啡豆");
}
protected void grindBeansByHand(){
System.out.println("手动磨碎咖啡豆");
}
//冲咖啡
protected void makeCoffee(){
System.out.println("冲咖啡");
}
//自动冲咖啡
final public void autoMake(){
this.getBeans();
this.putIntoCoffeemaker();
if(isMotorDriven()){
this.grindBeansByMotorDriven();
}else{
this.grindBeansByHand();
}
this.makeCoffee();
}
}
新增加一个属性:flag和setter方法,以及判断flag的isMotorDriven()方法,grindBeans()变为两个方法,一个是grindBeansByMotorDriven()电动磨碎咖啡豆,一个是grindBeansByHand()手动磨碎咖啡豆。autoMake()修改为判断isMotorDriven(),根据条件不同,选择是手动还是电动。flag为true,默认为电动。
子类实现:
public class Cappuccino extends AutoCoffee {
//设为手动
@Override
protected boolean isMotorDriven() {
return false;
}
@Override
public void makeCoffee() {
super.makeCoffee();
System.out.println("加入牛奶和奶泡");
}
}
public class CaramelMacchiato extends AutoCoffee {
@Override
public void makeCoffee() {
super.makeCoffee();
System.out.println("加入牛奶和焦糖");
}
}
Clinet.java
public class Clinet {
public static void main(String[] args) {
AutoCoffee cappuccino = new Cappuccino();
AutoCoffee caramelMacchiato = new CaramelMacchiato();
cappuccino.autoMake();
System.out.println("------------------");
caramelMacchiato.setFlag(false);
caramelMacchiato.autoMake();
}
}
卡布奇诺重写isMotorDriven()方法,返回false(而非flag属性),这样就只能是手动,客户端无法自己选择(即使调用setFlag()也不能改变),而焦糖玛奇朵是可以通过setFlag()来设置是手动还是电动。
结果如下:
获得咖啡豆
放入咖啡机
手动磨碎咖啡豆
冲咖啡
加入牛奶和奶泡
------------------
获得咖啡豆
放入咖啡机
手动磨碎咖啡豆
冲咖啡
加入牛奶和焦糖
模板方法还体现了一个重要的设计原则:好莱坞原则。但是很多人搞不清楚好莱坞原则和依赖倒置原则,改天另开博客,详述好莱坞原则、依赖倒置原则、控制反转和依赖注入的区别。