樣板方法模式(Template Method Pattern)

樣板方法模式(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,因此,直覺上,第一版的設計,可能看起來是長這樣:

 

image

上圖的設計看起來很直覺,首先是進行保養這個方法,因為在每個類別中都不一樣,所以定義成抽象方法。

 

不管是什麼保養法,各別的保養法都實踐了自己的保養流程,而每個保養流程,都是去推翻超類別的方法,並實踐自己的保養步驟。

例如,洗臉清潔、化妝水、保護通常都是固定的,因此被訂在超類別中,讓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介面的意思)。

image

 

其定義的演算法如下:(來自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: }

上述,我們將掛鉤做為條件控制,這是一個使用掛鉤很好一種範例。

 

最後,要介紹一個程式設計的守則:

別呼叫高階元件(比你高階的元件),由高階元件呼叫低階元件

這個守則是為了防止「依賴」的現象,當高低階元件有互向依賴的時候

就會難以去搞懂系統的設計。那這個跟樣板方法模式有何關係?

在樣板方法模式中,我們允許低階元件(各類的保養法的定期機制)將自己掛鉤到系統上。

但是事實上是由高階元件去決定何時用到這些低階元件,並決定如何去使用。

 

因此我們重新審視保養這個關係。

image

各個保養法只有提供一些實踐的細節,而這些保養法沒有被呼叫的話,絕對不會直接呼叫抽象類別。

PS.工廠方法、觀察者都是有遵循這個守則的設計模式哦。

 

 

最後分享一下:樣板方法模式與策略模式其實都是用來封裝演算法的設計模式,只是一個用「合成」一個是透過「繼承」。

樣板方法對演算法有更多的控制權,會重覆的程式碼都被搬到了超類別中,將來會在應用程式框架中常常見到樣板方法。

在其基底類別中提供了一個基礎的方法達到程式碼的再用性,並允許次類別來修改行為。而策略模式,則必須放棄對演算法的控制,

但策略模式採用委任模型則以彈性見長,透過物件的合成,可以在執行期去改變演算法,而僅需去改變其策略物件。

相較之下,策略模式的依賴度較樣板方法低很多。因為樣板方法中次類別實踐的改變方法是演算法的一部分。

PS.而工廠方法是樣板方法模式的一種特殊版本

 

其實樣板方法模式,都被適當地應用於許多情境中

例如,我們的網頁生命週期Page物件。

試想,若我們目前繼承了Page物件以後,我們可以隨意的在生命週期加入我們「加工」的程式碼(Ex.OnInit()、OnPageLoad、OnEvent()、OnPreRender()…etc)。

當我們有多個網頁的時候,就會發現,雖然繼承了Page物件。但我們在生命週期中並沒有忘記任何一個步驟。

但是我們卻可以藉由不同的次類別的實作方式,產生出許多漂亮的網頁不是嗎?