在現實生活中,很多事情都需要經過幾個步驟才能完成,例如請客吃飯,無論吃什麼,一般都包含:點單、吃東西、買單等幾個步驟,通常情況下這幾個步驟的次序是:點單=>吃東西=>買單。在這3個步驟中,點單和買單大同小異,最大的區別在於第2步-吃什麼?吃麵條和吃滿漢全席可大不相同。
在軟體開發中,有時候也會遇到類似的情況,某個方法的實現需要多個步驟(類似於“請客”),其中有些步驟是固定的,而有些步驟則存在可變性。為了提高程式碼複用性和系統靈活性,可以使用一種稱之為模板方法模式的設計模式來對這類情況進行設計。
模板方法模式(Template Method) | 學習難度:★★☆☆☆ | 使用頻率:★★★☆☆ |
一、銀行利息計算模組的設計
1.1 需求背景
Background:M公司欲為某銀行的業務支撐系統開發一個利息計算模組,利息計算流程如下:
(1)系統根據賬號和密碼驗證使用者資訊,如果使用者資訊錯誤,系統顯示錯誤提示。
(2)如果使用者資訊正確,則根據使用者型別的不同使用不同的利息計算公式計算利息(例如活期賬戶和定期賬戶具有不同的利息計算公式)
(3)系統顯示利息。
1.2 初始設計
M公司開發人員根據需求設計了一個Account類,在其中定義了3個方法實現上述3個步驟,其核心程式碼如下所示:
public class Account { // 驗證使用者資訊 public bool Validate(string account, string password) { // 具體程式碼省略 } // 計算利息 public void CalculateInterest(string type) { if (type.Equals("Current", StringComparison.OrdinalIgnoreCase)) { // 按活期利率計算利息,程式碼省略 } else if (type.Equals("Saving", StringComparison.OrdinalIgnoreCase)) { // 按定期利率計算利息,程式碼省略 } } // 顯示結果 public void Display() { // 具體程式碼省略 } }
客戶端可以通過呼叫Account類實現完整的利息計算流程,核心程式碼片段如下:
public class Client { public static void Main() { Account account = new Account(); if (account.Validate("張無忌", "123456")) // 驗證使用者 { account.CalculateInterest("Current"); // 計算利息 account.Display(); // 顯示利息 } } }
But,不難發現,該設計實現有以下兩個問題:
(1)系統可擴充套件性較差 => 如果需要增加一種新型別的使用者,例如“小額貸款使用者”,在系統中需要對應增加一種新的利息計算方法,不得不修改Account類的原始碼,在CalculateInterest方法中增加新的判斷邏輯,違背了開閉原則。
(2)客戶端需要逐個呼叫Account類中定義的方法,而且需要了解這些方法的執行與否,否則容易出錯 => 例如Account類中的3個方法的次序為:Validate() => CalculateInterest() => Display(),如果不按次序呼叫,可能會導致結果出錯。
針對問題(1),可以使用Account類的子類來解決,在子類中覆蓋父類的CalculateInterest()方法,實現擴充套件。但是針對問題(2),即使使用的是子類,也無法解決該問題。是否存在一種技術能夠一次解決問題(1)和問題(2)?
二、模板方法模式概述
2.1 模板方法模式簡介
模板方法可以算是最簡單的行為型設計模式,在其結構中只存在父類與子類之間的繼承關係,其定義如下:
模板方法(Template Method)模式:定義一個操作中演算法的框架,而將一些步驟延遲到子類中,模板方法使得子類可以不改變一個演算法的結構即可重新定義該演算法的特定步驟。模板方法是一種行為型模式。
2.2 模板方法模式結構
模板方法模式結構比較簡單,其核心是抽象類和其中的模板方法的設計,其結構如下圖所示:
(1)AbstractClass(抽象類):在抽象類中定義了一系列基本操作(Primitive Operations),這些基本操作可以是具體的,也可以是抽象的,每一個基本操作對應演算法的一個步驟,在其子類中可以重新定義或實現這些步驟。同時,在抽象類中實現了一個模板方法(Template Method),用於定義一個演算法的框架。
(2)ConcreteClass(具體子類):抽象類的子類,用於實現在父類中宣告的抽象基本操作以完成子類特定演算法的步驟,也可以覆蓋在父類中已經實現的具體基本操作。
多說無益,下面我們直接看程式碼實現,一眼就可以明白。
三、重構銀行利息計算模組設計
3.1 重構後的設計結構
其中,Account充當抽象類角色,CurrentAccount與SavingAccount充當具體子類角色。=> 是不是簡單得不行?
3.2 具體程式碼實現
(1)抽象類:Account
/// <summary> /// 抽象類:Account /// </summary> public abstract class Account { // 基本方法 - 具體方法 public bool Validate(string account, string password) { Console.WriteLine("賬號 : {0}", account); Console.WriteLine("密碼 : {0}", password); if (account.Equals("張無忌") && password.Equals("123456")) { return true; } else { return false; } } // 基本方法 - 抽象方法 public abstract void CalculateInterest(); // 基本方法 - 具體方法 public void Display() { Console.WriteLine("顯示利息"); } // 基本方法 - 鉤子方法 public virtual bool IsAllowDisplay() { return true; } // 模板方法 public void Handle(string account, string password) { if (!Validate(account, password)) { Console.WriteLine("賬戶或密碼錯誤,請重新輸入!"); return; } CalculateInterest(); if (IsAllowDisplay()) { Display(); } } }
(2)具體子類:CurrentAccount和SavingAccount
/// <summary> /// 具體子類:CurrentAccount => 活期賬戶類 /// </summary> public class CurrentAccount : Account { // 重寫父類的抽象基本方法 public override void CalculateInterest() { Console.WriteLine("按活期利率計算利息!"); } // 重寫父類的鉤子方法 public override bool IsAllowDisplay() { return base.IsAllowDisplay(); } } /// <summary> /// 具體子類:SavingAccount => 定期賬戶類 /// </summary> public class SavingAccount : Account { // 重寫父類的抽象基本方法 public override void CalculateInterest() { Console.WriteLine("按定期利率計算利息!"); } // 重寫父類的鉤子方法 public override bool IsAllowDisplay() { return false; } }
(3)客戶端測試
public class Program { public static void Main(string[] args) { Account account = AppConfigHelper.GetAccountInstance() as Account; if (account != null) { account.Handle("張無忌", "123456"); } Console.ReadKey(); } }
這裡,我們將具體子類配置在了配置檔案中:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="AccountType" value="Manulife.ChengDu.DesignPattern.TemplateMethod.SavingAccount, Manulife.ChengDu.DesignPattern.TemplateMethod" /> </appSettings> </configuration>
其中,AppConfigHelper類用於獲取配置檔案中的具體子類的例項:
public class AppConfigHelper { public static string GetAccountTypeName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["AccountType"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetAccountInstance() { string assemblyName = AppConfigHelper.GetAccountTypeName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
編譯執行後的結果如下圖所示:
如果這時我們需要更換具體子類,那麼無須更改原始碼,只需修改配置檔案:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="AccountType" value="Manulife.ChengDu.DesignPattern.TemplateMethod.CurrentAccount, Manulife.ChengDu.DesignPattern.TemplateMethod" /> </appSettings> </configuration>
重新執行客戶端後的結果如下圖所示:
四、模板方法模式總結
4.1 主要優點
模板方法中可以通過子類來覆蓋父類的基本方法,不同的子類可以提供基本方法的不同實現,更換和增加新的子類很方便,符合單一職責和開閉原則。
4.2 主要缺點
需要為每一個基本方法的不同實現一個子類,如果父類中可變的基本方法太多,將會導致類的個數增加,系統更加龐大,設計也會更加抽象。
4.3 應用場景
(1)對一些複雜的演算法進行分割,將其演算法中固定不變的部分設計為模板方法和父類具體方法,而一些可以改變的細節由其子類來實現。
(2)需要通過子類來決定父類演算法中某個步驟是否執行,實現子類對父類的反向控制。
參考資料
劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》