返回總目錄 10 Form Template Method(塑造模板函數) 概要 你有一些子類,其中相應的某些函數以相同的順序執行類似的操作,但各個操作的細節不同。 將這些操作分別放進獨立的函數中,並保持它們都有相同的簽名,於是原函數也就變得相同了,然後將原函數上移至基類。 動機 繼承是避免重覆行為 ...
10 Form Template Method(塑造模板函數)
概要
你有一些子類,其中相應的某些函數以相同的順序執行類似的操作,但各個操作的細節不同。
將這些操作分別放進獨立的函數中,並保持它們都有相同的簽名,於是原函數也就變得相同了,然後將原函數上移至基類。
動機
繼承是避免重覆行為的一個強大工具。無論何時,只要你看見兩個子類之中有類似的函數,就可以把它們提升到基類。但是如果這些函數並不完全相同該這麼辦?仍有必要儘量避免重覆,但又必須保持這些函數之間的實質差異。
常見的一種情況是:兩個函數以相同的順序執行大致相近的操作,但是各操作不完全相同。這種情況下我們可以將執行的序列移至基類,並藉助多態保證各操作仍得以保持差異性。這樣的函數被稱為Template Method(模板函數)。
範例
Customer類中有兩個用於列印的函數。Statment()函數用於ASCII碼列印報表,HtmlStatement()函數則以HTML格式輸出報表:
class Customer { public string Name { get; } private List<Rental> _rentals = new List<Rental>(); public List<Rental> GetRentals() { return _rentals; } public Customer(string name) { Name = name; } public string Statement() { string result = "Rental Record for " + Name + "\n"; foreach (var rental in _rentals) { result += "\t" + rental.Title + "\t" + rental.GetCharge().ToString() + "\n"; } //add footer lines result += "Amount owed is " + GetTotalCharge() + "\n"; result += "You earned " + GetTotalFrequentRenterPoints() + " frequent renter points"; return result; } public string HtmlStatement() { string result = "<h1>Rentals for <em>" + Name + "</em></h1>\n"; foreach (var rental in _rentals) { result += rental.Title + ": " + rental.GetCharge().ToString() + "<br/>\n"; } //add footer lines result += "<p>You owe <em>" + GetTotalCharge() + "<em/></p>\n"; result += "On this rental you earned <em>" + GetTotalFrequentRenterPoints() + "</em> frequent renter points"; return result; } public double GetTotalCharge() { return _rentals.Sum(rental => rental.GetCharge()); } public int GetTotalFrequentRenterPoints() { return _rentals.Sum(rental => rental.GetFrequentRenterPoints()); } } class Rental { public string Title { get; set; } public double GetCharge() { return 1.5; } public int GetFrequentRenterPoints() { return 3; } }
使用Form Template Method之前,需要對上述兩個函數做一些整理,使它們成為同一個基類下的子類函數。為了這一目的,使用函數對象針對“報表列印”創建一個獨立的策略繼承體系:
class Statement{}
class TextStatement : Statement{}
class HtmlStatement : Statement{}
現在,通過Move Method,將兩個負責輸出報表的函數分別搬移到對應的子類中:
class Customer { public string Name { get; } private List<Rental> _rentals = new List<Rental>(); public List<Rental> GetRentals() { return _rentals; } public Customer(string name) { Name = name; } /// <summary> /// 以ASCII碼列印報表 /// </summary> /// <returns></returns> public string Statement() { return new TextStatement().Value(this); } /// <summary> /// 以HTML格式列印報表 /// </summary> /// <returns></returns> public string HtmlStatement() { return new HtmlStatement().Value(this); } public double GetTotalCharge() { return _rentals.Sum(rental => rental.GetCharge()); } public int GetTotalFrequentRenterPoints() { return _rentals.Sum(rental => rental.GetFrequentRenterPoints()); } } class Statement { } class TextStatement : Statement { /// <summary> /// 以ASCII碼列印報表 /// </summary> /// <returns></returns> public string Value(Customer customer) { string result = "Rental Record for " + customer.Name + "\n"; var rentals = customer.GetRentals(); foreach (var rental in rentals) { result += "\t" + rental.Title + "\t" + rental.GetCharge().ToString() + "\n"; } //add footer lines result += "Amount owed is " + customer.GetTotalCharge() + "\n"; result += "You earned " + customer.GetTotalFrequentRenterPoints() + " frequent renter points"; return result; } } class HtmlStatement : Statement { /// <summary> /// 以HTML格式列印報表 /// </summary> /// <returns></returns> public string Value(Customer customer) { string result = "<h1>Rental Record for <em>" + customer.Name + "</em></h1>\n"; var rentals = customer.GetRentals(); foreach (var rental in rentals) { result += rental.Title + ": " + rental.GetCharge().ToString() + "<br/>\n"; } //add footer lines result += "<p>You owe <em>" + customer.GetTotalCharge() + "<em/></p>\n"; result += "On this rental you earned <em>" + customer.GetTotalFrequentRenterPoints() + "</em> frequent renter points"; return result; } } class Rental { public string Title { get; set; } public double GetCharge() { return 1.5; } public int GetFrequentRenterPoints() { return 3; } }
面對兩個子類中的相似函數,我可以開始實施Form Template Method了。本重構的關鍵在於:運用Extract Method將兩個函數的不同部分提煉出來,從而將相似的代碼和變動的代碼分開。每次提煉後,就建立一個簽名相同但本體不同的函數。
class TextStatement : Statement { public string HeaderString(Customer customer) { return "Rental Record for " + customer.Name + "\n"; } public string EachRentalsString(Rental rental) { return "\t" + rental.Title + "\t" + rental.GetCharge().ToString() + "\n"; } public string FooterString(Customer customer) { return "Amount owed is " + customer.GetTotalCharge() + "\n" + "You earned " + customer.GetTotalFrequentRenterPoints() + " frequent renter points"; } /// <summary> /// 以ASCII碼列印報表 /// </summary> /// <returns></returns> public string Value(Customer customer) { string result = HeaderString(customer); var rentals = customer.GetRentals(); foreach (var rental in rentals) { result += EachRentalsString(rental); } //add footer lines result += FooterString(customer); return result; } } class HtmlStatement : Statement { public string HeaderString(Customer customer) { return "<h1>Rental Record for <em>" + customer.Name + "</em></h1>\n"; } public string EachRentalsString(Rental rental) { return rental.Title + ": " + rental.GetCharge().ToString() + "<br/>\n"; } public string FooterString(Customer customer) { return "<p>You owe <em>" + customer.GetTotalCharge() + "<em/></p>\n" + "On this rental you earned <em>" + customer.GetTotalFrequentRenterPoints() + "</em> frequent renter points"; } /// <summary> /// 以HTML格式列印報表 /// </summary> /// <returns></returns> public string Value(Customer customer) { string result = HeaderString(customer); var rentals = customer.GetRentals(); foreach (var rental in rentals) { result += EachRentalsString(rental); } //add footer lines result += FooterString(customer); return result; } }
所有這些都修改完畢之後,兩個Value()函數看上去已經非常相似了,因此可以使用Pull Up Method將它們提升到基類中。提升完畢後,需要在基類中把子函數聲明為抽象函數。
public abstract class Statement { public abstract string HeaderString(Customer customer); public abstract string EachRentalsString(Rental rental); public abstract string FooterString(Customer customer); public string Value(Customer customer) { string result = HeaderString(customer); var rentals = customer.GetRentals(); foreach (var rental in rentals) { result += EachRentalsString(rental); } //add footer lines result += FooterString(customer); return result; } } class TextStatement : Statement { public override string HeaderString(Customer customer) { return "Rental Record for " + customer.Name + "\n"; } public override string EachRentalsString(Rental rental) { return "\t" + rental.Title + "\t" + rental.GetCharge().ToString() + "\n"; } public override string FooterString(Customer customer) { return "Amount owed is " + customer.GetTotalCharge() + "\n" + "You earned " + customer.GetTotalFrequentRenterPoints() + " frequent renter points"; } } class HtmlStatement : Statement { public override string HeaderString(Customer customer) { return "<h1>Rental Record for <em>" + customer.Name + "</em></h1>\n"; } public override string EachRentalsString(Rental rental) { return rental.Title + ": " + rental.GetCharge().ToString() + "<br/>\n"; } public override string FooterString(Customer customer) { return "<p>You owe <em>" + customer.GetTotalCharge() + "<em/></p>\n" + "On this rental you earned <em>" + customer.GetTotalFrequentRenterPoints() + "</em> frequent renter points"; } }
完成本重構後,處理其他種類的報表就容易多了:只需為Statement再建一個子類,併在其中覆寫3個抽象函數即可。
小結
模板方法模式是基於繼承的代碼復用技術,它體現了面向對象的諸多重要思想,是一種使用較為頻繁的模式。模板方法模式被廣泛應用於框架設計中,以確保通過父類來控制處理流程的邏輯順序。
To Be Continued……