📄 chapter16.htm
字号:
930页下程序<br>
<br>
这种方案适用于任何频繁用到的集合类。除此以外,集合类还可提供它自己的适配器类,并实现Fillable(稍后即可看到,在DynaTrash.java中)。<br>
<br>
3. 原型机制的重复应用<br>
现在,大家可以看到采用原型技术的、修订过的RecycleA.java版本了:<br>
<br>
931页程序<br>
<br>
所有Trash对象——以及ParseTrash及支撑类——现在都成为名为c16.trash的一个包的一部分,所以它们可以简单地导入。<br>
无论打开包含了Trash描述信息的数据文件,还是对那个文件进行解析,所有涉及到的操作均已封装到static(静态)方法ParseTrash.fillBin()里。所以它现在已经不是我们设计过程中要注意的一个重点。在本章剩余的部分,大家经常都会看到无论添加的是什么类型的新类,ParseTrash.fillBin()都会持续工作,不会发生改变,这无疑是一种优良的设计方案。<br>
提到对象的创建,这一方案确实已将新类型加入系统所需的变动严格地“本地化”了。但在使用RTTI的过程中,却存在着一个严重的问题,这里已明确地显露出来。程序表面上工作得很好,但却永远侦测到不能“硬纸板”(Cardboard)这种新的废品类型——即使列表里确实有一个硬纸板类型!之所以会出现这种情况,完全是由于使用了RTTI的缘故。RTTI只会查找那些我们告诉它查找的东西。RTTI在这里错误的用法是“系统中的每种类型”都进行了测试,而不是仅测试一种类型或者一个类型子集。正如大家以后会看到的那样,在测试每一种类型时可换用其他方式来运用多形性特征。但假如以这种形式过多地使用RTTI,而且又在自己的系统里添加了一种新类型,很容易就会忘记在程序里作出适当的改动,从而埋下以后难以发现的Bug。因此,在这种情况下避免使用RTTI是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。<br>
<br>
16.5 抽象的应用<br>
走到这一步,接下来该考虑一下设计方案剩下的部分了——在哪里使用类?既然归类到垃圾箱的办法非常不雅且过于暴露,为什么不隔离那个过程,把它隐藏到一个类里呢?这就是著名的“如果必须做不雅的事情,至少应将其本地化到一个类里”规则。看起来就象下面这样:<br>
<br>
932页图<br>
<br>
现在,只要一种新类型的Trash加入方法,对TrashSorter对象的初始化就必须变动。可以想象,TrashSorter类看起来应该象下面这个样子:<br>
class TrashSorter extends Vector {<br>
void sort(Trash t) { /* ... */ }<br>
}<br>
也就是说,TrashSorter是由一系列句柄构成的Vector(系列),而那些句柄指向的又是由Trash句柄构成的Vector;利用addElement(),可以安装新的TrashSorter,如下所示:<br>
TrashSorter ts = new TrashSorter();<br>
ts.addElement(new Vector());<br>
但是现在,sort()却成为一个问题。用静态方式编码的方法如何应付一种新类型加入的事实呢?为解决这个问题,必须从sort()里将类型信息删除,使其需要做的所有事情就是调用一个通用方法,用它照料涉及类型处理的所有细节。这当然是对一个动态绑定方法进行描述的另一种方式。所以sort()会在序列中简单地遍历,并为每个Vector都调用一个动态绑定方法。由于这个方法的任务是收集它感兴趣的垃圾片,所以称之为grab(Trash)。结构现在变成了下面这样:<br>
<br>
933页图<br>
<br>
其中,TrashSorter需要调用每个grab()方法;然后根据当前Vector容纳的是什么类型,会获得一个不同的结果。也就是说,Vector必须留意自己容纳的类型。解决这个问题的传统方法是创建一个基础“Trash
bin”(垃圾筒)类,并为希望容纳的每个不同的类型都继承一个新的衍生类。若Java有一个参数化的类型机制,那就也许是最直接的方法。但对于这种机制应该为我们构建的各个类,我们不应该进行麻烦的手工编码,以后的“观察”方式提供了一种更好的编码方式。<br>
OOP设计一条基本的准则是“为状态的变化使用数据成员,为行为的变化使用多性形”。对于容纳Paper(纸张)的Vector,以及容纳Glass(玻璃)的Vector,大家最开始或许会认为分别用于它们的grab()方法肯定会产生不同的行为。但具体如何却完全取决于类型,而不是其他什么东西。可将其解释成一种不同的状态,而且由于Java有一个类可表示类型(Class),所以可用它判断特定的Tbin要容纳什么类型的Trash。<br>
用于Tbin的构建器要求我们为其传递自己选择的一个Class。这样做可告诉Vector它希望容纳的是什么类型。随后,grab()方法用Class
BinType和RTTI来检查我们传递给它的Trash对象是否与它希望收集的类型相符。<br>
下面列出完整的解决方案。设定为注释的编号(如*1*)便于大家对照程序后面列出的说明。<br>
<br>
934-935页程序<br>
<br>
(1) TbinList容纳一系列Tbin句柄,所以在查找与我们传递给它的Trash对象相符的情况时,sort()能通过Tbin继承。<br>
(2) sortBin()允许我们将一个完整的Tbin传递进去,而且它会在Tbin里遍历,挑选出每种Trash,并将其归类到特定的Tbin中。请注意这些代码的通用性:新类型加入时,它本身不需要任何改动。只要新类型加入(或发生其他事件)时大量代码都不需要变化,就表明我们设计的是一个容易扩展的系统。<br>
(3)
现在可以体会添加新类型有多么容易了。为支持添加,只需要改动几行代码。如确实有必要,甚至可以进一步地改进设计,使更多的代码都保持“固定”。<br>
(4) 一个方法调用使bin的内容归类到对应的、特定类型的垃圾筒里。<br>
<br>
16.6 多重派遣<br>
上述设计方案肯定是令人满意的。系统内新类型的加入涉及添加或修改不同的类,但没有必要在系统内对代码作大范围的改动。除此以外,RTTI并不象它在RecycleA.java里那样被不当地使用。然而,我们仍然有可能更深入一步,以最“纯”的角度来看待RTTI,考虑如何在垃圾分类系统中将它完全消灭。<br>
为达到这个目标,首先必须认识到:对所有与不同类型有特殊关联的活动来说——比如侦测一种垃圾的具体类型,并把它置入适当的垃圾筒里——这些活动都应当通过多形性以及动态绑定加以控制。<br>
以前的例子都是先按类型排序,再对属于某种特殊类型的一系列元素进行操作。现在一旦需要操作特定的类型,就请先停下来想一想。事实上,多形性(动态绑定的方法调用)整个的宗旨就是帮我们管理与不同类型有特殊关联的信息。既然如此,为什么还要自己去检查类型呢?<br>
答案在于大家或许不以为然的一个道理:Java只执行单一派遣。也就是说,假如对多个类型未知的对象执行某项操作,Java只会为那些类型中的一种调用动态绑定机制。这当然不能解决问题,所以最后不得不人工判断某些类型,才能有效地产生自己的动态绑定行为。<br>
为解决这个缺陷,我们需要用到“多重派遣”机制,这意味着需要建立一个配置,使单一方法调用能产生多个动态方法调用,从而在一次处理过程中正确判断出多种类型。为达到这个要求,需要对多个类型结构进行操作:每一次派遣都需要一个类型结构。下面的例子将对两个结构进行操作:现有的Trash系列以及由垃圾筒(Trash
Bin)的类型构成的一个系列——不同的垃圾或废品将置入这些筒内。第二个分级结构并非绝对显然的。在这种情况下,我们需要人为地创建它,以执行多重派遣(由于本例只涉及两次派遣,所以称为“双重派遣”)。<br>
<br>
16.6.1 实现双重派遣<br>
记住多形性只能通过方法调用才能表现出来,所以假如想使双重派遣正确进行,必须执行两个方法调用:在每种结构中都用一个来判断其中的类型。在Trash结构中,将使用一个新的方法调用addToBin(),它采用的参数是由TypeBin构成的一个数组。那个方法将在数组中遍历,尝试将自己加入适当的垃圾筒,这里正是双重派遣发生的地方。<br>
<br>
937页图<br>
<br>
新建立的分级结构是TypeBin,其中包含了它自己的一个方法,名为add(),而且也应用了多形性。但要注意一个新特点:add()已进行了“过载”处理,可接受不同的垃圾类型作为参数。因此,双重满足机制的一个关键点是它也要涉及到过载。<br>
程序的重新设计也带来了一个问题:现在的基础类Trash必须包含一个addToBin()方法。为解决这个问题,一个最直接的办法是复制所有代码,并修改基础类。然而,假如没有对源码的控制权,那么还有另一个办法可以考虑:将addToBin()方法置入一个接口内部,保持Trash不变,并继承新的、特殊的类型Aluminum,Paper,Glass以及Cardboard。我们在这里准备采取后一个办法。<br>
这个设计方案中用到的大多数类都必须设为public(公用)属性,所以它们放置于自己的类内。下面列出接口代码:<br>
<br>
938页上程序<br>
<br>
在Aluminum,Paper,Glass以及Cardboard每个特定的子类型内,都会实现接口TypeBinMember的addToBin()方法,但每种情况下使用的代码“似乎”都是完全一样的:<br>
<br>
938-940页程序<br>
<br>
每个addToBin()内的代码会为数组中的每个TypeBin对象调用add()。但请注意参数:this。对Trash的每个子类来说,this的类型都是不同的,所以不能认为代码“完全”一样——尽管以后在Java里加入参数化类型机制后便可认为一样。这是双重派遣的第一个部分,因为一旦进入这个方法内部,便可知道到底是Aluminum,Paper,还是其他什么垃圾类型。在对add()的调用过程中,这种信息是通过this的类型传递的。编译器会分析出对add()正确的过载版本的调用。但由于tb[i]会产生指向基础类型TypeBin的一个句柄,所以最终会调用一个不同的方法——具体什么方法取决于当前选择的TypeBin的类型。那就是第二次派遣。<br>
下面是TypeBin的基础类:<br>
<br>
940页程序<br>
<br>
可以看到,过载的add()方法全都会返回false。如果未在衍生类里对方法进行过载,它就会一直返回false,而且调用者(目前是addToBin())会认为当前Trash对象尚未成功加入一个集合,所以会继续查找正确的集合。<br>
在TypeBin的每一个子类中,都只有一个过载的方法会被过载——具体取决于准备创建的是什么垃圾筒类型。举个例子来说,CardboardBin会过载add(DDCardboard)。过载的方法会将垃圾对象加入它的集合,并返回true。而CardboardBin中剩余的所有add()方法都会继续返回false,因为它们尚未过载。事实上,假如在这里采用了参数化类型机制,Java代码的自动创建就要方便得多(使用C++的“模板”,我们不必费事地为子类编码,或者将addToBin()方法置入Trash里;Java在这方面尚有待改进)。<br>
由于对这个例子来说,垃圾的类型已经定制并置入一个不同的目录,所以需要用一个不同的垃圾数据文件令其运转起来。下面是一个示范性的DDTrash.dat:<br>
<br>
941-942页程序<br>
<br>
下面列出程序剩余的部分:<br>
<br>
942-943页程序<br>
<br>
其中,TrashBinSet封装了各种不同类型的TypeBin,同时还有sortIntoBins()方法。所有双重派遣事件都会在那个方法里发生。可以看到,一旦设置好结构,再归类成各种TypeBin的工作就变得十分简单了。除此以外,两个动态方法调用的效率可能也比其他排序方法高一些。<br>
注意这个系统的方便性主要体现在main()中,同时还要注意到任何特定的类型信息在main()中都是完全独立的。只与Trash基础类接口通信的其他所有方法都不会受到Trash类中发生的改变的干扰。<br>
添加新类型需要作出的改动是完全孤立的:我们随同addToBin()方法继承Trash的新类型,然后继承一个新的TypeBin(这实际只是一个副本,可以简单地编辑),最后将一种新类型加入TrashBinSet的集合初化化过程。<br>
<br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -