重构手法之处理概括关系【4】
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……