模式重构(Pattern refactoring)
这一章我们会专注于通过逐步演化的方式应用设计模式来解决问题。也就是说,一开始我们会用比较粗糙的设计作为最初的解决方案,然后检验这个解决方案,进而针对这个问题使用不同的设计模式(有些模式是可行的,有些是不合适的)。在寻找更好的解决方案的过程中,最关键的问题是,“哪些东西是变化的?”
这个过程有点像Martin Fowler在《重构:改善既有代码的设计》那本书里谈到的那样(尽管他是通过代码片断而不是模式级别的设计来讨论重构)。以某个解决方案作为开始,当你发现这个解决方案不能再满足你的需要的时候就修正它。当然,这是一种很自然的做法,但是对于过程式的计算机编程来说要完成它是相当困难的,大家对于代码重构和设计重构的接受更加说明了面向对象编程是个“好东西”。
模拟一个垃圾循环再生器
这个问题的本质是这样的,垃圾在未分类的情况下被扔进垃圾箱,这样特定的类别信息就丢失了。但是,到后面为了给这些垃圾正确的分类,那些特定的类别信息又得被恢复出来。一开始,我们采用RTTI(Thinking in Java第二版第12章有述)作为解决方案。
这并非是一个轻而易举就能完成的设计,因为它有一些额外的限制。也正是因为有了这些限制才是这个问题更加有趣——它更像你在工作中可能会碰到的那些棘手的问题。额外的限制是指,这些垃圾运到垃圾再生厂(trash recycling plant)的时候,它们是混合在一起的。我们的程序必须要模拟垃圾分类。这正是需要RTTI的地方,你有一大堆叫不出名字的垃圾碎片,而我们的程序需要找出它们的确切类型。
//: refactor:recyclea:RecycleA.java
// Recycling with RTTI.
package refactor.recyclea;
import java.util.*;
import java.io.*;
import junit.framework.*;
abstract class Trash {
private double weight;
Trash(double wt) { weight = wt; }
abstract double getValue();
double getWeight() { return weight; }
// Sums the value of Trash in a bin:
static void sumValue(Iterator it) {
double val = 0.0f;
while(it.hasNext()) {
// One kind of RTTI:
// A dynamically-checked cast
Trash t = (Trash)it.next();
// Polymorphism in action:
val += t.getWeight() * t.getValue();
System.out.println(
"weight of " +
// Using RTTI to get type
// information about the class:
t.getClass().getName() +
" = " + t.getWeight());
}
System.out.println("Total value = " + val);
}
}
class Aluminum extends Trash {
static double val = 1.67f;
Aluminum(double wt) { super(wt); }
double getValue() { return val; }
static void setValue(double newval) {
val = newval;
}
}
class Paper extends Trash {
static double val = 0.10f;
Paper(double wt) { super(wt); }
double getValue() { return val; }
static void setValue(double newval) {
val = newval;
}
}
class Glass extends Trash {
static double val = 0.23f;
Glass(double wt) { super(wt); }
double getValue() { return val; }
static void setValue(double newval) {
val = newval;
}
}
public class RecycleA extends TestCase {
Collection
bin = new ArrayList(),
glassBin = new ArrayList(),
paperBin = new ArrayList(),
alBin = new ArrayList();
private static Random rand = new Random();
public RecycleA() {
// Fill up the Trash bin:
for(int i = 0; i < 30; i++)
switch(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
}
public void test() {
Iterator sorter = bin.iterator();
// Sort the Trash:
while(sorter.hasNext()) {
Object t = sorter.next();
// RTTI to show class membership:
if(t instanceof Aluminum)
alBin.add(t);
if(t instanceof Paper)
paperBin.add(t);
if(t instanceof Glass)
glassBin.add(t);
}
Trash.sumValue(alBin.iterator());
Trash.sumValue(paperBin.iterator());
Trash.sumValue(glassBin.iterator());
Trash.sumValue(bin.iterator());
}
public static void main(String args[]) {
junit.textui.TestRunner.run(RecycleA.class);
}
} ///:~
在与本书配套的源代码清单上,上面那个文件位于recyclea子目录里,而recyclea又是refactor子目录的一个分支。拆包(unpacking)工具会把它放到适当的子目录里。这么做的理由是,本章把这个特定的例子重写了好多次,把每个版本放到它们自己的目录里(通过使用每个目录的默认package,程序调用也很简单)可以避免类名字冲突。
程序创建了几个ArrayList对象用来存放Trash对象的引用。当然,ArrayLists实际上存放的是Objects对象,这样它们就可以存放任何东西。它们之所以存放Trash对象(或者由Trash派生出来的对象)只不过是因为你的小心翼翼,你不把Trash以外的东西传给它们。如果你往ArrayList里存放了“错误”的东西,你不会在编译时刻得到警告或者错误——你只能在运行时刻通过异常发现这些错误。
当Trash对象的引用添加到ArrayList以后,它们就丢失了特定的类别信息,变为只是Object对象的引用(它们被upcast了)。但是,由于多态的存在,当通过Iterator sorter调用动态绑定的方法时它还是会产生合适的行为,一旦最终的Object对象被cast回Trash对象,Trash. sumValue( )也采用一个Iterator来完成针对ArrayList中每个对象的操作。
先把不同类型的Trash对象upcast并放到一个能够存放基类引用的容器里,然后再把它们downcast出来,这么做看起来很傻。为什么不在一开始直接把Trash对象放到适当的容器里? (事实上,这就是垃圾回收这个例子令人迷惑的地方)。对于上面的程序要这么改动是很容易的,但是有时候采用downcasting这种方法对于某些系统的结构和灵活性都是很有好处的。
上面的程序满足了设计要求:它能够工作。如果只要求一个一次性的解决方案,那上面的方法就可以了。但是实用的程序通常是需要随着时间演化的,所以你必须得问问,“如果情况改变了会怎么样呢?”比如,现在硬纸板成了有用的可循环利用的物品,那该怎么把它集成到上面的系统呢(尤其是当程序又大又复杂的时候)。因为上面的例子里Switch语句里那些类型检查的代码是分散在整个程序里的,每次添加新类型的时候你就得查找所有类型检查的代码,如果你漏掉一个编译器是不会通过报告错误的方式给你提供任何帮助的。
这里针对每一种类型都进行测试,其实是对RTTI的一种误用。如果你只是因为某种子类型需要特殊对待而对其进行测试,那可能是恰当的。但是如果你是在针对Switch语句的每一个类型都进行测试,那么你可能是错过了某些重要的东西,而且这将注定使你的代码更加难以维护。下一小节,我们会看看这一程序是如何通过几个阶段的演化变得更加灵活的。对于程序设计,这是一个有价值的例子。
改进现有设计Improving the design
《设计模式》一书中,是围绕着“随着程序不断演化哪些东西将会发生变化?”这个问题来组织解决方案的。这对于任何设计来说通常都是最重要的一个问题。如果你能够围绕这个问题的答案来构建你的系统,将会带来一举两得的好处:不仅仅是你的系统容易维护(而且廉价),而且你还很可能创造出可以重用的组件(components),这样别的系统就更容易构建。这是面向对象编程本来就有的好处,但它不会自动发生;它需要你对于问题的思考和洞察力。这一小节我们来看看在完善系统的过程中它是怎么发生的。
对于我们的垃圾回收系统来说,“什么是变化的?”这个问题的答案是非常普通的:更多类型的(垃圾)会被加入到系统中来。也就是说,这个设计的最终目标是使得添加新的类型尽可能的方便。对于垃圾回收程序,我们想要做的是把所有涉及到特定类型信息的地方都封装起来,这样(如果没有别的原因)任何改动都可以被放到那些封装好的地方。最后的结果是这个过程也在相当程度上使程序其余部分的代码变得整洁。
多弄些对象“Make more objects”
这将引出一条常用的面向对象设计原则,我第一次是从Grady Booch那里听到的:“如果你的设计过于复杂,那就多弄些对象。”这条原则不但违反直觉而且简单的近乎荒谬,但它却是我所见过的最有用的指导原则。(你可能已经觉察到“多弄些对象”经常等同于“添加另外一个中间层。”)通常来说,如果你发现哪个地方代码非常凌乱,就可以考虑加入什么样的类可以把它弄的整洁一些。整理代码经常会带来另外一个好处是系统会拥有更好的结构和灵活性。
Trash对象最初是在main()函数的switch语句里被创建的,
for(int i = 0; i < 30; i++)
switch((int)(rand.nextInt(3)) {
case 0 :
bin.add(new
Aluminum(rand.nextDouble() * 100));
break;
case 1 :
bin.add(new
Paper(rand.nextDouble() * 100));
break;
case 2 :
bin.add(new
Glass(rand.nextDouble() * 100));
}
毫无疑问,上面的代码显的有些凌乱,而且当加入新的类型的时候你必须得改变这段代码。如果经常需要添加新类型,比较好的解决办法是使用一个单独的方法(method),这个方法利用所有必需的信息产生一个针对某一合适类型的对象的引用,这个引用会先被upcast成一个trash对象。《设计模式》一书提到这种方法的时候笼统的把它叫做创建型模式(creational pattern )(实际上有好几种创建型模式)。这里将要用到的特定模式是工厂方法(Factory Method)的一个变种。这里,工厂方法是Trash的一个静态成员函数,而更多的情况下它是作为一个被派生类覆写的方法而存在的。
factory method模式的思想是这样的,你把创建对象所需要的关键信息传递给它,然后它会把(已经upcast成基类的)引用作为返回值传给你。然后,你就可以利用这个对象的多态性了。这么一来,你甚至再也不需要知道被创建对象的确切类型。实际上,factory method为了防止你意外的误用所创建的对象,而把它的类型信息隐藏起来了。如果你想在不使用多态的情况下操纵对象,就必须得显式的使用RTTI和casting。
但是会有一些小问题,尤其是当你使用更为复杂的方法(这里没有列出),在基类里定义factory method而在派生类里覆写它的时候。
如果(创建)派生类所需的信息需要(比基类)更多的或者是不同的参数,那该怎么办呢?
“创建更多的对象”就可以解决这个问题。为了实现factory method模式,Trash类添加了一个新的叫factory的方法。为了隐藏创建对象所需的数据,新加了一个Messenger类,它携带了factory方法创建合适的Trash对象所必需的所有信息(本书开始的时候我们把Messenger也称作一个设计模式,但是它确实太简单了,可能你不想把它提升到这么高的高度)。下面是Messenger的一个简单实现:
class Messenger {
int type;
// Must change this to add another type:
static final int MAX_NUM = 4;
double data;
Messenger(int typeNum, double val) {
type = typeNum % MAX_NUM;
data = val;
}
}
Messenger对象的唯一任务就是为factory()方法保存它所需的信息。现在,如果某种情况下factory方法为了创建某一新类型的Trash对象需要更多的或者不同的信息,factory()接口就没必要改变了。Messenger类可以通过添加新的数据和新的构造函数来改变,或者采用更典型的面向对象的方法——subclassing。
这个简单例子里的factory()方法看起来像下面的样子:
static Trash factory(Messenger i) {
switch(i.type) {
default: // To quiet the compiler
case 0:
return new Aluminum(i.data);
case 1:
return new Paper(i.data);
case 2:
return new Glass(i.data);
// Two lines here:
case 3:
return new Cardboard(i.data);
}
}
这里,可以很简单的决定对象的确切类型,但你可以想象一下更为复杂的系统,在那个系统里factory()方法使用复杂难懂的算法。关键问题是这些东西现在都被隐藏到了同一个地方,当添加新类型的时候,你很清楚该到这里来改。
现在,创建新对象要比在main()函数里简单多了
for(int i = 0; i < 30; i++)
bin.add(
Trash.factory(
new Messenger(
rand.nextInt(Messenger.MAX_NUM),
rand.nextDouble() * 100)));
创建Messenger对象是为了用它传递数据给factory()方法,然后factory()方法会在堆上(heap)创建某一类型的Trash对象并returns the reference that’s added to the ArrayList bin.
当然,如果你改变了参数的个数和类型,上面的代码还需要改动,但是如果Messenger对象是自动生成的那就可以避免这样的改动了。例如,可以用一个包含所需参数的ArrayList传给Messenger对象的构造函数(或者直接传给factory()方法也可以)。这么做需要在运行时刻解析和检验传入的参数,但它的确提供了最大的灵活性。
从这段代码你可以看出factory是负责解决哪一类“一系列变化”的问题的:如果你向系统添加新的类型(所谓变化),必需要改变的只是factory内部的代码,也就是说factory把这部分变化所带来的影响隔离出来了。
to be continued......