9 Template Method Pattern(模板方法模式)
前言:封裝步驟的算法。
Vander作為老板,凡是親力親為,他新開了家咖啡店,這是他招牌咖啡卡布奇諾的沖泡方法:
1、把水煮沸
2、用沸水沖泡咖啡
3、將咖啡倒入咖啡杯
4、加糖和奶
Vander發(fā)現(xiàn)白天喝咖啡的人實(shí)在是不多,白天的生意很差,白天大家都喜歡喝奶茶,特別是夏天到了,冰涼的奶茶更受歡迎,于是Vander開始研制它的檸檬奶茶。以下是檸檬奶茶的制作方法:
1、把水煮沸
2、用沸水泡茶葉
3、將茶倒入茶杯中
4、加檸檬和奶
Vander如行云流水般寫完了這兩個(gè)制作方法。
咖啡:
public class Coffee {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("Boiling water");
}
public void brewCoffeeGrinds() {
System.out.println("Dripping Coffee throught filter");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
public void addSugarAndMilk() {
System.out.println("Adding sugar and milk");
}
}
奶茶:
public class MilkTea {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addTeaAndMilk();
}
public void boilWater() {
System.out.println("Boiling water");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
public void steepTeaBag() {
System.out.println("Steeping the tea");
}
public void addTeaAndMilk() {
System.out.println("Adding tea and milk");
}
}
他自認(rèn)為寫得很漂亮,請(qǐng)來了Panda大師一起來鑒賞,Panda大師一看,這個(gè)實(shí)現(xiàn)有太多冗余代碼了,首先奶茶跟咖啡制作流程既然是相似的,為什么不做成一個(gè)Beverage抽象類,讓它們來繼承呢,在抽象類中將prepareRecipe方法固定下來,這樣后面的制作的飲料都要遵循這個(gè)流程了,接下來煮水和將飲料倒入杯中實(shí)際上也是一樣的,也能在抽象的父類Beverage中實(shí)現(xiàn),steep(浸泡)和brew(沖泡)實(shí)際上也沒多大區(qū)別,所以給個(gè)新名字brew,最后加入糖和奶跟加入檸檬和奶,也是類似的,也給個(gè)新名字addCondiments。改造完之后類圖如下:
Vander仔細(xì)琢磨了Panda大師的做法,發(fā)現(xiàn)Panda大師實(shí)際上先做了泛化(將實(shí)現(xiàn)泛化成抽象通用),接著再將一些具體的步驟交給子類來完成。Vander想了想其實(shí)prepareRecipe完全可以定義成final。這樣就把這個(gè)傳統(tǒng)的做法定義下來,不允許子類擅自修改流程。
Panda大師一看,不錯(cuò),你終于學(xué)到精髓了,實(shí)際上我們剛剛的做法就是用了模板方法模式。Panda來總結(jié)一下模板方法模式的好處:
1、抽象父類中定義了算法(也就是飲料制作流程),保護(hù)了算法。
2、對(duì)于子類來說,抽象父類的存在將代碼的復(fù)用最大化。
3、算法只存在于一個(gè)地方,方便修改。
4、模板方法提供了一個(gè)框架,可以讓其他飲料插進(jìn)來,新的飲料只要實(shí)現(xiàn)自己的方法就行了。
5、抽象父類專注于方法本身,而由子類提供完整的實(shí)現(xiàn)。
Panda大師指導(dǎo)改造后,代碼成這樣:
Beverage:
public abstract class Beverage {
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
public void boilWater() {
System.out.println("Boiling water");
}
abstract void brew();
public void pourInCup() {
System.out.println("Pouring into cup");
}
abstract void addCondiments();
}
coffee:
public class Coffee extends Beverage {
@Override
public void brew() {
System.out.println("Dripping Coffee throught filter");
}
@Override
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}
Milktea:
public class Milktea extends Beverage {
public void brew() {
System.out.println("Steeping the tea");
}
public void addCondiments() {
System.out.println("Adding tea and milk");
}
}
說了那么多,模板方法模式具體是什么呢?
模板方法模式:在一個(gè)方法中定義一個(gè)算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法中的某些步驟。
實(shí)際上這個(gè)模式是用來創(chuàng)建一個(gè)算法的模板。什么是模板呢,實(shí)際上模板就是一個(gè)方法,更具體地說,這個(gè)方法將算法定義成一組步驟,其中任何步驟都可以是抽象的,由子類負(fù)責(zé)實(shí)現(xiàn)。這可以確保算法的結(jié)構(gòu)保持不變,同時(shí)由子類提供部分實(shí)現(xiàn)。
下面是模板方法模式常用的套路:
這里需要說明的是,抽象類中有個(gè)鉤子方法,這個(gè)鉤子方法是在抽象類中提供了一個(gè)簡(jiǎn)單的實(shí)現(xiàn),子類可以選擇去覆蓋它重新實(shí)現(xiàn),也可以使用父類的實(shí)現(xiàn)。
有了Panda大師度身定制咖啡和奶茶之后,添加其他飲料更加方便了,很快就加入了蔬菜汁等飲料。但是有些客人不喜歡喝帶奶的卡布奇諾,他喜歡喝純咖啡。Vander想了想,能不能讓客戶告訴我要不要加奶和糖,我在制作的時(shí)候就可以按照客戶的請(qǐng)求來完成了,實(shí)際上這是任何一個(gè)奶茶店和咖啡店都需要有的基本功能。Vander是這么改造的:
飲料父類:
public abstract class Beverage {
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if(customCondiments()) {
addCondiments();
}
}
public void boilWater() {
System.out.println("Boiling water");
}
abstract void brew();
public void pourInCup() {
System.out.println("Pouring into cup");
}
abstract void addCondiments();
public boolean customCondiments() {
return true;
}
}
咖啡類:
public class Coffee extends Beverage {
@Override
public void brew() {
System.out.println("Dripping Coffee throught filter");
}
@Override
void addCondiments() {
System.out.println("Adding sugar and milk");
}
public boolean customCondiments() {
String answer = getUserInput();
if(answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.println("would you like some milk and sugar with your coffee (y/n)? ");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
try {
answer = bufferedReader.readLine();
} catch (IOException e) {
System.err.println("IO error trying to read your answer");
}
if(answer == null) {
return "no";
}
return answer;
}
}
奶茶類:
public class MilkTea extends Beverage {
public void brew() {
System.out.println("Steeping the tea");
}
public void addCondiments() {
System.out.println("Adding sugar and milk");
}
public boolean customCondiments() {
String answer = getUserInput();
if(answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.println("would you like some milk and sugar with your tea (y/n)? ");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
try {
answer = bufferedReader.readLine();
} catch (IOException e) {
System.err.println("IO error trying to read your answer");
}
if(answer == null) {
return "no";
}
return answer;
}
}
實(shí)現(xiàn)的效果:
下面再說明一下鉤子算法的目的:
1、讓子類實(shí)現(xiàn)算法中的可選部分
2、能夠讓子類有機(jī)會(huì)對(duì)模板方法中某些即將發(fā)生的步驟作出反應(yīng)(如在上面的咖啡類中,完全可以讓父類來詢問是否加入佐料,而子類再進(jìn)行加入佐料后的一些附加操作)。
還要注意的是,在寫模板方法時(shí),不要將算法的步驟切割得太細(xì)(子類要實(shí)現(xiàn)太多操作),也不要步驟太少(會(huì)沒有彈性),所以要看情況折衷。步驟可選的時(shí)候就使用鉤子方法。
模板方法模式跟一個(gè)設(shè)計(jì)原則很吻合,這個(gè)設(shè)計(jì)原則就是好萊塢原則——?jiǎng)e調(diào)用(打電話給)我們,我們會(huì)調(diào)用(打電話給)你。好萊塢原則可以防止“依賴腐敗”,什么是依賴腐敗呢,當(dāng)高層組件依賴底層組件,而底層組件又依賴高層組件,而高層組件又依賴邊側(cè)組件,邊側(cè)組件還依賴于底層組件的時(shí)候依賴腐敗就發(fā)生了,這種情況很難搞清楚系統(tǒng)是如何設(shè)計(jì)的。我們?cè)试S底層組件將自己掛鉤到系統(tǒng)上,但是高層組件會(huì)決定什么時(shí)候和如何使用這些組件。換句話說:高層組件對(duì)待底層組建的方式就是“別調(diào)用我們,我們會(huì)調(diào)用你”。實(shí)際上不只是模板方法模式,工廠模式和觀察者模式也采用了好萊塢原則。
好萊塢原則 VS 依賴倒置原則
先對(duì)比一下策略模式和模板方法模式:
策略模式:將算法定義成對(duì)象,其他對(duì)象通過委托的方式來讓這些算法用起來。
模板方法模式:定義算法的一個(gè)大綱,算法中的個(gè)別步驟可以有不同的實(shí)現(xiàn)細(xì)節(jié),會(huì)重復(fù)使用的代碼可以放進(jìn)超類中,好讓所有的子類共享。
模板方法模式 VS 策略模式 VS 工廠方法模式
模式 | 敘述 |
---|---|
模板方法 | 子類決定如何實(shí)現(xiàn)算法中的步驟 |
策略 | 封裝可互換的行為,然后使用委托來決定要采用哪一個(gè)行為 |
工廠方法 | 由子類決定實(shí)例化哪個(gè)具體類 |
下面我們要進(jìn)入Java中的模板方法,模板方法并非剛剛舉的例子那樣明顯,有些隱藏得頗深,讓我們來細(xì)細(xì)挖掘。
Java數(shù)組類的設(shè)計(jì)者給出了一個(gè)排序的算法,但是排序的方法中compareTo方法卻是需要我們自身實(shí)現(xiàn)的,因?yàn)樵O(shè)計(jì)者無法知道你想按照什么東西來進(jìn)行排序,你必須告訴這個(gè)排序算法你依照什么來進(jìn)行排序的。
接下來看看Sun的源碼:
Comparable接口:
public interface Comparable<T> {
public int compareTo(T o);
}
Arrays的相關(guān)函數(shù):
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
/** To be removed in a future release. */
private static void legacyMergeSort(Object[] a) {
Object[] aux = a.clone();
mergeSort(aux, a, 0, a.length, 0);
} private static void mergeSort(Object[] src,
Object[] dest,
int low,
int high,
int off) {
int length = high - low;
// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low &&
((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
swap(dest, j, j-1);
return;
}
// Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += off;
high += off;
int mid = (low + high) >>> 1;
mergeSort(dest, src, low, mid, -off);
mergeSort(dest, src, mid, high, -off);
// If list is already sorted, just copy from src to dest. This is an
// optimization that results in faster sorts for nearly ordered lists.
if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
System.arraycopy(src, low, dest, destLow, length);
return;
}
// Merge sorted halves (now in src) into dest
for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
dest[i] = src[p++];
else
dest[i] = src[q++];
}
}
/**
* Swaps x[a] with x[b].
*/
private static void swap(Object[] x, int a, int b) {
Object t = x[a];
x[a] = x[b];
x[b] = t;
}
具體的排序細(xì)節(jié)見博客中數(shù)據(jù)結(jié)構(gòu)的MergeSort,這里關(guān)注的是Arrays的Sort方法會(huì)調(diào)用數(shù)組的compareTo來進(jìn)行比較,比較之后再進(jìn)行位置的對(duì)調(diào),這實(shí)際上就是讓子類數(shù)組來實(shí)現(xiàn)具體的算法細(xì)節(jié)(即比較),而父類數(shù)組完成算法的步驟(即排序)。
另外,sort()模板方法實(shí)現(xiàn)不使用繼承,sort方法被定義成一個(gè)靜態(tài)的方法,在運(yùn)行時(shí)和Comparable組合,如果類不實(shí)現(xiàn)Comparable接口的話就會(huì)導(dǎo)致sort方法中類型轉(zhuǎn)換失敗。
下面再看一個(gè)模板方法的實(shí)例JFrame,里面的paint方法是一個(gè)鉤子方法,默認(rèn)情況下是不做事情的,要是你繼承了JFrame并且實(shí)現(xiàn)了paint方法,它就會(huì)進(jìn)行相應(yīng)的操作。
MyFrame類:
public class MyFrame extends JFrame {
public MyFrame(String title) {
super(title);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(300, 300);
this.setVisible(true);
}
public void paint(Graphics graphics) {
super.paint(graphics);
String msg = "09-Template-Method-Pattern";
graphics.drawString(msg, 80, 150);
}
}
Main:
public class Main {
public static void main(String args[]) {
MyFrame frame = new MyFrame("Design Pattern");
}
}
我們查看堆棧信息發(fā)現(xiàn),paint方法也相當(dāng)于作為一個(gè)步驟被其它算法調(diào)用。
模板方法定義了算法的步驟,把這些步驟實(shí)現(xiàn)延遲到了子類,它是一種代碼復(fù)用的技巧。
好萊塢原則告訴我們,將決策權(quán)放在高層模塊中,以便決定如何以及何時(shí)調(diào)用低層模塊。
策略模式和模板方法模式都封裝算法,一個(gè)用組合,一個(gè)用繼承。工廠方法是模板方法的一種特殊版本。
最后又到了喜聞樂見的總結(jié)部分,我們又來總結(jié)我們現(xiàn)在現(xiàn)有的設(shè)計(jì)模式武器。
面向?qū)ο蠡A(chǔ)
抽象、封裝、多態(tài)、繼承
八大設(shè)計(jì)原則
設(shè)計(jì)原則一:封裝變化
設(shè)計(jì)原則二:針對(duì)接口編程,不針對(duì)實(shí)現(xiàn)編程
設(shè)計(jì)原則三:多用組合,少用繼承
設(shè)計(jì)原則四:為交互對(duì)象之間的松耦合設(shè)計(jì)而努力
設(shè)計(jì)原則五:對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉
設(shè)計(jì)原則六:依賴抽象,不要依賴于具體的類
設(shè)計(jì)原則七:只和你的密友談話
設(shè)計(jì)原則八:別找我,我有需要會(huì)找你
模式
**模板方法模式:在一個(gè)方法中定義一個(gè)算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法中的某些步驟。 ****