面向對象,是對事物屬性與行為的封裝,方法,指的就是行為。模闆方法,顯而易見是說某個方法充當了模闆的作用,其充分利用了抽象類虛實結合的特性,虛部抽象預留,實部固定延續,以達到将某種固有行為延續至子類的目的。反觀接口,則達不到這種目的。要搞明白模闆方法,首先我們從接口與抽象類的差別切入,這也是面試官經常會問到的問題。
汽車上的接口最常見的就是這幾個了,點煙器,USB,AUX等等,很明顯這些都是接口,它們都預留了某種标準,暴露在系統外部,并與外設對接。就拿點煙器接口來說吧,它原本是專門用于給點煙器供電的,後來由于這個接口在汽車上的通用性,于是衍生出了各種外部裝置,隻要是符合這個标準size的,帶正負極簧片的,直流12V的,那就可以使用,比如導航、行車記錄儀、吸塵器什麼的,以及其他各種車載電子裝置。public interface CigarLighterInterface {//點煙器接口
//供電方法,16V直流電
public void electrifyDC16V();
}
1public class GPS implements CigarLighterInterface {
2 //導航的實作
3 @Override
4 public void electrifyDC16V() {
5 System.out.println("連接配接衛星");
6 System.out.println("定位。。。");
7 }
8
9}
1public class CigarLighter implements CigarLighterInterface {
2 //點煙器的實作
3 @Override
4 public void electrifyDC16V() {
5 int time = 1000;
6 while(--time>0){
7 System.out.println("加熱電爐絲");
8 }
9 System.out.println("點煙器彈出");
10 }
11
12}
對于點煙器接口來說,它根本不在乎也不知道對接的外設是什麼鬼,它隻是定義了一種規範,一種标準,隻要符合的都可以對接。再比如USB接口的應用更加廣泛,外設更是應有盡有,具體例子可以參考文章《設計模式是什麼鬼(初探)》。
以上我們可以體會到接口的抽象是淋漓盡緻的,實作是空無的,也就是說其方法都是無實作的。然而這樣在某些場景下會存在一些問題,例如有時候我們在父類中隻需抽象出一些方法,并且同時也有一些實體方法,以供子類直接繼承,這怎麼辦?答案就是抽象類。舉個例子,哺乳動物類,我們人類就是哺乳動物。
什麼?鲸魚是哺乳類?是的,凡是喂奶的都是哺乳類,不要以為會遊泳的都是魚,會飛的都是鳥,蝙蝠同樣是哺乳類,隻不過是老鼠中的飛行員而已。
既然如此這哺乳動物肯定是都能喂奶了,但是到底是跑還是遊,或是飛呢還真不好說,但至少可以确認它們都是可以移動的。言歸正傳,我們開始定義哺乳動物抽象類。
1public abstract class Mammal {
2
3 //既然是哺乳動物當然會喂奶了,但這裡限制為隻能母的喂奶
4 protected final void feedMilk(){
5 if(female){//如果是母的……
6 System.out.println("喂奶");
7 }else{//如果是公的……或者可以抛個異常出去。
8 System.out.println("公的不會");
9 }
10 }
11
12 //哺乳動物當然可以移動,但具體怎麼移動還不知道。
13 public abstract void move();
14}
這裡我們省略了female屬性,其作用是為了控制喂奶行為,大家可以自行添加。可以看到的是,抽象類不同于接口,其自身是可以有具體實作的,也就是說抽象類是虛實結合的,虛部抽象行為,實部遺傳給子類,虛虛實實,飄忽不定。OK,我們看下人、鲸、蝠的子類實作。
public class Human extends Mammal {
@Override
public void move() {
System.out.println("兩條腿走路……");
}
}
public class Whale extends Mammal {
@Override
public void move() {
System.out.println("遊泳……");
}
}
public class Bat extends Mammal {
@Override
public void move() {
System.out.println("用翅膀飛……");
}
}
可以看到子類的各路實作都是自己獨有的行為方式,而喂奶那個行為是不需要自己實作的,它是屬于抽象哺乳類的共有行為,哺乳子類不能進行任何幹涉。這便是接口與抽象的最大差別了,接口是虛的,抽象類可以有虛有實,接口不在乎實作類是什麼,抽象類會延續其基因給子類。
其實到這裡我們已經說了一大半了,了解了以上部分,剩下的部分就非常簡單了,利用抽象類的這個特性,便有了“模闆方法”。舉例說明,我們做軟體項目管理,按瀑布式簡單來講分為:需求分析、設計、編碼、測試、釋出,先不管是用何種方式去實作各個細節,我們就抽象成這五個步驟。
public abstract class PM {
protected abstract void analyze();//需求分析
protected abstract void design();//設計
protected abstract void develop();//開發
protected abstract boolean test();//測試
protected abstract void release();//釋出
}
那麼問題來了,有個程式員在需求不明确或者設計不完善的情況下,一上來二話不說直接寫代碼,這樣就會造成資源的浪費,後期改動太大還會影響項目進度。那麼項目經理這時就應該規範一下這個任務流程,這裡我們加入kickoff()方法實作。
1public abstract class PM {
2 protected abstract void analyze();//需求分析
3 protected abstract void design();//設計
4 protected abstract void develop();//開發
5 protected abstract boolean test();//測試
6 protected abstract void release();//釋出
7
8 protected final void kickoff(){
9 analyze();
10 design();
11 develop();
12 test();
13 release();
14 }
15}
這樣就限制了整個項目周期的任務流程,注意這裡要用final聲明此方法子類不可以重寫,隻能乖乖的繼承下去用。至于其他的抽象方法,子類可以自由發揮,比如測試方法test(),子類可以用人工測試,自動化測試,我們不care,我們是站在項目管理的抽象高度,隻把控流程進度。這裡甚至我們還可以加入一些邏輯如下。
1public abstract class PM {
2 protected abstract void analyze();//需求分析
3 protected abstract void design();//設計
4 protected abstract void develop();//開發
5 protected abstract boolean test();//測試
6 protected abstract void release();//釋出
7
8 protected final void kickoff(){
9 analyze();
10 design();
11 do {
12 develop();
13 } while (!test());//如果測試失敗,則繼續開發改Bug。
14 release();
15 }
16}
以下子類隻需實作抽象方法,而不用實作固有的模闆方法kickoff(),因為它已經被父類PM實作了,并且子類也不能進行重寫。
1public class AutoTestPM extends PM{
2
3 @Override
4 protected void analyze() {
5 System.out.println("進行業務溝通,需求分析");
6 }
7
8 //design();develop();test();release();實作省略
9}
至此,我們的模闆方法就完成了,抽象類PM中的實方法kickoff()中,以某種邏輯編排調用了其他各個抽象方法,提供了一種固定模式的行為方式或是指導方針,以此達到虛實結合、柔中帶剛、剛柔并濟,靈活中不失規範的目的。
當然大部分情況我們使用接口會多于抽象類,因為接口靈活啊,抽象類不允許多繼承啊等等,其實我們還是要看應用場景,在某種無規矩不成方圓,或者規範比較明确,的情況下抽象類的應用是有必要的,世間萬物沒有最好的,隻有最合适的。