說到模板方法模式,它可能是一個讓我們深入骨髓而又不自知的模式了,因為它在我們開發過程中會經常遇到,並且也非常簡單。只不過,很多時候我們並不知道它就是模板方法模式而已。不負責任的說,當我們用到override
關鍵字重寫父類方法的時候,十有八九就跟模板方法模式有關了。
定義
先看一下模板方法模式的定義,模板方法模式定義了一個操作中的演算法的框架,而將一些步驟延遲到子類中。使得子類可以不改變一個演算法的結構即可重定義該演算法的某些步驟。
這裡延遲到子類說的玄乎,其實就是子類繼承並實現父類中的抽象方法(abstract
),而重定義該演算法的某些步驟指的就是子類重寫父類的虛方法(virtual
)。不過,不管是哪一個,子類都需要用到override
。
例項
我們還是通過一個例子來解釋模板方法模式,先來一個經典的腦筋急轉彎。
把一個大象裝進冰箱要幾個步驟?
答案是三步:
- 第一步,把冰箱門開啟
- 第二步,把大象放進去
- 第三步,把冰箱門關上
對應到前面的定義,這裡把大象裝進冰箱的步驟就是演算法的框架,而其中的每一步就是演算法的具體步驟。我們用程式碼實現看看:
public abstract class AnimalToFridge
{
public void Do()
{
OpenFridge();
PutIntoFridge();
CloseFridge();
}
private void OpenFridge()
{
Console.WriteLine("把冰箱門開啟");
}
public abstract void PutIntoFridge();
private void CloseFridge()
{
Console.WriteLine("把冰箱門關上");
}
}
上面定義了一個把動物放進冰箱的基類,Do()
方法定義了把大象裝進冰箱的演算法骨架,其中,開啟冰箱和關閉冰箱兩個步驟是固定不變的,變化只是把什麼動物放進去。
再定義一個把大象放冰箱的子類,繼承自上面的基類:
public class ElephantToFridge:AnimalToFridge
{
public override void PutIntoFridge()
{
Console.WriteLine("把大象放進去");
}
}
使用時,我們只需要呼叫Do()
方法就可以完成把大象放冰箱的動作了:
static void Main(string[] args)
{
AnimalToFridge elephantToFridge = new ElephantToFridge();
elephantToFridge.Do();
}
這時候如果我們要把其他動物放進去,只需要繼承AnimalToFridge
就可以了,例如,我們把狗放進冰箱:
public class DogToFridge: AnimalToFridge
{
public override void PutIntoFridge()
{
Console.WriteLine("把狗放進去");
}
}
但是你以為這麼簡單就結束了嗎?知道這個腦筋急轉的朋友應該都知道它還有第二問。
然後把一個長頸鹿裝進冰箱要幾個步驟?
答案是四步:
- 第一步,把冰箱門開啟
- 第二步,把大象弄出來
- 第三步,把長頸鹿放進去
- 第四步,把冰箱門關上
我們可以分析一下需求,也就是說,把大象放進之前不需要先把什麼拿出來,但是放長頸鹿需要先把大象弄出來。再進一步分析的話,可以推測把雞蛋、螞蟻這樣的小東西放進去,即使裡面有大象,應該也不需要先把大象拿出來,而放獅子、老虎這樣的大型動物就需要清空冰箱。為了滿足這樣的需求,我們的虛方法就登場了,程式碼可以做如下改進:
public abstract class AnimalToFridge
{
public void Do()
{
OpenFridge();
BeforePutIntoFridge();
PutIntoFridge();
CloseFridge();
}
private void OpenFridge()
{
Console.WriteLine("把冰箱門開啟");
}
protected virtual void BeforePutIntoFridge() { }
protected abstract void PutIntoFridge();
private void CloseFridge()
{
Console.WriteLine("把冰箱門關上");
}
}
基類中增加了一個BeforePutIntoFridge()
的虛方法,方法只有一個空的實現(當然,如果需要的話,也可以新增具體內容),除此之外,我把虛方法和抽象方法的訪問修飾符都改成protected
了,因為,演算法的單個步驟不應該被客戶端直接呼叫,呼叫了也沒有任何意義。這樣,我們的大象和長頸鹿子類就可以如下實現了:
public class ElephantToFridge : AnimalToFridge
{
protected override void PutIntoFridge()
{
Console.WriteLine("把大象放進去");
}
}
public class GiraffeToFridge : AnimalToFridge
{
protected override void BeforePutIntoFridge()
{
Console.WriteLine("把大象弄出來");
}
protected override void PutIntoFridge()
{
Console.WriteLine("把長頸鹿放進去");
}
}
ElephantToFridge
類不重寫父類的BeforePutIntoFridge()
方法,而GiraffeToFridge
類重寫了,也就是定義中所說的重定義了該演算法的某些步驟了。
好了,這樣就改造完成並滿足需求了,我們再來看一下最終的整體類圖:
這就是模板方法模式,其實就是對繼承加抽象方法和虛方法的使用,這可能算是繼承的巔峰時刻了吧,在其他模式中只有被吐槽的命。
UML類圖
再抽象一下就可以得到模板方法模式的UML類圖了:
鉤子函式
在學習模板方法模式的時候,我們可能會經常聽到鉤子函式這個概念。鉤子就是給子類一個授權,讓子類來可重定義模板方法的某些步驟,聽著高大上,說白了就是虛方法而已。
優缺點
優點
- 封裝了演算法骨架,提高了程式碼複用性,簡化了使用難度;
- 封裝不變部分,擴充套件可變部分,滿足開閉原則。
缺點
- 演算法骨架不易更改,也就是原先定義的演算法步驟如果需要變化,就不得不修改原始碼了;
- 擴充套件時,可能會產生很多子類,這是繼承不可避免的缺陷。
跟建造者模式的異同
建造者模式很多地方跟模板方法模式是很相似的,例如,他們都是通過繼承實現,都會把易變化的部分延遲到子類實現,並且都有一個方法封裝骨架,只不過,建造者模式延遲到子類的是各部件的建立,封裝的是最後的構建流程。而模板方法模式延遲到子類實現的是演算法的某些步驟,封裝的是演算法骨架。也就是說如果你承認建立物件也是一種演算法的話,那二者其實就差不多了。不過呢?他們也是有區別的,因為建造者模式中,各部件的建造需要客戶端配合完成,因此,建造各部件的方法需要是public
的,而模板方法模式中,各單獨的演算法步驟不應該被客戶端直接呼叫,因此通常是protected
的。不過,儘管如此,他們的設計思想確實是大同小異的。
說到這裡,還記得建造者模式是如何通過使用委託來緩解子類過多的問題的嗎?既然模板方法模式與建造者模式相似,那麼處理方式也應該相似了,我們看看最終實現效果:
public class AnimalToFridge
{
public void Do(Action beforePutIntoFridge,Action putIntoFridge)
{
OpenFridge();
beforePutIntoFridge?.Invoke();
putIntoFridge?.Invoke();
CloseFridge();
}
private void OpenFridge()
{
Console.WriteLine("把冰箱門開啟");
}
private void CloseFridge()
{
Console.WriteLine("把冰箱門關上");
}
}
直接把抽象方法和虛方法都去掉了,換成了委託,只保留了演算法骨架。這樣做好處很明顯,不需要子類了,無論多少動物,全部都通過委託搞定了。不過缺點也很明顯,演算法的實現交給了客戶端,給客戶端的使用帶來了不小的負擔,並且如果呼叫位置很多,還會導致大量程式碼重複,難以維護。
因此,模板方法模式具體該如何使用還得視情況而定。