設計模式-模板方法模式(Template Method)-Java

設計模式-模板方法模式(Template Method)-Java


目錄




內容

1、前言

  在現實生活中,很多事情都包含幾個實現步驟,例如請客喫飯,無論喫什麼,一般都包含點單、喫東西、買單等幾個步驟,通常情況下這幾個步驟的次序是:點單 --> 喫東西 --> 買單。在這三個步驟中,點單和買單大同小異,最大的區別在於第二步——喫什麼?吃麪條和喫滿漢全席可大不相同,如圖1-1所示
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fdu52cfS-1592096752754)(./images/請客喫飯.png)]
圖1-1 請客喫飯示意圖

2、模板方法模式概述

  在軟件開發中,有時也會遇到類似的情況,某個方法的實現需要多個步驟(類似“請客”),其中有些步驟是固定的(類似“點單”和“買單”),而有些步驟並不固定,存在可變性(類似“喫東西”)。爲了提高代碼的複用性和系統的靈活性,可以使用一種稱之爲模板方法模式的設計模式來對這類情況進行設計,在模板方法模式中,將實現功能的每一個步驟所對應的方法稱爲基本方法(例如“點單”、“喫東西”和“買單”),而調用這些基本方法同時定義基本方法的執行次序的方法稱爲模板方法(例如“請客”)。在模板方法模式中,可以將相同的代碼放在父類中,例如將模板方法“請客”以及基本方法“點單”和“買單”的實現放在父類中,而對於基本方法“喫東西”,在父類中只做一個聲明,將其具體實現放在不同的子類中,在一個子類中提供“吃麪條”的實現,而另一個子類提供“喫滿漢全席”的實現。通過使用模板方法模式,一方面提高了代碼的複用性,另一方面還可以利用面向對象的多態性,在運行時選擇一種具體子類,實現完整的“請客”方法,提高系統的靈活性和可擴展性

2.1、模板方法模式定義

  • 模板方法模式(Template Method Pattern):定義一個操作中算法的框架,而將一些步驟延遲到子類中。模板方法模式使得子類可以改變一個算法的結構即可重定義該算法的某些步驟。

  模板方法模式是一種基於繼承的代碼複用技術,它是一種類行爲型模式。

  模板方法模式是結構最簡單的行爲型設計模式,在其結構中只存在父類與子類之間的繼承關係。通過使用模板方法模式,可以將一些複雜流程的實現步驟封裝在一系列基本方法中,在抽象父類中提供一個稱之爲模板方法的方法來定義這些基本方法的執行次序,而通過其子類來覆蓋某些步驟,從而使得相同的算法框架可以有不同的執行結果。模板方法模式提供了一個模板方法來定義算法框架,而某些具體步驟的實現可以在其子類中完成。

2.2、模板方法模式結構

  模板方法模式結構比較簡單,其核心是抽象類和其中的模板方法的設計,其結構如圖2.2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CrXPmeOF-1592096752756)(./images/model_templateMethod.png)]
圖2.2-1 模板方法模式結構圖

2.3、模板方法模式結構圖中角色

  由圖2.3-1可知,模板方法模式包含如下兩個角色:

  • (1)AbstractClass(抽象類):在抽象類中定義了一系列基本操作(PrimitiveOperations),這些基本操作可以是具體的,也可以是抽象的,每一個基本操作對應算法的一個步驟,在其資料中可以重定義或實現這些步驟。同時,在抽象類中實現了一個模板方法(Template Method),用於定義一個算法的框架,模板方法不僅可以調用在抽象類中實現的基本方法,也可以調用在抽象類的子類中實現的基本方法,還可以調用其他對象中的方法。
  • (2)ConcreteClass(具體子類):它是抽象類的子類,用於實現在父類中聲明的抽象基本操作以完成子類特定算法的步驟,還可以覆蓋在父類中已經實現的具體基本操作。

2.4、模板方法模式典型實現

  在實現模板方法模式時,開發抽象類的軟件設計師和開發具體子類的軟件設計師之間可以進行協作。一個設計師負責給出一個算法的輪廓和框架,另一些設計師則負責給出這個算法的各個邏輯步驟。實現這些具體邏輯步驟的方法即爲基本方法,而將這些基本方法彙總起來的方法即爲模板方法,模板方法模式的名字也因此而來。下面將詳細介紹模板方法和基本方法:

  1. 模板方法
    一個模板方法是定義在抽象類中的、把基本操作方法組合在一起形成一個總算法或一個總行爲的方法。這個模板方法定義在抽象類中,並由子類不加以修改地完全繼承下來。模板方法是一個具體方法,它給出了一個頂層邏輯框架,而邏輯的組成步驟在抽象類中可以是具體方法,也可以是抽象方法。由於模板方法是具體方法,因此模板方法模式中的抽象層只能是抽象類,而不是接口。
  2. 基本方法
    基本方法是實現算法各個步驟的方法,是模板方法的組成部分。基本方法又可以分爲三種:抽象方法(Abstract Method)、具體方法(Concrete Method)和鉤子方法(Hook Method)。
  • (1) 抽象方法:一個抽象方法由抽象類聲明、由其具體子類實現。在C#語言裏一個抽象方法以abstract關鍵字標識。
  • (2) 具體方法:一個具體方法由一個抽象類或具體類聲明並實現,其子類可以進行覆蓋也可以直接繼承。
  • (3) 鉤子方法:一個鉤子方法由一個抽象類或具體類聲明並實現,而其子類可能會加以擴展。

  通常在父類中給出的實現是一個空實現(可使用virtual關鍵字將其定義爲虛函數),並以該空實現作爲方法的默認實現,當然鉤子方法也可以提供一個非空的默認實現。在模板方法模式中,鉤子方法有兩類:第一類鉤子方法可以與一些具體步驟“掛鉤”,以實現在不同條件下執行模板方法中的不同步驟,這類鉤子方法的返回類型通常是bool類型的,這類方法名一般爲IsXXX(),用於對某個條件進行判斷,如果條件滿足則執行某一步驟,否則將不執行,如下代碼片段所示:

……
//模板方法
public void TemplateMethod()
{
Open();
Display();
//通過鉤子方法來確定某步驟是否執行
if (IsPrint())
{
Print();
}
}
//鉤子方法
public bool IsPrint()
{
return true;
}
……

  在代碼中IsPrint()方法即是鉤子方法,它可以決定Print()方法是否執行,一般情況下,鉤子方法的返回值爲true,如果不希望某方法執行,可以在其子類中覆蓋鉤子方法,將其返回值改爲false即可,這種類型的鉤子方法可以控制方法的執行,對一個算法進行約束。

  還有一類鉤子方法就是實現體爲空的具體方法,子類可以根據需要覆蓋或者繼承這些鉤子方法,與抽象方法相比,這類鉤子方法的好處在於子類如果沒有覆蓋父類中定義的鉤子方法,編譯可以正常通過,但是如果沒有覆蓋父類中聲明的抽象方法,編譯將報錯。在模板方法模式中,抽象類的典型代碼如下:

abstract class AbstractClass
{
//模板方法
public void TemplateMethod()
{
PrimitiveOperation1();
PrimitiveOperation2();
PrimitiveOperation3();
}
//基本方法—具體方法
public void PrimitiveOperation1()
{
//實現代碼
}
//基本方法—抽象方法
public abstract void PrimitiveOperation2();
//基本方法—鉤子方法
public virtual void PrimitiveOperation3()
{ }
}

  在抽象類中,模板方法TemplateMethod()定義了算法的框架,在模板方法中調用基本方法以實現完整的算法,每一個基本方法如PrimitiveOperation1()、PrimitiveOperation2()等均實現了算法的一部分,對於所有子類都相同的基本方法可在父類提供具體實現,例如PrimitiveOperation1(),否則在父類聲明爲抽象方法或鉤子方法,由不同的子類提供不同的實現,例如PrimitiveOperation2()和PrimitiveOperation3()。可在抽象類的子類中提供抽象步驟的實現,也可覆蓋父類中已經實現的具體方法,具體子類的典型代碼如下:

class ConcreteClass : AbstractClass
{
public override void PrimitiveOperation2()
{
//實現代碼
}
public override void PrimitiveOperation3()
{
//實現代碼
}
}

  在模板方法模式中,由於面向對象的多態性,子類對象在運行時將覆蓋父類對象,子類中定義的方法也將覆蓋父類中定義的方法,因此程序在運行時,具體子類的基本方法將覆蓋父類中定義的基本方法,子類的鉤子方法也將覆蓋父類的鉤子方法,從而可以通過在子類中實現的鉤子方法對父類方法的執行進行約束,實現子類對父類行爲的反向控制。

3、模板方法模式完整解決方案-銀行利息計算

3.1、說明

  某軟件公司欲爲某銀行的業務支撐系統開發一個利息計算模塊,利息計算流程如下:

  • (1) 系統根據賬號和密碼驗證用戶信息,如果用戶信息錯誤,系統顯示出錯提示;
  • (2) 如果用戶信息正確,則根據用戶類型的不同使用不同的利息計算公式計算利息(如活期賬戶和定期賬戶具有不同的利息計算公式);
  • (3) 系統顯示利息。

  試使用模板方法模式設計該利息計算模塊。

3.2、類圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QQIXiqxX-1592096752757)(./images/銀行利息計算模塊.png)]
圖3.1-1 銀行利息計算模塊

3.3、實現

  代碼實現:

  • Account類代碼3.3-1:賬戶類-抽象類

      package templateMethod;
    
      // 賬戶類,充當抽象類
      public abstract class Account {
      	//基本方法——具體方法
      	public boolean Validate(String account, String password) {
      		System.out.println("賬號:" + account);
      		System.out.println("密碼:" + password);
      		//模擬登錄
      		return account.equals("張無忌") && password.equals("123456");
      	}
      	//基本方法——抽象方法
      	public abstract void CalculateInterest();
      	//基本方法——具體方法
      	public void Display() {
      		System.out.println("顯示利息!");
      	}
      	//模板方法
      	public void Handle(String account, String password) {
      		if (!Validate(account,password))
      		{
      			System.out.println("賬戶或密碼錯誤!");
      			return;
      		}
      		CalculateInterest();
      		Display();
      	}
      }
    
  • CurrentAccount類代碼3.3-2:活期賬戶類-具體子類

      package templateMethod;
    
      // 活期賬戶類,充當具體子類
      public class CurrentAccount extends Account{
    
      	//覆蓋父類的抽象基本方法
      	@Override
      	public void CalculateInterest() {
      		System.out.println("按活期利率計算利息!");
      	}
      }
    
  • SavingAccount類代碼3.3-3:定期賬戶類-具體子類

      package templateMethod;
    
      // :定期賬戶類,充當具體子類
      public class SavingAccount extends Account{
    
      	@Override
      	public void CalculateInterest() {
      		System.out.println("按定期利率計算利息!");
      	}
      }
    
  • Utils:工具類

      package templateMethod;
    
      import java.util.Properties;
    
      // 工具類
      public class Utils {
    
      	public static Account getAccount() {
      		try {
      			Properties prop = new Properties();
      			prop.load(Utils.class.getClassLoader().getResourceAsStream("account.properties"));
      			String className = prop.getProperty("account");
      			return (Account)(Class.forName(className).newInstance());
      		}catch (Exception e) {
      			e.printStackTrace();
      			return null;
      		}
      	}
      }
    
  • account.properties:配置文件內容

      account=templateMethod.SavingAccount
    
  • Client:客戶端-測試類

      package templateMethod;
    
      public class Client {
      	public static void main(String[] args)
      	{
      		// 工具類生成並獲取具體賬戶對象
      		Account account;
      		account = Utils.getAccount();
      		// 執行算法
      		account.Handle("張無忌", "123456");
      	}
      }
    
  • 測試結果

      賬號:張無忌
      密碼:123456
      按定期利率計算利息!
      顯示利息!
    

4、鉤子方法

  模板方法模式中,在父類中提供了一個定義算法框架的模板方法,還提供了一系列抽象方法、具體方法和鉤子方法,其中鉤子方法的引入使得子類可以控制父類的行爲。最簡單的鉤子方法就是空方法,代碼如下:

public virtual void Display() { }

  當然也可以在鉤子方法中定義一個默認的實現,如果子類不覆蓋鉤子方法,則執行父類的默認實現代碼。

  另一種鉤子方法可以實現對其他方法進行約束,這種鉤子方法通常返回一個bool類型,即返回true或false,用來判斷是否執行某一個基本方法,下面通過一個實例來說明這種鉤子方法的使用。

  某軟件公司欲爲銷售管理系統提供一個數據圖表顯示功能,該功能的實現包括如下幾個步驟:

  • (1) 從數據源獲取數據;
  • (2) 將數據轉換爲XML格式;
  • (3) 以某種圖表方式顯示XML格式的數據。

  該功能支持多種數據源和多種圖表顯示方式,但所有的圖表顯示操作都基於XML格式的數據,因此可能需要對數據進行轉換,如果從數據源獲取的數據已經是XML數據則無須轉換。

  由於該數據圖表顯示功能的三個步驟次序是固定的,且存在公共代碼(例如數據格式轉換代碼),滿足模板方法模式的適用條件,可以使用模板方法模式對其進行設計。因爲數據格式的不同,XML數據可以直接顯示,而其他格式的數據需要進行轉換,因此第(2)步“將數據轉換爲XML格式”的執行存在不確定性,爲了解決這個問題,可以定義一個鉤子方法IsNotXMLData()來對數據轉換方法進行控制。通過分析,該圖表顯示功能的基本結構如圖4-1所示:
在這裏插入圖片描述
圖4-1 數據圖表顯示功能結構圖
可以將公共方法和框架代碼放在抽象父類中,代碼如下:

//DataViewer.cs
using System;
namespace TemplateMethodSample
{
abstract class DataViewer
{
//抽象方法:獲取數據
public abstract void GetData();
//具體方法:轉換數據
public void ConvertData()
{
Console.WriteLine("將數據轉換爲XML格式。");
}
//抽象方法:顯示數據
public abstract void DisplayData();
//鉤子方法:判斷是否爲XML格式的數據
public virtual bool IsNotXMLData()
{
return true;
}
//模板方法
public void Process()
{
GetData();
//如果不是XML格式的數據則進行數據轉換
if (IsNotXMLData())
{
ConvertData();
}
DisplayData();
}
}
}

  在上面的代碼中,引入了一個鉤子方法IsNotXMLData(),其返回類型爲bool類型,在模板方法中通過它來對數據轉換方法ConvertData()進行約束,該鉤子方法的默認返回值爲true,在子類中可以根據實際情況覆蓋該方法,其中用於顯示XML格式數據的具體子類XMLDataViewer代碼如下:

//XMLDataViewer.cs
using System;
namespace TemplateMethodSample
{
class XMLDataViewer : DataViewer
{
//實現父類方法:獲取數據
public override void GetData()
{
Console.WriteLine("從XML文件中獲取數據。");
}
//實現父類方法:顯示數據,默認以柱狀圖方式顯示,可結合橋接模式來改進
public override void DisplayData()
{
Console.WriteLine("以柱狀圖顯示數據。");
}
//覆蓋父類的鉤子方法
public override bool IsNotXMLData()
{
return false;
}
}
}

  在具體子類XMLDataViewer中覆蓋了鉤子方法IsNotXMLData(),返回false,表示該數據已爲XML格式,無須執行數據轉換方法ConvertData(),客戶端代碼如下:

//Program.cs
using System;
namespace TemplateMethodSample
{
class Program
{
static void Main(string[] args)
{
DataViewer dv;
dv = new XMLDataViewer();
dv.Process();
Console.Read();
}
}
}

該程序運行結果如下:

從XML文件中獲取數據。
以柱狀圖顯示數據。

5、總結

  模板方法模式是基於繼承的代碼複用技術,它體現了面向對象的諸多重要思想,是一種使用較爲頻繁的模式。模板方法模式廣泛應用於框架設計中,以確保通過父類來控制處理流程的邏輯順序(如框架的初始化,測試流程的設置等)。

5.1、優缺點

  • 主要優點

  • (1) 在父類中形式化地定義一個算法,而由它的子類來實現細節的處理,在子類實現詳細的處理算法時並不會改變算法中步驟的執行次序。

  • (2) 模板方法模式是一種代碼複用技術,它在類庫設計中尤爲重要,它提取了類庫中的公共行爲,將公共行爲放在父類中,而通過其子類來實現不同的行爲,它鼓勵我們恰當使用繼承來實現代碼複用。

  • (3) 可實現一種反向控制結構,通過子類覆蓋父類的鉤子方法來決定某一特定步驟是否需要執行。

  • (4) 在模板方法模式中可以通過子類來覆蓋父類的基本方法,不同的子類可以提供基本方法的不同實現,更換和增加新的子類很方便,符合單一職責原則和開閉原則

  • 主要缺點

    • 需要爲每一個基本方法的不同實現提供一個子類,如果父類中可變的基本方法太多,將會導致類的個數增加,系統更加龐大,設計也更加抽象,此時,可結合橋接模式來進行設計。

5.2、適用場景

  在以下情況下可以考慮使用模板方法模式:

  • (1) 對一些複雜的算法進行分割,將其算法中固定不變的部分設計爲模板方法和父類具體方法,而一些可以改變的細節由其子類來實現。即:一次性實現一個算法的不變部分,並將可變的行爲留給子類來實現。
  • (2) 各子類中公共的行爲應被提取出來並集中到一個公共父類中以避免代碼重複。
  • (3) 需要通過子類來決定父類算法中某個步驟是否執行,實現子類對父類的反向控制。

後記

  設計模式部分參考設計模式(劉偉).pdf,作者博客地址:https://blog.csdn.net/LoveLion
  本項目爲參考某馬視頻開發,相關視頻及配套資料可自行度娘或者聯繫本人。上面爲自己編寫的開發文檔,持續更新。歡迎交流,本人QQ:806797785

前端項目源代碼地址:https://gitee.com/gaogzhen/vue-leyou
後端JAVA源代碼地址:https://gitee.com/gaogzhen/JAVA
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章