設計模式實戰-模板方法模式

piny發表於2021-09-09

1 定義與型別

  • 模板方法模式(Template Method Pattern)
    Define the skeleton of an algorithm in an operation,deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.(定義一個操作中的演算法的框架,而將一些步驟延遲到子類中。使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。)


    圖片描述

    定義


    圖片描述

    定義補充


    圖片描述

    型別


    圖片描述

    模板方法模式的通用類圖


    模板方法模式確實非常簡單,僅僅使用了Java的繼承機制,但它是一個應用非常廣泛的模式。其中,AbstractClass叫做抽象模板,它的方法分為兩類:
    基本方法
    基本操作,是由子類實現的方法,並且在模板方法被呼叫
    模板方法
    可以有一個或幾個,一般是一個具體方法,也就是一個框架,實現對基本方法的排程,完成固定的邏輯
    為了防止惡意的操作,一般模板方法都加上final關鍵字,不允許被覆寫。

在類圖中還有一個角色:具體模板。ConcreteClass1和ConcreteClass2屬於具體模板,實現父類所定義的一個或多個抽象方法,也就是父類定義的基本方法在子類中得以實現。

我們來看其通用程式碼,AbstractClass抽象模板類

public abstract class AbstractClass {     //基本方法
     protected abstract void doSomething();     //基本方法
     protected abstract void doAnything();     //模板方法
     public void templateMethod(){             /*
              * 呼叫基本方法,完成相關的邏輯
              */
             this.doAnything();             this.doSomething();
     }
}
  • 具體模板類

public class ConcreteClass1 extends AbstractClass {     //實現基本方法
     protected void doAnything() {             //業務邏輯處理
     }     protected void doSomething() {             //業務邏輯處理
     }
}public class ConcreteClass2 extends AbstractClass {     //實現基本方法
     protected void doAnything() {             //業務邏輯處理
     }     protected void doSomething() {             //業務邏輯處理
     }
}
  • 場景類

public class Client {
     public static void main(String[] args) {
             AbstractClass class1 = new ConcreteClass1();
             AbstractClass class2 = new ConcreteClass2();               
             //呼叫模板方法
             class1.templateMethod();
             class2.templateMethod();
     }
}

抽象模板中的基本方法儘量設計為protected型別,符合迪米特法則,不需要暴露的屬性或方法儘量不要設定為protected型別。實現類若非必要,儘量不要擴大父類中的訪問許可權。

2 適用場景

圖片描述

場景


多個子類有公有的方法,並且邏輯基本相同時。
重要、複雜的演算法,可以把核心演算法設計為模板方法,周邊的相關細節功能則由各個子類實現。
重構時,模板方法模式是一個經常使用的模式,把相同的程式碼抽取到父類中,然後透過鉤子函式(見“模板方法模式的擴充套件”)約束其行為。

3 優點

圖片描述


封裝不變部分,擴充套件可變部分
把認為是不變部分的演算法封裝到父類實現,而可變部分的則可以透過繼承來繼續擴充套件
提取公共部分程式碼,便於維護
如果我們不抽取到父類中,任由這種散亂的程式碼發生,想想後果是什麼樣子?維護人員為了修正一個缺陷,需要到處查詢類似的程式碼
行為由父類控制,子類實現
基本方法是由子類實現的,因此子類可以透過擴充套件的方式增加相應的功能,符合開閉原則

4 缺點

圖片描述


抽象類負責宣告最抽象、最一般的事物屬性和方法,實現類完成具體的事物屬性和方法
但是模板方法模式卻顛倒了,抽象類定義了部分抽象方法,由子類實現,子類執行的結果影響了父類的結果,也就是子類對父類產生了影響,這在複雜的專案中,會帶來程式碼閱讀的難度,而且也會讓新手產生不適感。

相關設計模式

圖片描述

示例 -  輝煌工程——製造悍馬

先不考慮擴充套件性,那好辦,先按照最一般的經驗設計類圖


悍馬車模型最一般的類圖

  • 非常簡單的實現,悍馬車有兩個型號,H1和H2
    按照需求,只需要悍馬模型,那好我就給你悍馬模型,先寫個抽象類,然後兩個不同型號的模型實現類,透過簡單的繼承就可以實現業務要求

我們先從抽象類開始編寫

抽象悍馬模型

public abstract class HummerModel {     /*
      * 首先,這個模型要能夠被髮動起來,別管是手搖發動,還是電力發動,反正
      * 是要能夠發動起來,那這個實現要在實現類裡了
      */
     public abstract void start();     //能發動,還要能停下來,那才是真本事
     public abstract void stop();       
     //喇叭會出聲音,是滴滴叫,還是嗶嗶叫
     public abstract void alarm();     //引擎會轟隆隆地響,不響那是假的
     public abstract void engineBoom();     //那模型應該會跑吧,別管是人推的,還是電力驅動的,總之要會跑
     public abstract void run();
}

定義了悍馬模型都必須具有的特質:能夠發動、停止,喇叭會響,引擎可以轟鳴,而且還可以停止
但是每個型號的悍馬實現是不同的

  • H1型號的悍馬

public class HummerH1Model extends HummerModel {     //H1型號的悍馬車鳴笛
     public void alarm() {
             System.out.println("悍馬H1鳴笛...");
     }     //引擎轟鳴聲
     public void engineBoom() {
             System.out.println("悍馬H1引擎聲音是這樣的...");
     }     //汽車發動
     public void start() {
             System.out.println("悍馬H1發動...");
     }  
     //停車
     public void stop() {
             System.out.println("悍馬H1停車...");
     }     //開動起來
     public void run(){             //先發動汽車
             this.start();             //引擎開始轟鳴
             this.engineBoom();             //然後就開始跑了,跑的過程中遇到一條狗擋路,就按喇叭
             this.alarm();             //到達目的地就停車
             this.stop();
     }
}

注意run()方法,這是一個彙總的方法,一個模型生產成功,要拿給客戶檢測吧,怎麼檢測?
“是騾子是馬,拉出去溜溜”,這就是一種檢驗方法,讓它跑起來!
透過run()這樣的方法,把模型的所有功能都測試到了。

  • H2型號悍馬

public class HummerH2Model extends HummerModel {     //H2型號的悍馬車鳴笛
     public void alarm() {
             System.out.println("悍馬H2鳴笛...");
     }     //引擎轟鳴聲
     public void engineBoom() {
             System.out.println("悍馬H2引擎聲音是這樣在...");
     }     //汽車發動
     public void start() {
             System.out.println("悍馬H2發動...");
     }  
     //停車
     public void stop() {
             System.out.println("悍馬H2停車...");
     }  
     //開動起來
     public void run(){             //先發動汽車
             this.start();              
             //引擎開始轟鳴
             this.engineBoom();         
             //然後就開始跑了,跑的過程中遇到一條狗擋路,就按喇叭
             this.alarm();              
             //到達目的地就停車
             this.stop();
     }
}

程式編寫到這裡,已經發現問題兩個實現類的run()方法完全相同
那這個run()方法的實現應該出現在抽象類,不應該在實現類上,抽象是所有子類的共性封裝

問題發現,馬上更改


圖片描述

修改後的悍馬車模類圖

抽象類HummerModel#run(),由抽象方法變更為實現方法

public abstract class HummerModel {     /*
      * 首先,這個模型要能發動起來,別管是手搖發動,還是電力發動,反正
      * 是要能夠發動起來,那這個實現要在實現類裡了
      */
     public abstract void start();      
     //能發動,還要能停下來,那才是真本事
     public abstract void stop();       
     //喇叭會出聲音,是滴滴叫,還是嗶嗶叫
     public abstract void alarm();      
     //引擎會轟隆隆地響,不響那是假的
     public abstract void engineBoom(); 
     //那模型應該會跑吧,別管是人推的,還是電力驅動,總之要會跑
     public void run(){             //先發動汽車
             this.start();              
             //引擎開始轟鳴
             this.engineBoom();         
             //然後就開始跑了,跑的過程中遇到一條狗擋路,就按喇叭
             this.alarm();              
             //到達目的地就停車
             this.stop();
     }
}

在抽象的悍馬模型上已經定義了run()方法的執行規則,先啟動,然後引擎立刻轟鳴,中間還要按一下喇叭,然後停車
它的兩個具體實現類就不需要實現run()方法了,只要把相應程式碼上的run()方法刪除即可

場景類實現的任務就是把生產出的模型展現給客戶

public class Client {
     public static void main(String[] args) {             //XX公司要H1型號的悍馬
             HummerModel h1 = new HummerH1Model();              
             //H1模型演示
             h1.run();
     }
}

執行結果

悍馬H1發動...

悍馬H1引擎聲音是這樣的...

悍馬H1鳴笛...

悍馬H1停車...

目前客戶只要看H1型號的悍馬車,沒問題,生產出來,同時可以執行起來給他看看。非常簡單,那如果我告訴你這就是模板方法模式你會不會很不屑呢?就這模式,太簡單了,我一直在使用呀!是的,你經常在使用,但你不知道這是模板方法模式,那些所謂的高手就可以很牛地說:“用模板方法模式就可以實現”,你還要很崇拜地看著,哇,牛人,模板方法模式是什麼呀?這就是模板方法模式。

擴充套件

圖片描述


到目前為止,這兩個模型都穩定地執行,突然有一天,老大急匆匆地找到了我:

“看你怎麼設計的,車子一啟動,喇叭就狂響,吵死人了!客戶提出H1型號的悍馬喇叭想讓它響就響,H2型號的喇叭不要有聲音,趕快修改一下。”

自己惹的禍,就要想辦法解決它,稍稍思考一下,解決辦法有了,先畫出類圖

圖片描述

擴充套件悍馬車模類圖


類圖改動似乎很小,在抽象類HummerModel中增加了一個實現方法isAlarm,確定各個型號的悍馬是否需要聲音,由各個實現類覆寫該方法,同時其他的基本方法由於不需要對外提供訪問,因此也設計為protected型別


  • 擴充套件後的抽象模板類

public abstract class HummerModel {     /*
      * 首先,這個模型要能夠被髮動起來,別管是手搖發動,還是電力發動,反正
      * 是要能夠發動起來,那這個實現要在實現類裡了
      */
     protected abstract void start();   
     //能發動,還要能停下來,那才是真本事
     protected abstract void stop();    
     //喇叭會出聲音,是滴滴叫,還是嗶嗶叫
     protected abstract void alarm();   
     //引擎會轟隆隆的響,不響那是假的
     protected abstract void engineBoom();      
     //那模型應該會跑吧,別管是人推的,還是電力驅動,總之要會跑
     final public void run() {          
             //先發動汽車
             this.start();              
             //引擎開始轟鳴
             this.engineBoom();         
             //要讓它叫的就是就叫,喇嘛不想讓它響就不響
             if(this.isAlarm()){                      this.alarm();
             }             //到達目的地就停車
             this.stop();
     }  
     //鉤子方法,預設喇叭是會響的
     protected  boolean isAlarm(){             return true;
     }
}
  • 在抽象類中,isAlarm是一個實現方法
    模板方法根據其返回值決定是否要響喇叭,子類可以覆寫該返回值,由於H1型號的喇叭是想讓它響就響,不想讓它響就不響,由人控制

  • 擴充套件後的H1悍馬

public class HummerH1Model extends HummerModel {     private boolean alarmFlag = true;  //要響喇叭
     protected void alarm() {
             System.out.println("悍馬H1鳴笛...");
     }     protected void engineBoom() {
             System.out.println("悍馬H1引擎聲音是這樣的...");
     }     protected void start() {
             System.out.println("悍馬H1發動...");
     }     protected void stop() {
             System.out.println("悍馬H1停車...");
     }     protected boolean isAlarm() {             return this.alarmFlag;
     }     //要不要響喇叭,是由客戶來決定的
     public void setAlarm(boolean isAlarm){             this.alarmFlag = isAlarm;
     }
}

只要呼叫H1型號的悍馬,預設是有喇叭響的,當然你可以不讓喇叭響,透過isAlarm(false)就可以實現。H2型號的悍馬是沒有喇叭聲響的

  • 擴充套件後的H2悍馬

public class HummerH2Model extends HummerModel {     protected void alarm() {
             System.out.println("悍馬H2鳴笛...");
     }     protected void engineBoom() {
             System.out.println("悍馬H2引擎聲音是這樣的...");
     }     protected void start() {
             System.out.println("悍馬H2發動...");
     }     protected void stop() {
             System.out.println("悍馬H2停車...");
     }     //預設沒有喇叭的
     protected boolean isAlarm() {             return false;
     }
}

H2型號的悍馬設定isAlarm()的返回值為false,也就是關閉了喇叭功能

  • 擴充套件後的場景類

public class Client {     public static void main(String[] args) throws IOException {
             System.out.println("-------H1型號悍馬--------");
             System.out.println("H1型號的悍馬是否需要喇叭聲響?0-不需要   1-需要");
             String type=(new BufferedReader(new InputStreamReader([System.in]()))).readLine();
             HummerH1Model h1 = new HummerH1Model();             if(type.equals("0")){  
                     h1.setAlarm(false);
             }
             h1.run();
             System.out.println("n-------H2型號悍馬--------");
             HummerH2Model h2 = new HummerH2Model();
             h2.run();
     }
}

執行是需要互動的,首先,要求輸入H1型號的悍馬是否有聲音,如下所示:

-------H1型號悍馬--------

H1型號的悍馬是否需要喇叭聲響?0-不需要 1-需要

輸入“0”後的執行結果如下所示:

-------H1型號悍馬--------

H1型號的悍馬是否需要喇叭聲響?0-不需要 1-需要

0

悍馬H1發動...

悍馬H1引擎聲音是這樣的...

悍馬H1停車...

-------H2型號悍馬--------

悍馬H2發動...

悍馬H2引擎聲音是這樣的...

悍馬H2停車...

輸入“1”後的執行結果如下所示:

-------H1型號悍馬--------

H1型號的悍馬是否需要喇叭聲響?0-不需要 1-需要

1

悍馬H1發動...

悍馬H1引擎聲音是這樣的...

悍馬H1鳴笛...

悍馬H1停車...

-------H2型號悍馬--------

悍馬H2發動...

悍馬H2引擎聲音是這樣的...

悍馬H2停車...

看到沒,H1型號的悍馬是由客戶自己控制是否要響喇叭,也就是說外界條件改變,影響到模板方法的執行。
在我們的抽象類中isAlarm的返回值就是影響了模板方法的執行結果,該方法就叫做鉤子方法(Hook Method)。有了鉤子方法模板方法模式才算完美,大家可以想想,由子類的一個方法返回值決定公共部分的執行結果,是不是很有吸引力呀!

模板方法模式就是在模板方法中按照一定的規則和順序呼叫基本方法,具體到前面那個例子,就是run()方法按照規定的順序(先呼叫start(),然後再呼叫engineBoom(),再呼叫alarm(),最後呼叫stop())呼叫本類的其他方法,並且由isAlarm()方法的返回值確定run()中的執行順序變更。

Coding!!!

圖片描述


圖片描述


圖片描述


圖片描述


圖片描述

最佳實踐

初級程式設計師在寫程式的時候經常會問高手“父類怎麼呼叫子類的方法”。這個問題很有普遍性,反正我是被問過好幾回,那麼父類是否可以呼叫子類的方法呢?我的回答是能,但強烈地、極度地不建議這麼做,那該怎麼做呢?

把子類傳遞到父類的有參構造中,然後呼叫
使用反射的方式呼叫
父類呼叫子類的靜態方法

這三種都是父類直接呼叫子類的方法,好用不?好用!解決問題了嗎?解決了!專案中允許使用不?不允許!
我就一直沒有搞懂為什麼要用父類呼叫子類的方法。如果一定要呼叫子類,那為什麼要繼承它呢?搞不懂。其實這個問題可以換個角度去理解,父類建立框架,子類在重寫了父類部分的方法後,再呼叫從父類繼承的方法,產生不同的結果(而這正是模板方法模式)。這是不是也可以理解為父類呼叫了子類的方法呢?你修改了子類,影響了父類行為的結果,曲線救國的方式實現了父類依賴子類的場景,模板方法模式就是這種效果。

模板方法在一些開源框架中應用非常多,它提供了一個抽象類,然後開源框架寫了一堆子類。在《××× In Action》中就說明了,如果你需要擴充套件功能,可以繼承這個抽象類,然後覆寫protected方法,再然後就是呼叫一個類似execute方法,就完成你的擴充套件開發,非常容易擴充套件的一種模式。

應用

AbstractList



作者:芥末無疆sss
連結:
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3034/viewspace-2815643/,如需轉載,請註明出處,否則將追究法律責任。

相關文章