重构类关系-Form Template Method塑造模板函数十
1.塑造模板函数
1.1.使用场景
你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。
将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。
继承是避免重复行为的一个强大工具。无论何时,只要你看见两个子类之中有类似的函数,就可以把它们提升到超类。但是如果这些函数并不完全相同该怎么办?我们仍有必要尽量避免重复,但又必须保持这些函数之间的实质差异。
常见的一种情况是:两个函数以相同顺序执行大致相近的操作,但是各操作不完全相同。这种情况下我们可以将执行操作的序列移至超类,并借助多态保证各操作仍得以保持差异性。这样的函数被称为Template Method(模板函数)[Gang of Four]。
1.2.如何做
- 在各个子类中分解目标函数,使分解后的各个函数要不完全相同,要不完全不同。
- 运用Pull Up Method (322)将各子类内完全相同的函数上移至超类。
- 对于那些(剩余的、存在于各子类内的)完全不同的函数,实施Rename Method (273),使所有这些函数的签名完全相同。
- 这将使得原函数变为完全相同,因为它们都执行同样一组函数调用;但各子类会以不同方式响应这些调用。
- 修改上述所有签名后,编译并测试。
- 运用Pull Up Method (322)将所有原函数逐一上移至超类。在超类中将那些代表各种不同操作的函数定义为抽象函数。
- 编译,测试。
- 移除其他子类中的原函数,每删除一个,编译并测试。
1.3.示例
现在我将完成第1章遗留的那个范例。在此范例中,我有一个Customer,其中有两个用于打印的函数。statement()函数以ASCII码打印报表
public String statement() {
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) +
" frequent renter points";
return result;
}
函数htmlStatement() 则以HTML 格式输出报表:
public String htmlStatement() {
Enumeration rentals = _rentals.elements();
String result = "<H1>Rentals for <EM>" + getName() + "</EM></H1><P>\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for each rental
result += each.getMovie().getTitle()+ ": " +
String.valueOf(each.getCharge()) + "<BR>\n";
}
//add footer lines
result += "<P>You owe <EM>" + String.valueOf(getTotalCharge()) + "</EM><P>\n";
result += "On this rental you earned <EM>" +
String.valueOf(getTotalFrequentRenterPoints()) +
"</EM> frequent renter points<P>";
return result;
}
使用Form Template Method (345)之前,我需要对上述两个函数做一些整理,使它们成为同一个超类下的子类函数。为了这一目的,我使用函数对象 [Beck]针对“报表打印”创建一个独立的策略继承体系,
class Statement {
}
class TextStatement extends Statement {
}
class HtmlStatement extends Statement {
}
现在,通过Move Method (142),我将两个负责输出报表的函数分别搬移到对应的子类中
class Customer...
public String statement() {
return new TextStatement().value(this);
}
public String htmlStatement() {
return new HtmlStatement().value(this);
}
class TextStatement {
public String value(Customer aCustomer) {
Enumeration rentals = aCustomer.getRentals();
String result = "Rental Record for " + aCustomer.getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(aCustomer.getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +
" frequent renter points";
return result;
}
class HtmlStatement {
public String value(Customer aCustomer) {
Enumeration rentals = aCustomer.getRentals();
String result = "<H1>Rentals for <EM>" + aCustomer.getName() + "</EM></H1><P>\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for each rental
result += each.getMovie().getTitle()+ ": " +
String.valueOf(each.getCharge()) + "<BR>\n";
}
//add footer lines
result += "<P>You owe <EM>" + String.valueOf(aCustomer.getTotalCharge()) +
"</EM><P>\n";
result += "On this rental you earned <EM>"
String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +
"</EM> frequent renter points<P>";
return result;
}
搬移之后,我还对这两个函数的名称做了一些修改,使它们更好地适应Strategy模式的要求。我之所以为它们取相同名称,因为两者之间的差异不在于函数,而在于函数所属的类。如果你想试着编译这段代码,还必须在Customer类中添加一个getRentals()函数,并放宽getTotalCharge()函数和getTotalFrequent-RenterPoints()函数的可见度。
面对两个子类中的相似函数,我可以开始实施Form Template Method (345)了。本重构的关键在于:运用Extract Method (110)将两个函数的不同部分提炼出来,从而将相似的代码和变动的代码分开。每次提炼后,我就建立一个签名相同但本体不同的函数。
第一个例子就是打印报表表头。上述两个函数都通过Customer对象获取信息,但对运算结果字符串的格式化方式不同。我可以将“对字符串的格式化”提炼到独立函数中,并将提炼所得命以相同的签名
class TextStatement...
String headerString(Customer aCustomer) {
return "Rental Record for " + aCustomer.getName() + "\n";
}
public String value(Customer aCustomer) {
Enumeration rentals = aCustomer.getRentals();
String result =headerString(aCustomer);
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(aCustomer.getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +
" frequent renter points";
return result;
}
class HtmlStatement...
String headerString(Customer aCustomer) {
return "<H1>Rentals for <EM>" + aCustomer.getName() + "</EM></H1><P>\n";
}
public String value(Customer aCustomer) {
Enumeration rentals = aCustomer.getRentals();
String result = headerString(aCustomer);
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for each rental
result += each.getMovie().getTitle()+ ": " +
String.valueOf(each.getCharge()) + "<BR>\n";
}
//add footer lines
result += "<P>You owe <EM>" + String.valueOf(aCustomer.getTotalCharge()) + "</ EM><P>\n";
result += "On this rental you earned <EM>" +
String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +
"</EM> frequent renter points<P>";
return result;
}
编译并测试,然后继续处理其他元素。我将逐一对各个元素进行上述过程。下面是整个重构完成后的结果:
class TextStatement …
public String value(Customer aCustomer) {
Enumeration rentals = aCustomer.getRentals();
String result = headerString(aCustomer);
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += eachRentalString(each);
}
result += footerString(aCustomer);
return result;
}
String eachRentalString (Rental aRental) {
return "\t" + aRental.getMovie().getTitle()+ "\t" +
String.valueOf(aRental.getCharge()) + "\n";
}
String footerString (Customer aCustomer) {
return "Amount owed is " + String.valueOf(aCustomer.getTotalCharge()) + "\n" +
"You earned " + String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +
" frequent renter points";
}
class HtmlStatement…
public String value(Customer aCustomer) {
Enumeration rentals = aCustomer.getRentals();
String result = headerString(aCustomer);
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += eachRentalString(each);
}
result += footerString(aCustomer);
return result;
}
String eachRentalString (Rental aRental) {
return aRental.getMovie().getTitle()+ ": " +
String.valueOf(aRental.getCharge()) + "<BR>\n";
}
String footerString (Customer aCustomer) {
return "<P>You owe <EM>" + String.valueOf(aCustomer.getTotalCharge()) +
"</EM><P>" + "On this rental you earned <EM>" +
String.valueOf(aCustomer.getTotalFrequentRenterPoints()) +
"</EM> frequent renter points<P>";
}
所有这些修改都完成后,两个value()函数看上去已经非常相似了,因此我可以使用Pull up Method (322)将它们提升到超类中。提升完毕后,我需要在超类中把子类函数声明为抽象函数。
class Statement...
public String value(Customer aCustomer) {
Enumeration rentals = aCustomer.getRentals();
String result = headerString(aCustomer);
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += eachRentalString(each);
}
result += footerString(aCustomer);
return result;
}
abstract String headerString(Customer aCustomer);
abstract String eachRentalString (Rental aRental);
abstract String footerString (Customer aCustomer);
然后我把TextStatement.value()函数拿掉,编译并测试。完成之后再把HtmlStatement.value()也删掉,再次编译并测试。
完成本重构后,处理其他种类的报表就容易多了:你只需为Statement再建一个子类,并在其中覆写3个抽象函数即可。