78.JAVA编程思想——改进设计

78.JAVA编程思想——改进设计

《Design Patterns》书内所有方案的组织都围绕“程序进化时会发生什么变化”这个问题展开。对于任何设计来说,这都可能是最重要的一个问题。若根据对这个问题的回答来构造自己的系统,就可以得到两个方面的结果:系统不仅更易维护(而且更廉价),而且能产生一些能够重复使用的对象,进而使其他相关系统的构造也变得更廉价。这正是面向对象程序设计的优势所在,但这一优势并不是自动体现出来的。它要求对我们对需要解决的问题有全面而且深入的理解。在这一节中,我们准备在系统的逐步改进过程中向大家展示如何做到这一点。

就目前这个回收系统来说,对“什么会变化”这个问题的回答是非常普通的:更多的类型会加入系统。因此,设计的目标就是尽可能简化这种类型的添加。在回收程序中,我们准备把涉及特定类型信息的所有地方都封装起来。这样一来(如果没有别的原因),所有变化对那些封装来说都是在本地进行的。这种处理方式也使代码剩余的部分显得特别清爽。

1     “制作更多的对象”

这样便引出了面向对象程序设计时一条常规的准则,最早是在Grady Booch说的:“若设计过于复杂,就制作更多的对象”。尽管听起来有些暧昧,且简单得可笑,但这确实是我知道的最有用一条准则(大家以后会注意到“制作更多的对象”经常等同于“添加另一个层次的迂回”)。一般情况下,如果发现一个地方充斥着大量繁复的代码,就需要考虑什么类能使它显得清爽一些。用这种方式整理系统,往往会得到一个更好的结构,也使程序更加灵活。

首先考虑Trash 对象首次创建的地方,这是main()里的一个switch 语句:

for(int i = 0; i < 30; i++)

switch((int)(Math.random() * 3)){

case 0 :

bin.addElement(new

Aluminum(Math.random() * 100));

break;

case 1 :

bin.addElement(new

Paper(Math.random() * 100));

break;

case 2 :

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 addanother type:

static final int MAX_NUM = 4;

double data;

Info(int typeNum, double dat) {

type = typeNum % MAX_NUM;

data = dat;

}

}

Info 对象唯一的任务就是容纳用于factory()方法的信息。现在,假如出现了一种特殊情况,factory()需要更多或者不同的信息来新建一种类型的Trash 对象,那么再也不需要改动factory()了。通过添加新的数据和构建器,我们可以修改Info 类,或者采用子类处理更典型的面向对象形式。用于这个简单示例的factory()方法如下:

static Trash factory(Info i) {

switch(i.type) {

default: // To quiet thecompiler

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.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 将那种变化的影响隔离出来了。

2     用于原型创建的一个范式

上述设计方案的一个问题是仍然需要一个中心场所,必须在那里知道所有类型的对象:在factory()方法内部。如果经常都要向系统添加新类型,factory()方法为每种新类型都要修改一遍。若确实对这个问题感到苦恼,可试试再深入一步,将与类型有关的所有信息——包括它的创建过程——都移入代表那种类型的类内部。这样一来,每次新添一种类型的时候,需要做的唯一事情就是从一个类继承。

为将涉及类型创建的信息移入特定类型的Trash 里,必须使用“原型”(prototype)范式(来自《Design Patterns》那本书)。这里最基本的想法是我们有一个主控对象序列,为自己感兴趣的每种类型都制作一个。这个序列中的对象只能用于新对象的创建,采用的操作类似内建到Java 根类Object 内部的clone()机制。在这种情况下,我们将克隆方法命名为tClone()。准备创建一个新对象时,要事先收集好某种形式的信息,用它建立我们希望的对象类型。然后在主控序列中遍历,将手上的信息与主控序列中原型对象内任何适当的信息作对比。若找到一个符合自己需要的,就克隆它。

采用这种方案,我们不必用硬编码的方式植入任何创建信息。每个对象都知道如何揭示出适当的信息,以及如何对自身进行克隆。所以一种新类型加入系统的时候,factory()方法不需要任何改变。

为解决原型的创建问题,一个方法是添加大量方法,用它们支持新对象的创建。但在Java 1.1 中,如果拥有指向Class 对象的一个句柄,那么它已经提供了对创建新对象的支持。利用Java 1.1 的“反射”技术,即便我们只有指向Class 对象的一个句柄,亦可正常地调用一个构建器。这对原型问题的解决无疑是个完美的方案。

原型列表将由指向所有想创建的Class 对象的一个句柄列表间接地表示。除此之外,假如原型处理失败,则factory()方法会认为由于一个特定的Class 对象不在列表中,所以会尝试装载它。通过以这种方式动态装载原型,Trash 类根本不需要知道自己要操纵的是什么类型。因此,在我们添加新类型时不需要作出任何形式的修改。于是,我们可在本章剩余的部分方便地重复利用它。

2.1     代码

import java.util.*;

import java.lang.reflect.*;

public
abstract class
Trash {

private
double
weight;

Trash(double
wt) {

weight =
wt;

}

Trash() {

}

public
abstractdouble
value();

public
double
weight() {

return
weight;

}

// Sums thevalue of Trash in a bin:

public
staticvoid
sumValue(Vector
bin) {

Enumeration
e = bin.elements();

double
val = 0.0f;

while (e.hasMoreElements()) {

// One kind of RTTI:

// A dynamically-checked cast

Trash t = (Trash)
e.nextElement();

val +=
t.weight() * t.value();

System.out.println(

"weight of " +

// Using RTTI to get type

// information about the class:

t.getClass().getName() +" = "+
t.weight());

}

System.out.println("Total value = " +
val);

}

// Remainderof class provides support for

//prototyping:

public
staticclass
PrototypeNotFoundException
extends Exception {

}

public
staticclass
CannotCreateTrashException
extends Exception {

}

private
static
Vector trashTypes =
newVector();

public
static
Trash factory(Info
info)throwsPrototypeNotFoundException, CannotCreateTrashException {

for (int
i = 0; i <
trashTypes
.size();
i++) {

// Somehow determine the new type

// to create, and create one:

Class
tc = (Class) trashTypes.elementAt(i);

if (tc.getName().indexOf(info.id)!= -1) {

try {

// Get the dynamic constructor method

// that takes a double argument:

Constructor
ctor = tc.getConstructor(new Class[] {
double.class});

// Call the constructor to create a

// new object:

return (Trash)
ctor.newInstance(new Object[] {
new Double(info.data) });

} catch (Exception
ex) {

ex.printStackTrace();

throw
new
CannotCreateTrashException();

}

}

}

// Class was not in the list. Try to load it,

// but it must be in your class path!

try {

System.out.println("Loading "+
info.id);

trashTypes.addElement(Class.forName(info.id));

} catch (Exception
e) {

e.printStackTrace();

throw
new
PrototypeNotFoundException();

}

// Loaded successfully. Recursive call

// should work this time:

return
factory(info);

}

public
staticclass
Info {

public String
id;

public
double
data;

public Info(String
name, double
data) {

id =
name;

this.data =
data;

}

}

} /// :~

基本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[] {newDouble(info.data)}

在这种情况下,double 必须置入一个封装(容器)类的内部,使其真正成为这个对象数组的一部分。通过调用newInstance(),会提取出double,但大家可能会觉得稍微有些迷惑——参数既可能是double,也可能是Double,但在调用的时候必须用Double 传递。幸运的是,这个问题只存在于基本数据类型中间。

理解了具体的过程后,再来创建一个新对象,并且只为它提供一个Class 句柄,事情就变得非常简单了。就目前的情况来说,内部循环中的return 永远不会执行,我们在终点就会退出。在这儿,程序动态装载Class对象,并把它加入trashTypes(垃圾类型)列表,从而试图纠正这个问题。若仍然找不到真正有问题的地方,同时装载又是成功的,那么就重复调用factory 方法,重新试一遍。

正如大家会看到的那样,这种设计方案最大的优点就是不需要改动代码。无论在什么情况下,它都能正常地使用(假定所有Trash 子类都包含了一个构建器,用以获取单个double 参数)。

提到对象的创建,这一方案确实已将新类型加入系统所需的变动严格地“本地化”了。但在使用RTTI 的过程中,却存在着一个严重的问题,这里已明确地显露出来。程序表面上工作得很好,但却永远侦测到不能“硬纸板”(Cardboard)这种新的废品类型——即使列表里确实有一个硬纸板类型!之所以会出现这种情况,完全是由于使用了RTTI 的缘故。RTTI 只会查找那些我们告诉它查找的东西。RTTI 在这里错误的用法是“系统中的每种类型”都进行了测试,而不是仅测试一种类型或者一个类型子集。正如大家以后会看到的那样,在测试每一种类型时可换用其他方式来运用多形性特征。但假如以这种形式过多地使用RTTI,而且又在自己的系统里添加了一种新类型,很容易就会忘记在程序里作出适当的改动,从而埋下以后难以发现的Bug。因此,在这种情况下避免使用RTTI
是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。

时间: 2024-10-08 11:26:09

78.JAVA编程思想——改进设计的相关文章

75.JAVA编程思想——设计范式

75.JAVA编程思想--设计范式 向大家介绍重要但却并不是那么传统的"范式"(Pattern)程序设计方法. 在向面向对象程序设计的演化过程中,或许最重要的一步就是"设计范式"(Design Pattern)的问世.它在由Gamma,Helm 和Johnson 编著的<DesignPatterns>一书中被定义成一个"里程碑"(该书由Addison-Wesley 于1995 年出版).那本书列出了解决这个问题的23 种不同的方法.我

39.JAVA编程思想之外篇——JAVA图形化设计精简大全一文覆盖

39.JAVA编程思想之外篇--JAVA图形化设计精简大全一文覆盖 欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51204948 目录 Java图形化界面设计--容器(JFrame)...1 Java基本类(JFC)...1 l     AWTAbstract Window Toolkit(AWT)抽象窗口工具包... 2 l     Swing包... 2 l     AWT和Swing的区别... 6 Swing基本框

java编程思想总结(三)

java编程思想总结(三) java编程思想总结是一个持续更新的系列,是本人对自己多年工作中使用到java的一个经验性总结,也是温故而知新吧,因为很多基础的东西过了这么多年,平时工作中用不到也会遗忘掉,所以看看书,上上网,查查资料,也算是记录下自己的笔记吧,过一段时间之后再来看看也是蛮不错的,也希望能帮助到正在学习的人们,本系列将要总结一下几点: 面向对象的编程思想 java的基本语法 一些有趣的框架解析 实战项目的整体思路 代码的优化以及性能调优的几种方案 整体项目的规划和视角 其它遗漏的东西

java编程思想学习(1)

Smalltalk 这是第一种成功的面向对象程序设计语言,也是Java 的基础语言.Smalltalk (java的基础语言)的五大基本特征: (1) 所有东西都是对象.可将对象想象成一种新型变量:它保存着数据,但可要求它对自身进行操作.理论上讲,可从要解决的问题身上提出所有概念性的组件,然后在程序中将其表达为一个对象. (2) 程序是一大堆对象的组合:通过消息传递,各对象知道自己该做些什么.为了向对象发出请求,需向那个对象"发送一条消息".更具体地讲,可将消息想象为一个调用请求,它调

77.JAVA编程思想——模拟垃圾回收

77.JAVA编程思想--模拟垃圾回收 这个问题的本质是若将垃圾丢进单个垃圾筒,事实上是未经分类的.但在以后,某些特殊的信息必须恢复,以便对垃圾正确地归类.在最开始的解决方案中,RTTI 扮演了关键的角色.这并不是一种普通的设计,因为它增加了一个新的限制.正是这个限制使问题变得非常有趣--它更象我们在工作中碰到的那些非常麻烦的问题.这个额外的限制是:垃圾抵达垃圾回收站时,它们全都是混合在一起的.程序必须为那些垃圾的分类定出一个模型.这正是RTTI 发挥作用的地方:我们有大量不知名的垃圾,程序将正

JAVA编程规则【转自java编程思想】

本附录包含了大量有用的建议,帮助大家进行低级程序设计,并提供了代码编写的一般性指导: (1) 类名首字母应该大写.字段.方法以及对象(句柄)的首字母应小写.对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母.例如:ThisIsAClassNamethisIsMethodOrFieldName若在定义中出现了常数初始化字符,则大写static final基本类型标识符中的所有字母.这样便可标志出它们属于编译期的常数.Java包(Package)属于一种特殊情况:它们全都是小

79.JAVA编程思想——抽象应用

79.JAVA编程思想--抽象应用 1     抽象应用 接下来该考虑一下设计方案剩下的部分了--在哪里使用类?既然归类到垃圾箱的办法非常不雅且过于暴露,为什么不隔离那个过程,把它隐藏到一个类里呢?这就是著名的"如果必须做不雅的事情,至少应将其本地化到一个类里"规则. 现在,只要一种新类型的Trash 加入方法,对TrashSorter 对象的初始化就必须变动.可以想象,TrashSorter 类看起来应该象下面这个样子: class TrashSorter extends Vecto

83.JAVA编程思想——关于JAVA性能

83.JAVA编程思想--关于JAVA性能 Java 语言特别强调准确性,但可靠的行为要以性能作为代价.这一特点反映在自动收集垃圾.严格的运行期检查.完整的字节码检查以及保守的运行期同步等等方面.对一个解释型的虚拟机来说,由于目前有大量平台可供挑选,所以进一步阻碍了性能的发挥. "先做完它,再逐步完善.幸好需要改进的地方通常不会太多." 1     基本方法 只有正确和完整地检测了程序后,再可着手解决性能方面的问题: (1) 在现实环境中检测程序的性能.若符合要求,则目标达到.若不符合

33.JAVA编程思想——JAVA IO File类

33.JAVA编程思想--JAVA IO File类 RandomAccessFile用于包括了已知长度记录的文件.以便我们能用 seek()从一条记录移至还有一条:然后读取或改动那些记录. 各记录的长度并不一定同样:仅仅要知道它们有多大以及置于文件何处就可以. 首先.我们有点难以相信RandomAccessFile 不属于InputStream 或者OutputStream 分层结构的一部分.除了恰巧实现了DataInput 以及DataOutput(这两者亦由 DataInputStream