9 Template Method Pattern(模板方法模式)
前言:封裝步驟的算法矮燎。
Vander作為老板,凡是親力親為,他新開了家咖啡店舞虱,這是他招牌咖啡卡布奇諾的沖泡方法:
1、把水煮沸
2母市、用沸水沖泡咖啡
3矾兜、將咖啡倒入咖啡杯
4、加糖和奶
Vander發(fā)現(xiàn)白天喝咖啡的人實在是不多患久,白天的生意很差椅寺,白天大家都喜歡喝奶茶,特別是夏天到了蒋失,冰涼的奶茶更受歡迎配并,于是Vander開始研制它的檸檬奶茶。以下是檸檬奶茶的制作方法:
1高镐、把水煮沸
2溉旋、用沸水泡茶葉
3、將茶倒入茶杯中
4嫉髓、加檸檬和奶
Vander如行云流水般寫完了這兩個制作方法观腊。
咖啡:
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)為寫得很漂亮,請來了Panda大師一起來鑒賞算行,Panda大師一看梧油,這個實現(xiàn)有太多冗余代碼了,首先奶茶跟咖啡制作流程既然是相似的州邢,為什么不做成一個Beverage抽象類儡陨,讓它們來繼承呢,在抽象類中將prepareRecipe方法固定下來量淌,這樣后面的制作的飲料都要遵循這個流程了骗村,接下來煮水和將飲料倒入杯中實際上也是一樣的,也能在抽象的父類Beverage中實現(xiàn)呀枢,steep(浸泡)和brew(沖泡)實際上也沒多大區(qū)別胚股,所以給個新名字brew,最后加入糖和奶跟加入檸檬和奶裙秋,也是類似的琅拌,也給個新名字addCondiments缨伊。改造完之后類圖如下:
Vander仔細(xì)琢磨了Panda大師的做法,發(fā)現(xiàn)Panda大師實際上先做了泛化(將實現(xiàn)泛化成抽象通用)进宝,接著再將一些具體的步驟交給子類來完成刻坊。Vander想了想其實prepareRecipe完全可以定義成final。這樣就把這個傳統(tǒng)的做法定義下來党晋,不允許子類擅自修改流程紧唱。
Panda大師一看,不錯隶校,你終于學(xué)到精髓了漏益,實際上我們剛剛的做法就是用了模板方法模式。Panda來總結(jié)一下模板方法模式的好處:
1深胳、抽象父類中定義了算法(也就是飲料制作流程)绰疤,保護了算法。
2舞终、對于子類來說轻庆,抽象父類的存在將代碼的復(fù)用最大化。
3敛劝、算法只存在于一個地方余爆,方便修改。
4夸盟、模板方法提供了一個框架蛾方,可以讓其他飲料插進來,新的飲料只要實現(xiàn)自己的方法就行了上陕。
5桩砰、抽象父類專注于方法本身,而由子類提供完整的實現(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");
}
}
說了那么多,模板方法模式具體是什么呢庶溶?
模板方法模式:在一個方法中定義一個算法的骨架煮纵,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下偏螺,重新定義算法中的某些步驟行疏。
實際上這個模式是用來創(chuàng)建一個算法的模板。什么是模板呢砖茸,實際上模板就是一個方法隘擎,更具體地說殴穴,這個方法將算法定義成一組步驟凉夯,其中任何步驟都可以是抽象的货葬,由子類負(fù)責(zé)實現(xiàn)。這可以確保算法的結(jié)構(gòu)保持不變劲够,同時由子類提供部分實現(xiàn)震桶。
下面是模板方法模式常用的套路:
這里需要說明的是,抽象類中有個鉤子方法征绎,這個鉤子方法是在抽象類中提供了一個簡單的實現(xiàn)蹲姐,子類可以選擇去覆蓋它重新實現(xiàn),也可以使用父類的實現(xiàn)人柿。
有了Panda大師度身定制咖啡和奶茶之后柴墩,添加其他飲料更加方便了,很快就加入了蔬菜汁等飲料凫岖。但是有些客人不喜歡喝帶奶的卡布奇諾江咳,他喜歡喝純咖啡。Vander想了想哥放,能不能讓客戶告訴我要不要加奶和糖歼指,我在制作的時候就可以按照客戶的請求來完成了,實際上這是任何一個奶茶店和咖啡店都需要有的基本功能甥雕。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;
}
}
實現(xiàn)的效果:
下面再說明一下鉤子算法的目的:
1踩身、讓子類實現(xiàn)算法中的可選部分
2、能夠讓子類有機會對模板方法中某些即將發(fā)生的步驟作出反應(yīng)(如在上面的咖啡類中社露,完全可以讓父類來詢問是否加入佐料挟阻,而子類再進行加入佐料后的一些附加操作)。
還要注意的是峭弟,在寫模板方法時赁濒,不要將算法的步驟切割得太細(xì)(子類要實現(xiàn)太多操作),也不要步驟太少(會沒有彈性)孟害,所以要看情況折衷拒炎。步驟可選的時候就使用鉤子方法。
模板方法模式跟一個設(shè)計原則很吻合挨务,這個設(shè)計原則就是好萊塢原則——別調(diào)用(打電話給)我們击你,我們會調(diào)用(打電話給)你。好萊塢原則可以防止“依賴腐敗”谎柄,什么是依賴腐敗呢丁侄,當(dāng)高層組件依賴底層組件,而底層組件又依賴高層組件朝巫,而高層組件又依賴邊側(cè)組件鸿摇,邊側(cè)組件還依賴于底層組件的時候依賴腐敗就發(fā)生了,這種情況很難搞清楚系統(tǒng)是如何設(shè)計的劈猿。我們允許底層組件將自己掛鉤到系統(tǒng)上拙吉,但是高層組件會決定什么時候和如何使用這些組件潮孽。換句話說:高層組件對待底層組建的方式就是“別調(diào)用我們,我們會調(diào)用你”筷黔。實際上不只是模板方法模式往史,工廠模式和觀察者模式也采用了好萊塢原則。
好萊塢原則 VS 依賴倒置原則
先對比一下策略模式和模板方法模式:
策略模式:將算法定義成對象佛舱,其他對象通過委托的方式來讓這些算法用起來椎例。
模板方法模式:定義算法的一個大綱,算法中的個別步驟可以有不同的實現(xiàn)細(xì)節(jié)请祖,會重復(fù)使用的代碼可以放進超類中订歪,好讓所有的子類共享。
模板方法模式 VS 策略模式 VS 工廠方法模式
模式 | 敘述 |
---|---|
模板方法 | 子類決定如何實現(xiàn)算法中的步驟 |
策略 | 封裝可互換的行為肆捕,然后使用委托來決定要采用哪一個行為 |
工廠方法 | 由子類決定實例化哪個具體類 |
下面我們要進入Java中的模板方法陌粹,模板方法并非剛剛舉的例子那樣明顯,有些隱藏得頗深福压,讓我們來細(xì)細(xì)挖掘掏秩。
Java數(shù)組類的設(shè)計者給出了一個排序的算法,但是排序的方法中compareTo方法卻是需要我們自身實現(xiàn)的荆姆,因為設(shè)計者無法知道你想按照什么東西來進行排序蒙幻,你必須告訴這個排序算法你依照什么來進行排序的。
接下來看看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方法會調(diào)用數(shù)組的compareTo來進行比較邮破,比較之后再進行位置的對調(diào),這實際上就是讓子類數(shù)組來實現(xiàn)具體的算法細(xì)節(jié)(即比較)仆救,而父類數(shù)組完成算法的步驟(即排序)抒和。
另外,sort()模板方法實現(xiàn)不使用繼承彤蔽,sort方法被定義成一個靜態(tài)的方法摧莽,在運行時和Comparable組合,如果類不實現(xiàn)Comparable接口的話就會導(dǎo)致sort方法中類型轉(zhuǎn)換失敗顿痪。
下面再看一個模板方法的實例JFrame镊辕,里面的paint方法是一個鉤子方法,默認(rèn)情況下是不做事情的蚁袭,要是你繼承了JFrame并且實現(xiàn)了paint方法征懈,它就會進行相應(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)于作為一個步驟被其它算法調(diào)用卖哎。
模板方法定義了算法的步驟,把這些步驟實現(xiàn)延遲到了子類,它是一種代碼復(fù)用的技巧亏娜。
好萊塢原則告訴我們焕窝,將決策權(quán)放在高層模塊中,以便決定如何以及何時調(diào)用低層模塊照藻。
策略模式和模板方法模式都封裝算法袜啃,一個用組合汗侵,一個用繼承幸缕。工廠方法是模板方法的一種特殊版本。
最后又到了喜聞樂見的總結(jié)部分晰韵,我們又來總結(jié)我們現(xiàn)在現(xiàn)有的設(shè)計模式武器发乔。
面向?qū)ο蠡A(chǔ)
抽象、封裝雪猪、多態(tài)栏尚、繼承
八大設(shè)計原則
設(shè)計原則一:封裝變化
設(shè)計原則二:針對接口編程,不針對實現(xiàn)編程
設(shè)計原則三:多用組合只恨,少用繼承
設(shè)計原則四:為交互對象之間的松耦合設(shè)計而努力
設(shè)計原則五:對擴展開放译仗,對修改關(guān)閉
設(shè)計原則六:依賴抽象,不要依賴于具體的類
設(shè)計原則七:只和你的密友談話
設(shè)計原則八:別找我官觅,我有需要會找你
模式
**模板方法模式:在一個方法中定義一個算法的骨架纵菌,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下休涤,重新定義算法中的某些步驟咱圆。 ****