《设计模式解析》第4章 一个标准的面向对象解决方案

类别:软件工程 点击:0 评论:0 推荐:
第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++代码片断补充的。

本文地址:http://com.8s8s.com/it/it34132.htm