
在現實生活中,很多事情都需要經過幾個步驟才能完成,例如請客吃飯,無論吃什麼,一般都包含:點單、吃東西、買單等幾個步驟,通常情況下這幾個步驟的次序是:點單->吃東西->買單。在這3個步驟中,點單和買單大同小異,最大的差別在于第2步-吃什麼?吃面條和吃滿漢全席可大不相同。在軟體開發中,有時候也會遇到類似的情況,某個方法的實作需要多個步驟(類似于“請客”),其中有些步驟是固定的,而有些步驟則存在可變性。為了提高代碼複用性和系統靈活性,可以使用一種稱之為模闆方法模式的設計模式來對這類情況進行設計。
在現實生活中,很多事情都需要經過幾個步驟才能完成,例如請客吃飯,無論吃什麼,一般都包含:點單、吃東西、買單等幾個步驟,通常情況下這幾個步驟的次序是:點單=>吃東西=>買單。在這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;
}
}
View Code
編譯運作後的結果如下圖所示:
如果這時我們需要更換具體子類,那麼無須更改源代碼,隻需修改配置檔案:
<?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)需要通過子類來決定父類算法中某個步驟是否執行,實作子類對父類的反向控制。
參考資料
劉偉,《設計模式的藝術—軟體開發人員内功修煉之道》
作者:周旭龍
出處:http://edisonchou.cnblogs.com
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。