headfirst設計模式(9)—模板方法模式

来源:https://www.cnblogs.com/skyseavae/archive/2019/04/02/10633729.html
-Advertisement-
Play Games

前言 這一章的模板方法模式,個人感覺它是一個簡單,並且實用的設計模式,先說說它的定義: 模板方法模式定義了一個演算法的步驟,並允許子類別為一個或多個步驟提供其實踐方式。讓子類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。(百度百科) 額, 這段定義呢,如果說我在不瞭解這個設計模式的時候,我看著 ...


前言

這一章的模板方法模式,個人感覺它是一個簡單,並且實用的設計模式,先說說它的定義:

模板方法模式定義了一個演算法的步驟,並允許子類別為一個或多個步驟提供其實踐方式。讓子類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。(百度百科)

額, 這段定義呢,如果說我在不瞭解這個設計模式的時候,我看著反正是雲里霧裡的,畢竟定義嘛,就是用一堆看不懂的名詞把一個看不懂的名詞描述出來,但是學了這個設計模式,反過來看,又會覺得它的定義很正確。

模板方法模式的關鍵點有3個:

1,有一個由多個步驟構成的方法(模板方法)

2,子類可以自行實現其中的一個或多個步驟(模板方法中的步驟)

3,架構允許的情況下,子類可以重新定義某些步驟

話說,這不就是上面那段話嗎?列成3點以後咋感覺越看越玄了呢?難道這就是傳說中的玄學編程?

列出來的目的是,後面的例子裡面會依次講到這3點,話不多說,代碼在idea裡面已經蓄勢待發!

模板方法模式基本實現

1,故事背景

在實現之前呢,需要有一個歷史背景,不然不知道來龍去脈,容易印象不深刻,headfirst裡面是這樣的一個例子:

在一個店裡面,有2種飲料,它們的沖泡步驟是這樣的:

1,咖啡:把水煮沸,用沸水沖泡咖啡,倒進杯子,加糖和牛奶

2,茶:把水煮沸,用沸水浸泡茶葉,倒進杯子,加檸檬

當然,本著沒有專業的自動化釀造技術的咖啡店不是一個好的科技公司,這段邏輯當然得用代碼來實現了啊,然後就進入大家最喜歡的貼代碼環節:

/**
 * 咖啡
 */
public class Coffee {

    /**
     * 準備
     */
    public void prepare() {
        boilWater();//把水煮沸
        brewCoffeeGrinds();//沖泡咖啡
        pourInCup();//倒進杯子
        addSugarAndMilk();//添加糖和牛奶
    }

    /**
     * 把水煮沸
     */
    private void boilWater() {
        System.out.println("把水煮沸");
    }

    /**
     * 沖泡咖啡
     */
    private void brewCoffeeGrinds() {
        System.out.println("用沸水沖泡咖啡");
    }

    /**
     * 倒進杯子
     */
    private void pourInCup() {
        System.out.println("倒進杯子");
    }

    /**
     * 添加糖和牛奶
     */
    private void addSugarAndMilk() {
        System.out.println("添加糖和牛奶");
    }
}

/**
 * 茶
 */
public class Tea {

    /**
     * 準備
     */
    public void prepare() {
        boilWater();//把水煮沸
        steepTeaBag();//泡茶
        pourInCup();//倒進杯子
        addLemon();//加檸檬
    }

    /**
     * 把水煮沸
     */
    private void boilWater() {
        System.out.println("把水煮沸");
    }

    /**
     * 泡茶
     */
    private void steepTeaBag() {
        System.out.println("用沸水浸泡茶葉");
    }

    /**
     * 倒進杯子
     */
    private void pourInCup() {
        System.out.println("倒進杯子");
    }

    /**
     * 加檸檬
     */
    private void addLemon() {
        System.out.println("添加檸檬");
    }
}

上面貼了咖啡和茶的實現,對外提供的public方法是prepare()方法,其他的內部方法,都是private(不需要的方法不要提供出去,外面的世界鍋太多,它們還小,經不住那麼多的打擊),按道理來說,上面兩段代碼,思路清晰,註釋完整,代碼整潔。

但是,boilWater(),pourInCup()兩個方法,其實內容是一模一樣的,對於一個程式來說,有2段一模一樣的代碼的時候,就應該思考,是不是有什麼地方不對。因為,有2段就表示要改2個同樣的地方,有10段,就要改10個同樣的地方,System.out.println()當然能改啊。

邏輯一多咋辦?邏輯一多當然也能改啊,畢竟總所周知,程式員是只需要3個鍵(Ctrl,C,V)就能正常工作的。但是,還有鍵盤在別人手上啊,他們寫在什麼地方的知道不?

 

2,邏輯抽象第一版

邏輯上來說,這個地方就會被抽象出一個公共類:

/**
 * 咖啡因的飲料(將燒水和倒進杯子兩個方法抽象出來)
 */
public abstract class CaffeineBeverage {

    public abstract void prepare();//子類必須要有一個準備飲料的方法

    /**
     * 把水煮沸
     */
    protected void boilWater() {
        System.out.println("把水煮沸");
    }

    /**
     * 倒進杯子
     */
    protected void pourInCup() {
        System.out.println("倒進杯子");
    }
}

咖啡和茶的實現就會變成下麵這樣:

/**
 * 咖啡
 */
public class Coffee extends CaffeineBeverage{

    /**
     * 準備
     */
    public void prepare() {
        boilWater();//把水煮沸
        brewCoffeeGrinds();//沖泡咖啡
        pourInCup();//倒進杯子
        addSugarAndMilk();//添加糖和牛奶
    }

    /**
     * 沖泡咖啡
     */
    private void brewCoffeeGrinds() {
        System.out.println("用沸水沖泡咖啡");
    }

    /**
     * 添加糖和牛奶
     */
    private void addSugarAndMilk() {
        System.out.println("添加糖和牛奶");
    }
}

/**
 * 茶
 */
public class Tea extends CaffeineBeverage{

    /**
     * 準備
     */
    public void prepare() {
        boilWater();//把水煮沸
        steepTeaBag();//泡茶
        pourInCup();//倒進杯子
        addLemon();//加檸檬
    }

    /**
     * 泡茶
     */
    private void steepTeaBag() {
        System.out.println("用沸水浸泡茶葉");
    }

    /**
     * 加檸檬
     */
    private void addLemon() {
        System.out.println("添加檸檬");
    }
}

一般來說,實際業務中,抽象到這個地方,已經比複製粘貼的時候好很多了。但是,很多時候,不能只看錶面,還需要總結更加深層次的東西,讓業務的實現變得更加簡單。

比如在這個例子中,prepare()方法中的步驟還可以總結,抽象。

原來的沖泡步驟:

1,咖啡:把水煮沸,用沸水沖泡咖啡,倒進杯子,加糖和牛奶

2,茶:把水煮沸,用沸水浸泡茶葉,倒進杯子,加檸檬

抽象後:

咖啡/茶:把水煮沸,用沸水 【沖泡咖啡/浸泡茶葉】,倒進杯子,加 【糖和牛奶/檸檬】

第2步和第4步,還可以抽象成,沖泡,加調味品

 

3,模板方法模式抽象

那麼抽象類的代碼就會變成這樣:

/**
 * 咖啡因的飲料(模板方法模式)
 */
public abstract class CaffeineBeverage {

    /**
     * 準備(構造成final方法,防止子類重寫演算法)
     */
    final void prepare() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    /**
     * 沖泡
     */
    abstract void brew();

    /**
     * 添加調料
     */
    abstract void addCondiments();

    /**
     * 把水煮沸
     */
    public void boilWater() {
        System.out.println("把水煮沸");
    }

    /**
     * 倒進杯子
     */
    public void pourInCup() {
        System.out.println("倒進杯子");
    }
}

咖啡和茶的實現如下:

/**
 * 咖啡
 */
public class Coffee extends CaffeineBeverage {

    /**
     * 沖泡咖啡
     */
    void brew() {
        System.out.println("用沸水沖泡咖啡");
    }

    /**
     * 添加糖和牛奶
     */
    void addCondiments() {
        System.out.println("添加糖和牛奶");
    }
}

/**
 * 茶
 */
public class Tea extends CaffeineBeverage {

    /**
     * 泡茶
     */
    void brew() {
        System.out.println("用沸水浸泡茶葉");
    }

    /**
     * 加檸檬
     */
    void addCondiments() {
        System.out.println("添加檸檬");
    }
}

測試類:

/**
 * 測試類
 */
public class Test {
 
    public static void main(String[] args) {
        Tea tea = new Tea();
        Coffee coffee = new Coffee();
        System.out.println("泡茶...");
        tea.prepare();
        System.out.println("沖咖啡...");
        coffee.prepare();
    }
}

測試結果:

泡茶...
把水煮沸
用沸水浸泡茶葉
倒進杯子
添加檸檬
沖咖啡...
把水煮沸
用沸水沖泡咖啡
倒進杯子
添加糖和牛奶

這個就是一個模板方法模式比較通用的一個實現了,中間就有模板方法模式的2個要點:

(1),有一個由多個步驟構成的方法,這裡就是prepare(),由4個步驟構成的一個模板方法

(2),子類可以自行實現其中的一個或多個步驟,咖啡和茶分別都實現了brew(),addCondiments()

其實到這個地方呢,模板方法模式的一般邏輯就大概講完了,一般來說,實現也就是上面的那個樣子,但是,還是有很多時候,會出現各種各樣的其他實現方式,畢竟抽象這個東西,對於不同的業務,不同的邏輯,那簡直就是多種多樣,只要不違背設計原則,簡單易用,那麼總會有意識無意識的用到模板方法模式。

接下來就介紹幾種常見的操作。

模板方法模式的常見操作

1,空實現

比如說,在把水煮沸前有一個前置步驟,有的飲料需要,但是有的飲料不需要,那麼就可以在模板方法裡面,給它留一個位置,讓子類去選擇性的覆蓋實現

/**
 * 咖啡因的飲料(模板方法模式,空實現)
 */
public abstract class CaffeineBeverage {

    /**
     * 準備(構造成final方法,防止子類重寫演算法)
     */
    final void prepare() {
        beforeBoilWater();
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    /**
     * 燒水前置操作
     */
    protected void beforeBoilWater(){

    }
    //其他方法省略...
}

2,預設實現

比如說,加調味品這個步驟其實是可選的,加不加調味品,每種飲料加不加調味品,模板方法可以交給子類自己去控制。

/**
 * 咖啡因的飲料(模板方法模式,預設實現)
 */
public abstract class CaffeineBeverage {

    /**
     * 準備(構造成final方法,防止子類重寫演算法)
     */
    final void prepare() {
        boilWater();
        brew();
        pourInCup();
        if(needCondiments()){
            addCondiments();
        }
    }

    /**
     * 是否需要調味品
     */
    protected boolean needCondiments(){
        return false;
    }
    //其他方法省略...
}

這既是模板方法模式的第3個要點,架構允許的情況下,子類可以重新定義某些步驟,只要模板方法可以定義添加或者移除某個,某些步驟,那麼子類就可以根據自己的實際情況來選擇實現整個方法的邏輯步驟。這個也就是這個設計模式中常說的一種叫鉤子方法。

總結

模板方法模式,其實就是,在抽象類中定義一個操作中的演算法的步驟,而將一些步驟的實現延遲到子類中。

這裡有一個建議是,儘量在抽象的時候,保證僅存在父類的方法去調用子類的方法,而不要同時存在子類的方法去調用父類的方法。

個人覺得,這樣做的原因有幾點:

1,貫徹這個建議後,整個邏輯會顯得很簡單易懂,反正沒有實現的,在子類就能找到實現

2,相互依賴調來調去的後果就是在系統越來越複雜以後,最後就沒人能看懂了

3,防止抽象類的方法變動引起子類的改動,這個其實不算原因,因為一般來說,抽象類是比較穩定的,而且,子類可以調的抽象類方法在修改時肯定要考量子類的,不允許子類調用的方法肯定都處理過了,一般肯定是調不到的(誒,反射你湊過來幹嘛?)

 

最後的最後,總結一下模板方法模式的優缺點吧

優點:

1,代碼復用便於維護,子類可擴展

2,行為由父類控制,子類只需要關心自己所需要的步驟即可,開發難度低

缺點:

1,每一個不同的實現都需要一個子類來實現,導致類的個數增加,使得系統更加龐大

所以,如果發現子類很多,是不是要想想是不是設計模式用錯了,去隔壁找找其他的設計模式吧


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 盒模型的組成大家肯定都懂,由里向外content,padding,border,margin. 盒模型是有兩種標準的,一個是標準模型,一個是IE模型。 從上面兩圖不難看出在標準模型中,盒模型的寬高只是內容(content)的寬高, 而在IE模型中盒模型的寬高是內容(content)+填充(paddi ...
  • CSS介紹 CSS(Cascading Style Sheet,層疊樣式表)定義如何顯示HTML元素。 當瀏覽器讀到一個樣式表,它就會按照這個樣式表來對文檔進行格式化(渲染)。 CSS語法 CSS實例 每個CSS樣式由兩個組成部分:選擇器和聲明。聲明又包括屬性和屬性值。每個聲明之後用分號結束。 CS ...
  • 背景 一直覺得npm、cnpm、yarn的安裝刪除基本一樣用哪個都行,不過俗話說的好,實踐出真知,這裡記錄一下今天簡單測試得到的結果總結。 可能會有錯誤,希望大家評論指正,十分感謝。 測試電腦系統:Mac 初始化 步驟:在三個文件夾里分別執行以下命令 結果都是添加了一個package.json文件 ...
  • vue中v-text / v-html使用 顯示123 ...
  • 入門 ...
  • Demo asdasdasd <!DOCTYPE html> <html lang="en"> <head> <title>Demo</title> <style> #app{ width: 100px; height: 35px; background-color: #006600; text-a ...
  • 一、引言 大家都知道單例模式,通過一個全局變數來避免重覆創建對象而產生的消耗,若系統存在大量的相似對象時,又該如何處理?參照單例模式,可通過對象池緩存可共用的對象,避免創建多對象,儘可能減少記憶體的使用,提升性能,防止記憶體溢出。 在軟體開發過程,如果我們需要重覆使用某個對象的時候,如果我們重覆地使用n ...
  • 根據我的2019年 "個人發展計劃" ,要做一個自媒體,經過1個半月的精心準備,自媒體計劃終於正式啟動了,而這背後涵蓋了大量的思考、策劃、設計、準備和技術性的工作,伴隨著自媒體的內容輸出,我也把建設的過程記錄下來... 萬事開頭難,今天先寫第一部分也是最重要的部分 打地基 導讀 一、為什麼要做自媒體 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...