提交 53216f11 编写于 作者: Q quanke

Update 16.4 改进设计.md

上级 1ad95f20
# 16.4 改进设计
16.4 改进设计
《Design Patterns》书内所有方案的组织都围绕“程序进化时会发生什么变化”这个问题展开。对于任何设计来说,这都可能是最重要的一个问题。若根据对这个问题的回答来构造自己的系统,就可以得到两个方面的结果:系统不仅更易维护(而且更廉价),而且能产生一些能够重复使用的对象,进而使其他相关系统的构造也变得更廉价。这正是面向对象程序设计的优势所在,但这一优势并不是自动体现出来的。它要求对我们对需要解决的问题有全面而且深入的理解。在这一节中,我们准备在系统的逐步改进过程中向大家展示如何做到这一点。
就目前这个回收系统来说,对“什么会变化”这个问题的回答是非常普通的:更多的类型会加入系统。因此,设计的目标就是尽可能简化这种类型的添加。在回收程序中,我们准备把涉及特定类型信息的所有地方都封装起来。这样一来(如果没有别的原因),所有变化对那些封装来说都是在本地进行的。这种处理方式也使代码剩余的部分显得特别清爽。
16.4.1 “制作更多的对象”
这样便引出了面向对象程序设计时一条常规的准则,我最早是在Grady Booch那里听说的:“若设计过于复杂,就制作更多的对象”。尽管听起来有些暧昧,且简单得可笑,但这确实是我知道的最有用一条准则(大家以后会注意到“制作更多的对象”经常等同于“添加另一个层次的迂回”)。一般情况下,如果发现一个地方充斥着大量繁复的代码,就需要考虑什么类能使它显得清爽一些。用这种方式整理系统,往往会得到一个更好的结构,也使程序更加灵活。
首先考虑Trash对象首次创建的地方,这是main()里的一个switch语句:
```
for(int i = 0; i < 30; i++)
switch((int)(Math.random() * 3)) {
case 0 :
......@@ -23,11 +26,14 @@
bin.addElement(new
Glass(Math.random() * 100));
}
```
这些代码显然“过于复杂”,也是新类型加入时必须改动代码的场所之一。如果经常都要加入新类型,那么更好的方案就是建立一个独立的方法,用它获取所有必需的信息,并创建一个句柄,指向正确类型的一个对象——已经上溯造型到一个Trash对象。在《Design Patterns》中,它被粗略地称呼为“创建范式”。要在这里应用的特殊范式是Factory方法的一种变体。在这里,Factory方法属于Trash的一名static(静态)成员。但更常见的一种情况是:它属于衍生类中一个被过载的方法。
Factory方法的基本原理是我们将创建对象所需的基本信息传递给它,然后返回并等候句柄(已经上溯造型至基础类型)作为返回值出现。从这时开始,就可以按多形性的方式对待对象了。因此,我们根本没必要知道所创建对象的准确类型是什么。事实上,Factory方法会把自己隐藏起来,我们是看不见它的。这样做可防止不慎的误用。如果想在没有多形性的前提下使用对象,必须明确地使用RTTI和指定造型。
但仍然存在一个小问题,特别是在基础类中使用更复杂的方法(不是在这里展示的那种),且在衍生类里过载(覆盖)了它的前提下。如果在衍生类里请求的信息要求更多或者不同的参数,那么该怎么办呢?“创建更多的对象”解决了这个问题。为实现Factory方法,Trash类使用了一个新的方法,名为factory。为了将创建数据隐藏起来,我们用一个名为Info的新类包含factory方法创建适当的Trash对象时需要的全部信息。下面是Info一种简单的实现方式:
```
class Info {
int type;
// Must change this to add another type:
......@@ -39,9 +45,12 @@ class Info {
}
}
```
Info对象唯一的任务就是容纳用于factory()方法的信息。现在,假如出现了一种特殊情况,factory()需要更多或者不同的信息来新建一种类型的Trash对象,那么再也不需要改动factory()了。通过添加新的数据和构建器,我们可以修改Info类,或者采用子类处理更典型的面向对象形式。
用于这个简单示例的factory()方法如下:
```
static Trash factory(Info i) {
switch(i.type) {
default: // To quiet the compiler
......@@ -56,27 +65,38 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在,
return new Cardboard(i.data);
}
}
```
在这里,对象的准确类型很容易即可判断出来。但我们可以设想一些更复杂的情况,factory()将采用一种复杂的算法。无论如何,现在的关键是它已隐藏到某个地方,而且我们在添加新类型时知道去那个地方。
新对象在main()中的创建现在变得非常简单和清爽:
```
for(int i = 0; i < 30; i++)
bin.addElement(
Trash.factory(
new Info(
(int)(Math.random() * Info.MAX_NUM),
Math.random() * 100)));
```
我们在这里创建了一个Info对象,用于将数据传入factory();后者在内存堆中创建某种Trash对象,并返回添加到Vector bin内的句柄。当然,如果改变了参数的数量及类型,仍然需要修改这个语句。但假如Info对象的创建是自动进行的,也可以避免那个麻烦。例如,可将参数的一个Vector传递到Info对象的构建器中(或直接传入一个factory()调用)。这要求在运行期间对参数(自变量)进行分析与检查,但确实提供了非常高的灵活程度。
大家从这个代码可看出Factory要负责解决的“领头变化”问题:如果向系统添加了新类型(发生了变化),唯一需要修改的代码在Factory内部,所以Factory将那种变化的影响隔离出来了。
16.4.2 用于原型创建的一个范式
上述设计方案的一个问题是仍然需要一个中心场所,必须在那里知道所有类型的对象:在factory()方法内部。如果经常都要向系统添加新类型,factory()方法为每种新类型都要修改一遍。若确实对这个问题感到苦恼,可试试再深入一步,将与类型有关的所有信息——包括它的创建过程——都移入代表那种类型的类内部。这样一来,每次新添一种类型的时候,需要做的唯一事情就是从一个类继承。
为将涉及类型创建的信息移入特定类型的Trash里,必须使用“原型”(prototype)范式(来自《Design Patterns》那本书)。这里最基本的想法是我们有一个主控对象序列,为自己感兴趣的每种类型都制作一个。这个序列中的对象只能用于新对象的创建,采用的操作类似内建到Java根类Object内部的clone()机制。在这种情况下,我们将克隆方法命名为tClone()。准备创建一个新对象时,要事先收集好某种形式的信息,用它建立我们希望的对象类型。然后在主控序列中遍历,将手上的信息与主控序列中原型对象内任何适当的信息作对比。若找到一个符合自己需要的,就克隆它。
采用这种方案,我们不必用硬编码的方式植入任何创建信息。每个对象都知道如何揭示出适当的信息,以及如何对自身进行克隆。所以一种新类型加入系统的时候,factory()方法不需要任何改变。
为解决原型的创建问题,一个方法是添加大量方法,用它们支持新对象的创建。但在Java 1.1中,如果拥有指向Class对象的一个句柄,那么它已经提供了对创建新对象的支持。利用Java 1.1的“反射”(已在第11章介绍)技术,即便我们只有指向Class对象的一个句柄,亦可正常地调用一个构建器。这对原型问题的解决无疑是个完美的方案。
原型列表将由指向所有想创建的Class对象的一个句柄列表间接地表示。除此之外,假如原型处理失败,则factory()方法会认为由于一个特定的Class对象不在列表中,所以会尝试装载它。通过以这种方式动态装载原型,Trash类根本不需要知道自己要操纵的是什么类型。因此,在我们添加新类型时不需要作出任何形式的修改。于是,我们可在本章剩余的部分方便地重复利用它。
```
//: Trash.java
// Base class for Trash recycling examples
package c16.trash;
......@@ -163,22 +183,37 @@ public abstract class Trash {
}
}
} ///:~
```
基本Trash类和sumValue()还是象往常一样。这个类剩下的部分支持原型范式。大家首先会看到两个内部类(被设为static属性,使其成为只为代码组织目的而存在的内部类),它们描述了可能出现的违例。在它后面跟随的是一个Vector trashTypes,用于容纳Class句柄。
在Trash.factory()中,Info对象id(Info类的另一个版本,与前面讨论的不同)内部的String包含了要创建的那种Trash的类型名称。这个String会与列表中的Class名比较。若存在相符的,那便是要创建的对象。当然,还有很多方法可以决定我们想创建的对象。之所以要采用这种方法,是因为从一个文件读入的信息可以转换成对象。
发现自己要创建的Trash(垃圾)种类后,接下来就轮到“反射”方法大显身手了。getConstructor()方法需要取得自己的参数——由Class句柄构成的一个数组。这个数组代表着不同的参数,并按它们正确的顺序排列,以便我们查找的构建器使用。在这儿,该数组是用Java 1.1的数组创建语法动态创建的:
```
new Class[] {double.class}
```
这个代码假定所有Trash类型都有一个需要double数值的构建器(注意double.class与Double.class是不同的)。若考虑一种更灵活的方案,亦可调用getConstructors(),令其返回可用构建器的一个数组。
从getConstructors()返回的是指向一个Constructor对象的句柄(该对象是java.lang.reflect的一部分)。我们用方法newInstance()动态地调用构建器。该方法需要获取包含了实际参数的一个Object数组。这个数组同样是按Java 1.1的语法创建的:
```
new Object[] {new Double(info.data)}
```
在这种情况下,double必须置入一个封装(容器)类的内部,使其真正成为这个对象数组的一部分。通过调用newInstance(),会提取出double,但大家可能会觉得稍微有些迷惑——参数既可能是double,也可能是Double,但在调用的时候必须用Double传递。幸运的是,这个问题只存在于基本数据类型中间。
理解了具体的过程后,再来创建一个新对象,并且只为它提供一个Class句柄,事情就变得非常简单了。就目前的情况来说,内部循环中的return永远不会执行,我们在终点就会退出。在这儿,程序动态装载Class对象,并把它加入trashTypes(垃圾类型)列表,从而试图纠正这个问题。若仍然找不到真正有问题的地方,同时装载又是成功的,那么就重复调用factory方法,重新试一遍。
正如大家会看到的那样,这种设计方案最大的优点就是不需要改动代码。无论在什么情况下,它都能正常地使用(假定所有Trash子类都包含了一个构建器,用以获取单个double参数)。
1. Trash子类
为了与原型机制相适应,对Trash每个新子类唯一的要求就是在其中包含了一个构建器,指示它获取一个double参数。Java 1.1的“反射”机制可负责剩下的所有工作。
下面是不同类型的Trash,每种类型都有它们自己的文件里,但都属于Trash包的一部分(同样地,为了方便在本章内重复使用):
```
//: Aluminum.java
// The Aluminum class with prototyping
package c16.trash;
......@@ -191,9 +226,11 @@ public class Aluminum extends Trash {
val = newVal;
}
} ///:~
```
下面是一种新的Trash类型:
```
//: Cardboard.java
// The Cardboard class with prototyping
package c16.trash;
......@@ -206,12 +243,15 @@ public class Cardboard extends Trash {
val = newVal;
}
} ///:~
```
可以看出,除构建器以外,这些类根本没有什么特别的地方。
2. 从外部文件中解析出Trash
与Trash对象有关的信息将从一个外部文件中读取。针对Trash的每个方面,文件内列出了所有必要的信息——每行都代表一个方面,采用“垃圾(废品)名称:值”的固定格式。例如:
```
c16.Trash.Glass:54
c16.Trash.Paper:22
c16.Trash.Paper:11
......@@ -247,10 +287,15 @@ c16.Trash.Paper:66
c16.Trash.Aluminum:36
c16.Trash.Cardboard:22
```
注意在给定类名的时候,类路径必须包含在内,否则就找不到类。
为解析它,每一行内容都会读入,并用字串方法indexOf()来建立“:”的一个索引。首先用字串方法substring()取出垃圾的类型名称,接着用一个静态方法Double.valueOf()取得相应的值,并转换成一个double值。trim()方法则用于删除字串两头的多余空格。
Trash解析器置入单独的文件中,因为本章将不断地用到它。如下所示:
```
//: ParseTrash.java
// Open a file and parse its contents into
// Trash objects, placing each into a Vector
......@@ -290,8 +335,11 @@ public class ParseTrash {
}
} ///:~
```
在RecycleA.java中,我们用一个Vector容纳Trash对象。然而,亦可考虑采用其他集合类型。为做到这一点,fillBin()的第一个版本将获取指向一个Fillable的句柄。后者是一个接口,用于支持一个名为addTrash()的方法:
```
//: Fillable.java
// Any object that can be filled with Trash
package c16.trash;
......@@ -299,9 +347,11 @@ package c16.trash;
public interface Fillable {
void addTrash(Trash t);
} ///:~
```
支持该接口的所有东西都能伴随fillBin使用。当然,Vector并未实现Fillable,所以它不能工作。由于Vector将在大多数例子中应用,所以最好的做法是添加另一个过载的fillBin()方法,令其以一个Vector作为参数。利用一个适配器(Adapter)类,这个Vector可作为一个Fillable对象使用:
```
//: FillableVector.java
// Adapter that makes a Vector Fillable
package c16.trash;
......@@ -314,13 +364,16 @@ public class FillableVector implements Fillable {
v.addElement(t);
}
} ///:~
```
可以看到,这个类唯一的任务就是负责将Fillable的addTrash()同Vector的addElement()方法连接起来。利用这个类,已过载的fillBin()方法可在ParseTrash.java中伴随一个Vector使用:
```
public static void
fillBin(String filename, Vector bin) {
fillBin(filename, new FillableVector(bin));
}
```
这种方案适用于任何频繁用到的集合类。除此以外,集合类还可提供它自己的适配器类,并实现Fillable(稍后即可看到,在DynaTrash.java中)。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册