模板方法模式:由子類決定如何實現父類算法中的哪一步。
例子:咖啡機自動衝咖啡,但是我們可能做卡布奇諾(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()來設置是手動還是電動。
結果如下:
獲得咖啡豆
放入咖啡機
手動磨碎咖啡豆
衝咖啡
加入牛奶和奶泡
------------------
獲得咖啡豆
放入咖啡機
手動磨碎咖啡豆
衝咖啡
加入牛奶和焦糖
模板方法還體現了一個重要的設計原則:好萊塢原則。但是很多人搞不清楚好萊塢原則和依賴倒置原則,改天另開博客,詳述好萊塢原則、依賴倒置原則、控制反轉和依賴注入的區別。