樣板方法模式(Template Method Pattern)
好久不見,今天要分享的是樣板方法模式
樣板方法模式是要解決什麼樣的問題呢?
這次的情境如下:
女人擁有愛美的天性。
我們現在針對不同需求的人設計二個簡單的保養流程
然而,保養方法不只是塗抹化妝品而己,還包含了一些基本的步驟:
以下一一來看:
保養法A: 洗臉清潔 化妝水 去角質 均衡滋潤-塗美白精華液 依上述的保養法,程式可能是這麼寫的: 1: public class 保養法A 2: {
3: void 進行保養() 4: {
5: 洗臉清潔()
6: 化妝水()
7: 去角質()
8: 塗美白精華液()
9: 保護()
10: }
11:
12: void 洗臉清潔(){ 13: //實作程式碼 14: }
15:
16: void 化妝水(){ 17: //實作程式碼 18: }
19:
20: void 去角質(){ 21: //實作程式碼 22: }
23:
24: void 塗美白精華液(){ 25: //實作程式碼 26: }
27:
28: void 保護(){ 29: //實作程式碼 30: }
31: }
|
保養法B: 洗臉清潔 化妝水 拔粉刺 均衡滋潤-塗抗氧化精華液 依上述的保養法,程式可能是這麼寫的: 1: public class 保養法B 2: {
3: void 進行保養() 4: {
5: 洗臉清潔()
6: 化妝水()
7: 拔粉刺()
8: 塗抗氧化精華液()
9: 保護()
10: }
11:
12: void 洗臉清潔(){ 13: //實作程式碼 14: }
15:
16: void 化妝水(){ 17: //實作程式碼 18: }
19:
20: void 去角質(){ 21: //實作程式碼 22: }
23:
24: void 塗美白精華液(){ 25: //實作程式碼 26: }
27:
28: void 保護(){ 29: //實作程式碼 30: }
31: }
|
所以我們開始考慮這些「保養流程」,應用在不同需求的保養下,該如何設計呢?
因為保養的方法有一些重覆的部分,例如說,洗臉清潔是不可以少的,清潔完的化妝水,也是標準流程
而在精華夜的使用上開始有了不同的應對方法。最後,在不同需求的之下,有保養法A、B各別去角質與拔粉刺的處置。
ok,因此,直覺上,第一版的設計,可能看起來是長這樣:
上圖的設計看起來很直覺,首先是進行保養這個方法,因為在每個類別中都不一樣,所以定義成抽象方法。
不管是什麼保養法,各別的保養法都實踐了自己的保養流程,而每個保養流程,都是去推翻超類別的方法,並實踐自己的保養步驟。
例如,洗臉清潔、化妝水、保護通常都是固定的,因此被訂在超類別中,讓AB保養法來共享。
另外,在均衡滋潤的處置上,我們看到AB保養法出現了不同的處置,在均衡滋潤上我們可以選擇用乳液或是眼霜。
不過因為A需要美白,而B需要抗皺,所以在均衡滋潤的保養步驟,被抽出來,作為超類別的方法,但是「依需求」去讓每個
不同需求的次類別去實踐自己的保養重點,因此被次類別繼承後覆寫該方法,來實作不同的保養流程。
最後,因為AB不同的保養法中,還有其他個別的需求:拔粉刺/去角質,因此讓各次類別放入了專屬定期的保養方法。
看完了以上的類別圖,我們己經知道這幾個保養法有一些共同點,也就是採用了相同的演算法(部分方法甚至完全一樣,所以拉到了基底類別去):
1.清潔洗臉
2.化妝水
3.定期的特殊處置(深層清潔)
4.依需求作適當的均衡滋潤(透過精華液)
5.保護(上面沒提,主要是透過隔離霜)
而上述的類別圖,目前已經將1、2與5,進行了抽離,放到了基底類別中,但是在均衡滋潤與定期的特殊處置上,並尚未抽出來。
但是他們其實是定期的必要步驟,只是應用了不同效果的乳液與處置。
那麼我們有辦法將「進行保養」這個方法也給予他抽象化嗎?
首先!!我們來逐一抽象進行保養這個方法到基底類別。
在保養法A與B中,第4點的依需求作適當的均衡滋潤(透過精華液),其實就抽象化成一個共同的名稱,讓他們共用方法:
因此我們給予他一個共同的抽象名稱:均衡滋潤。
另外第3點的定期深層清潔處置,我們也來選擇一個新的方法名稱,叫作定期深層清潔好了!。
如此一來,新的保養這個基底類別中,他的「進行保養」的方法就看起來像這個樣子:
1: Void 進行保養()
2: {
3: 清潔洗臉()
4: 化妝水 ()
5: 定期的深層清潔 ()
6: 均衡滋潤 ()
7: 保護 ()
8: }
因此我們有了新的進行保養的方法後,所以要開始改造超類別
1: public abstract class 保養{
2: final void 進行保養(){
3: 清潔洗臉();
4: 化妝水 ();
5: 定期的深層清潔 ();
6: 均衡滋潤 ();
7: 保護 ();
8: }
9: abstract void 定期深層清潔();
10: abstract void 均衡滋潤();
11:
12: void 清潔洗臉(){
13: //實作程式碼
14: }
15: void 化妝水(){
16: //實作程式碼
17: }
18: void 保護(){
19: //實作程式碼
20: }
21: }
由上列的程式碼,我們可以知道進行保養的流程,我們並不希望由次類別推翻,因此宣告成final(保護你的演算法)。
而定期的深層清潔與均衡滋潤我們將名稱通用化。但因為處置方法的做法不同,因此被宣告成抽象方法,剩下就交給次類別去操心了。
那麼保養法A、跟B的實踐呢?如下:
1: public class 保養法A : 保養 2: {
3: public override void 定期的深層清潔( ) 4: {
5: //實踐去角質 6: }
7:
8: public override void 均衡滋潤( ) 9: {
10: //實踐塗美白精華液 11: }
12: }
|
1: public class 保養法B : 保養 2: {
3: public override void 定期的深層清潔( ) 4: {
5: //實踐拔粉刺 6: }
7:
8: public override void 均衡滋潤( ) 9: {
10: //實踐塗抗氧化精華液 11: }
12: }
|
從上述的簡單保養的例子,你已經體驗到了樣板方法模式了。其中,超類別:「保養」中,包含了實際的樣板方法,
樣板方法定義了一個演算法的步驟,並允許次類別為一個或多個步驟提供其實踐的方式。
因此以「進行保養」這個方法而言,它就是一個方法,而且,他是用了進行保養流程演算法的一個樣板。
而這個樣板中,每個步驟都恰好定義成一個方法,某些方法是被超類別處理(例如,洗臉、化妝水、保護)。其他方法則是由
次類別去實踐,而要被次類別實踐的方法,就必須宣告成抽象方法。
舊有的繼承覆寫方式會帶來什麼樣的困擾呢?演算法其實還是由各個的保養法去控制,而且存在著重複的程式碼。
而對於演算法所做法的程式碼改變,需要修改次類別的許多地方…
那演算法透過了樣板方法帶給我們什麼好處?
1.透過超類別保護演算法,因演算法只存在超類別,容易修改。(超類別專注在演算法本身)
2.次類別的程式碼再用性極大,由次類別來實踐完整的程式碼
以下正式介紹,樣板方法模式給各位認識:
樣板方法模式(來自Head First Design Pattern一書定義)
將一個演算法的骨架定義在一個方法中,而演算法本身會用到的一些方法,則是定義在次類別中。樣板方法讓次類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟
因此會叫作樣板”方法”的原因就是因為是包裝了演算法(而不是平常所見的UI介面的意思)。
其定義的演算法如下:(來自Head First Design Pattern一書演示)
1: abstract class AbstractClass{
2: final void templateMethod(){
3: primitiveOperation1();
4: primitiveOperation2();
5: concreteOperation();
6: hook();
7: }
8:
9: abstract void primitiveOperation1();
10: abstract void primitiveOperation2();
11: final concreteOperation(){
12: //Implementation Here
13: }
14:
15: void hook()
16: {
17: //do nothing or do something;
18: }
19: }
從上述的程式碼,我們可以看到,在樣板方法模式中,有一個小技巧稱為:Hook
什麼是hook?(掛鉤)
這是一個定義在超類別的方法,而且什麼都先不做,或是預設做些什麼(在超類別的時候)
那誰能掛上東西呢?答案是次類別!由它來決定用途為何。(就算是不爽去用,超類別也有一個預設的實踐板本(視其需求)。)
因此來看看如何應用Hook到我們的保養流程中
掛鉤要如何應用在樣板模式呢?我們的情境就是,透過掛鉤來控制我們的某一段的演算法程式碼是如執行。
從我們先前定出來的超類別,我們可以看到有一個步驟可以做為掛鉤的好實例:也就是「定期的深層清潔」這個步驟
那我們如何得知是不是該定期的深層清潔?最簡單的方法就是去檢查是不是定期的時間到了:
1: public abstract class 保養{
2: final void 進行保養(){
3: 清潔洗臉();
4: 化妝水 ();
5: if (檢查定期清潔的時間){
6: 定期的深層清潔 ();
7: }
8: 均衡滋潤 ();
9: 保護 ();
10: }
11: abstract void 定期深層清潔();
12: abstract void 均衡滋潤();
13:
14: void 清潔洗臉(){
15: //實作程式碼
16: }
17: void 化妝水(){
18: //實作程式碼
19: }
20: void 保護(){
21: //實作程式碼
22: }
23: void 檢查定期清潔的時間()
24: {
25: //預設是什麼也不做,由次類別去設定其定期清潔的時間(因為定期的頻率不同)
26: return false;
27: }
28: }
1: public class 保養法A : 保養
2: {
3: public override void 定期的深層清潔( )
4: {
5: //實踐去角質
6: }
7:
8: public override void 均衡滋潤( )
9: {
10: //實踐塗美白精華液
11: }
12:
13: public override 檢查定期深層清潔的時間()
14: {
15: if (假如累積滿二週)
16: return true;
17: else
18: return false;
19: }
20: }
上述,我們將掛鉤做為條件控制,這是一個使用掛鉤很好一種範例。
最後,要介紹一個程式設計的守則:
別呼叫高階元件(比你高階的元件),由高階元件呼叫低階元件
這個守則是為了防止「依賴」的現象,當高低階元件有互向依賴的時候
就會難以去搞懂系統的設計。那這個跟樣板方法模式有何關係?
在樣板方法模式中,我們允許低階元件(各類的保養法的定期機制)將自己掛鉤到系統上。
但是事實上是由高階元件去決定何時用到這些低階元件,並決定如何去使用。
因此我們重新審視保養這個關係。
各個保養法只有提供一些實踐的細節,而這些保養法沒有被呼叫的話,絕對不會直接呼叫抽象類別。
PS.工廠方法、觀察者都是有遵循這個守則的設計模式哦。
最後分享一下:樣板方法模式與策略模式其實都是用來封裝演算法的設計模式,只是一個用「合成」一個是透過「繼承」。
樣板方法對演算法有更多的控制權,會重覆的程式碼都被搬到了超類別中,將來會在應用程式框架中常常見到樣板方法。
在其基底類別中提供了一個基礎的方法達到程式碼的再用性,並允許次類別來修改行為。而策略模式,則必須放棄對演算法的控制,
但策略模式採用委任模型則以彈性見長,透過物件的合成,可以在執行期去改變演算法,而僅需去改變其策略物件。
相較之下,策略模式的依賴度較樣板方法低很多。因為樣板方法中次類別實踐的改變方法是演算法的一部分。
PS.而工廠方法是樣板方法模式的一種特殊版本
其實樣板方法模式,都被適當地應用於許多情境中
例如,我們的網頁生命週期Page物件。
試想,若我們目前繼承了Page物件以後,我們可以隨意的在生命週期加入我們「加工」的程式碼(Ex.OnInit()、OnPageLoad、OnEvent()、OnPreRender()…etc)。
當我們有多個網頁的時候,就會發現,雖然繼承了Page物件。但我們在生命週期中並沒有忘記任何一個步驟。
但是我們卻可以藉由不同的次類別的實作方式,產生出許多漂亮的網頁不是嗎?