第4章
一个标准的面向对象解决方案
概述
本章对我们在第3章讨论的问题,“一个迫切需要灵活代码的问题”,给出一个初步的解决方案。这是一个合理的初步尝试,它能够迅速地解决问题。不过它却漏掉了一个重要的系统需求:CAD/CAM系统持续演化时所需要的灵活性。
在本章,我基于面向对象描述了一个解决方案。它并不大,但确实能起作用。
注意:在本章的主体部分,我将仅展示Java代码示例。对应的C++代码示例在本章的末尾。
用特殊手段来解决问题
考虑第3章“一个迫切需要灵活代码的问题”里描述的那两个不同的CAD/CAM系统。我该怎样构建一个信息析取系统,才能使得不管使用哪一个CAD/CAM系统,对客户对象而言它看起来都是一样的呢?
通过思考如何解决这个问题,我推断如果我能解决槽的问题,那么我也能用同样的方案来处理剪切块、洞等特性的问题。通过对槽的思考,我发现我能够轻易地特化每一种情况。也就是说,我将有一个Slot类。在面对V1系统时,我将为它生成一个派生类,在面对V2系统时,我将为它生成另一个派生类。如图4-1所示。
图4-1 槽的设计
通过对每一个特性类型进行扩展,我就完成了这个解决方案,如图4-2所示。
图4-2信息析取问题的原始解决方案
当然,图4-2展示的是相当高层的设计。每个V1xxx类将会和相应的V1库通信,而每个V2xxx类则会和V2模型中的相应对象通信。
如果单独地来看每一个类,这个解决方案就更加容易想象了。
l V1Slot可通过记住它所从属的模型以及被实例化时它在V1系统中的ID来实现。这样,任何时候当V1Slot的一个方法被调用以获取信息时,这个方法将不得不调用V1中的一序列子例程从而得到相关的信息。
l V2Slot将会以一种更加简单的方式被实现,即每个V2Slot对象将会包含V2系统中的一个相应的槽对象。这样,任何时候当该对象被查询某种信息时,它将简单地将这个请求传送给OOGSlot对象并将响应回送到先前发出请求的那个客户对象。
图4-3展示了一张包含V1和V2系统的更为详细的图。
图4-3 初始方案
我将为这个设计中的两个类提供代码示例。这些示例仅仅用于帮助你理解如何实现这个设计。如果你觉得你能够实现这个设计,请随意略过下面的Java代码示例(C++代码示例在本章的末尾)。
示例 4-1 Java代码片段:
实例化V1特性
// 实例化特性的代码片断
// 不提供错误检验——仅用作演示
// 每一个特性对象需要知道和它对应的模型号码以及特性ID
// 以便在收到请求时获取信息
// 注意这样的信息是如何传送进每个对象的构造函数的
// 打开模型
modelNum = V1OpenModel(modelName);
nElements = V1GetNumberofElements(modelNum);
Feature features[] = new Feature[MAXFEATURES];
// 为模型中的每一个特性做
for(i= 0; i < nElements; i++) {
// 确定当前的特性并创建适当的特性对象
switch(V1GetType(modelNum, i)) {
case SLOT:
features[i] = new V1Slot(modelNum,
VlGetID(modelNum, i));
break;
case HOLE:
features[i] = new VlHole(modelNum,
VlGetID(modelNum, i));
break;
}
}
示例 4-2 Java代码片断:
V1方法的实现
// modelNum和myID是私有成员
// 它们包含对应模型和特性(在V1中的)的有关信息
class V1Slot {
double getx () {
// 为V1调用适当的方法以得到所需要的信息。注意:
// 为得到信息,这个方法实际上可能调用V1中的几个方法
return V1GetXforSlot(modelNum, myID);
}
}
class VlHole {
double getx () {
// 为V1调用适当的方法以得到所需要的信息。注意:
// 为得到信息,这个方法实际上可能调用V1中的几个方法
return V1GetXforHole(modelNum, myID);
}
}
示例 4-3 Java代码片段:
初始化V2特性
// 实例化特性的代码片断
// 不提供错误检验——仅用作演示
// 每一个特性对象需要知道它在V2中对应的特性
// 以便在收到请求时获取信息
// 注意这样的信息是如何传送进每个对象的构造函数的
// 打开模型
myModel = V2OpenModel(modelName);
nElements = myModel.getNumElements();
Feature features[] = new Feature[MAXFEATURES];
(待续)
示例 4-3 Java代码片段:[1]
初始化V2特性(继续)
OOGFeature *oogF;
// 为模型中的每一个特性做
for (i= 0; i < nElements; i++) {
// 确定当前的特性并创建适当的特性对象
oogF = myModel->getElement(i);
switch(oogF->myType()){
case SLOT:
features[i] = new V2Slot(oogF);
break;
case HOLE:
features[i] = new V2Hole(oogF);
break;
}
}
示例4-4 Java代码片段:
V2方法的实现
// oogF是V2中对应的特性对象
class V2Slot {
double getX (} {
// 调用oogF的适当方法以得到所需的信息。
return oogF->getX();
}
}
class V2Hole {
double getX () {
// 调用oogF的适当方法以得到所需的信息。
return oogF->getX();
}
}
在图4-3中,我增加了几个特性所需要的方法。注意它们是如何因特性的类型而不同的。这表明我没有在整个特性体系中使用多态。然而这并不成问题,因为不管怎样,这个专家系统总是需要知道它所拥有的特性的类型,它需要从不同类型的特性得到不同种类的信息。
This brings up the point that I am not so interested in polymorphism of the features. Rather,我需要能够插即用不同的CAD/CAM系统而不需要改变这个专家系统。
我想要做的——透明地处理多个CAD/CAM版本——给我几点暗示,这不是一个好方案:
方法中的重复——我能轻易想象那些调用V1系统的方法,它们之间将会有许多相似之处。比如Slot的V1getx和Hole的V1getx将会非常相似。
凌乱——它不总是一个好预示,但采用这个方案,它是另外一个增强我不适的因素。
高耦合——这个方案是高耦合的,因为各个特性彼此间接地相关联。这些关系表明如果以下事情发生,我们很可能需要修改所有的特性:
——需要一个新的CAD/CAM系统。
——修改一个已有的CAD/CAM系统。
低内聚——内聚是相当之低,因为执行核心功能的方法被分散在这些类之中。
然而我最关心的问题出现在我深入观察特性的时候。想象在CAD/CAM系统的第三个版本出现时会发生什么。组合爆炸将会杀死我们!看看图4-3中类图的第三行。
l 这里有五类特性。
l 每一类特性有两个类,每个CAD/CAM系统一个类。
l 当第三个版本加入时,每组将拥有三个类,而不是两个类。
l 我拥有不再是十个类,而是十五个类。
可以肯定,维护这样一个系统,我不会从中获得任何乐趣!
分析的一个陷阱:过早关注太多细节。
在开发过程中,我们的分析可能出现的一个普遍问题是我们过早的陷入到细节之中。这是自然的,因为细节问题容易处理。细节的解决方案通常是显而易见的,但却不一定是最好的开端。在拥抱细节之前,能延迟多久,就延迟多久。
在这个案例中,我达到一个目标:特性信息的一个共同的API。同时我还从职责的视角定义了对象。然而,我为此付出的代价是为每一件事情创建特殊的case。在我得到新的特殊的case时,我将不得不以同样的方式实现它们,并因此付出高昂的维护代价。
这是我第一个令人羞愧的解决方案,我很快就开始讨厌它。相比我在上面所给出的更多逻辑上的理由,我的直觉更多地助长了这种感觉。我觉得这里面有问题。
在这个案例中,我强烈地感觉到一定有一个更好的解决方案。然而,即便是两个小时以后,这依然是我能够想到的最好的解决方案。正如你将会在本书后面看到的,这个问题是我的通用途径。
注意你的直觉
对于设计质量,肠胃的直觉是一个强大得令人吃惊的指标。我建议开发人员学着去聆听他们的直觉。
说肠胃的直觉,我指的是当你看到某种你不喜欢的东西时,你胃里的那种感觉。我知道这听起来是不科学的(它确实不科学),但是我的经验常常告诉我,每当我的直觉不喜欢一个设计时,一个更好的设计就在某个角落。当然,有时候附近可能有好几个不同的角落,而且我不能总是确定它藏身何处。
总结
我展示了如果对每一件事情都使用special-casing,解决这个问题是多么容易。它允许我在不改变现状的前提下增加额外的方法。然而,这也有几个不利之处:高度的重复,低内聚以及类爆炸(来自将来的变化)。
对继承的过度信任将会导致比本该需要的(或者至少是,比我认为应该需要的)更加高昂的维护代价。
补充:C++代码示例
示例4-5 C++代码片段:
实例化V1特性
// 实例化特性的代码片断
// 不提供错误检验——仅用作演示
// 每一个特性对象需要知道和它对应的模型号码以及特性ID
// 以便在收到请求时获取信息
// 注意这样的信息是如何传送进每个对象的构造函数的
// 打开模型
modelNum = V1OpenModel(modelName);
nElements = V1GetNumberofElements(modelNum);
Feature *features[MAXFEATURES];
// 为模型中的每一个特性做
for(i = 0; i < nElements; i++) {
// 确定当前的特性并创建适当的特性对象
switch( V1GetType(modelNum, i)) {
case SLOT:
features[i] = new V1Slot(modelNum,
VlGetID(modelNum, i));
break;
case HOLE:
features [i] = new VlHole(modelNum,
VlGetID( modelNum, i));
break;
}
}
示例 4-6 C++代码片断:
V1方法的实现
// modelNum和myID是私有成员
// 它们包含对应模型和特性(在V1中的)的有关信息
double VlSlot::getX () {
// 为V1调用适当的方法以得到所需要的信息。注意:
// 为得到信息,这个方法实际上可能调用V1中的几个方法
return V1GetXforSlot( modelNum, myID);
}
double VlHole::getX (} {
// 为V1调用适当的方法以得到所需要的信息。注意:
// 为得到信息,这个方法实际上可能调用V1中的几个方法
return V1GetXforHole( modelNum, myID);
}
示例 4-7 C++ 代码片段:
初始化V2特性
// 实例化特性的代码片断
// 不提供错误检验——仅用作演示
// 每一个特性对象需要知道它在V2中对应的特性
// 以便在收到请求时获取信息
// 注意这样的信息是如何传送进每个对象的构造函数的
// 打开模型
myModel = V2OpenModel(modelName);
nElements = myModel->getNumElements();
Feature *features[MAXFEATURES];
OOGFeature *oogF;
// 为模型中的每一个特性做
for (i= 0; i < nElements; i++) {
(待续)
示例 4-7 C++代码片段:
初始化V2特性(继续)
// 确定当前的特性并创建适当的特性对象
oogF = myModel->getElement(i);
switch(oogF->myType()){
case SLOT:
features[i] = new V2Slot(oogF);
break;
case HOLE:
features[i] = new V2Hole(oogF);
break;
}
}
示例4-8 C++代码片段:
V2方法的实现
// oogF是V2中对应的特性对象
double V2Slot::getX (} {
// 调用oogF的适当方法以得到所需的信息。
return oogF->getX();
}
double V2Hole::getX () {
// 调用oogF的适当方法以得到所需的信息。
return oogF->getX();
}
[1] 译者注:紫色的代码片段在笔者参照的电子书中并无对应原文,是参照本章末尾的C++代码片断补充的。