叶公之家

导航

类的关系(转贴自OOWGSOO,备查)

类的关系


版本:1.0
作者:oowgsoo
审核:
日期:2009年02月01日
备注:

前言

    现实世界的事物是普遍联系的,作为对现实世界模拟的软件世界也应该是普遍联系的。在面向对象的系统中,最基本的单元是类,正确识别类之间的联系,并用UML图形表示出来对于面向对象的系统来说是很重要的工作,而这正是本文的目的所在。

依赖和耦合

    一个类发生了改变导致了另一个类也要发生变化,这种联系就是依赖。依赖关系在面向对象的系统中无处不在,例如我们稍微改变了一点头文件中类的成员变量或者成员函数的声明,那么你就要相应的修改对该类依赖的其它类,系统就要重新编译。这个过程并不轻松,有时候甚至是绝对不允许的,例如一套基础库的设计,你敢随意修改你头文件中类的声明吗?那些底下使用你的库的用户们还不得把你骂死。

    我们用耦合来度量类之间依赖的程度。耦合度越高,系统的内聚性越强,但是扩展性越差;反之,耦合度越低,系统的扩展性越强,但是内聚性越差。

    依赖和耦合是对同一个问题的两个不同的思考角度。因此两者是相辅相成的,缺一不可。

    以依赖的方向为依据,类的关系可以分为:

  1. 无依赖,两个类之间完全没有关系,互相独立,耦合程度为零
  2. 单向依赖,一个类依赖于另一个类,而后者并不依赖于前者,耦合程度中等
  3. 双向依赖,两个类之间互相依赖,其中的任何一个类改变必然导致另一个类也随之改变,耦合程度高

    上面的分类已经完备了,但是在实际应用中,这种分类还是太粗,不太实用。因为这种分类仅仅是从依赖的个数(也就是量)上考虑了耦合度,却没有考虑到类本身的稳定程度(也就是质),如果两个类本身就非常稳定,几乎是从来不改变的,那么即使是双向依赖,它们的耦合度依旧很低。那么在面向对象的设计中,有没有本身稳定性就很高的类呢?有啊,那不就是抽象类嘛,或者也可以说是接口类,抽象类的稳定性是远高于具体类的稳定性的,否则就肯定是设计错误了。在综合考虑了变化的量和质两个正交的角度后,再排除掉无依赖即零耦合的特殊情况后,我们就得到了下表:

这里,我们把类的关系分为六种(再次强调是除了无依赖即零耦合的特殊情况后),其中具体类-具体类的双向依赖耦合程度最高,抽象类-抽象类的单向依赖耦合程度最低。

UML的分类

    上一节,我们综合考虑了依赖的方向和类的稳定程度,把类的关系分为了六种(实际上是7种),本来以为问题就此解决了,但是在设计UML的那些大师们看起来,这也太小儿科了,依赖的方向?用箭头就可以表示了;具体类还是抽象类吗?看看类名是不是倾斜的就知道了。我们还需要在两个类之间的那根连线上继续做文章,再继续的细分。于是就有了继承,组合,聚合,关联,依赖五个新名词,这些名词也实在是复杂啊,很多人直接就被它们雷晕了。网上相关的资料不少,但是不是写得太简单,就是写得太复杂,所以我还是自己动手吧。

    要理解设计UML的那些大师们为什么要对类的关系这么分类?我们需要站在大师们的位置上从以下几个角度来考虑问题:

  1. 首先我们要知道,引入这些名词是为了进一步细化度量类和类之间的耦合关系,因此我们要学会从这些类之间的关系理解类与类之间的耦合程度。如果要用人际关系来做类比的话,类与类之间的耦合程度也就是人与人之间的亲疏程度,所以下面我会举几个人际关系方面的例子来做类比
  2. 其次我们要知道,引入这些关系归根结底是要在类图中体现出来的,因此我们要学会这些类之间的关系如何用不同的图形来表示
  3. 其次我们要知道,理论上类图和程序代码是可以相互转换的,因此我们要学会这些类之间的关系是怎样在程序代码中体现的
  4. 最后,无论是类和类之间的耦合关系,类图还是代码本质上都是问题本身决定的,是需求决定了设计

依赖

    什么是依赖?最开始已经说了,所有的联系都可以叫做依赖,那里的依赖无疑是广义的依赖。而这里UML的依赖可没有那么泛泛,它是狭义的,专指两个类之间比彼此独立的强点,又比保持联系弱点的那种联系。

    用人际关系来类比,这里指的是那种和你只有一面之缘的人的关系,比陌生人强点又比朋友弱点的那种关系。比如你在公共汽车上看到一个美女,上前搭讪两句,但是人家没理你,故事就结束了,这就是你们之间的关系就是单向依赖。如果美女也回了你两句,然后故事就结束了,那么你们之间的关系就是双向依赖。

    在UML中,依赖的图形表示是虚线+箭头,箭头指向被依赖的类:

    在程序代码中,具有依赖关系的类常常被声明为成员函数的参数,而不声明为类的成员变量。在上面的例子中,Me类中并没有Girl类的指针声明。

关联

    什么是关联?UML中的关联关系是指两个类之间即保持联系,又不是严格所属关系的那种联系。

    用人际关系来类比,这里指的是那种普通朋友之间的关系。比如如果刚才那位公共汽车上的美女和你聊得很好,给你留下了电话号码,你忙不迭把人家的电话号码记在了自己的电话薄里。这时你们之间的关系就是关联。

    在UML中,关联的图形表示是实线+箭头,箭头指向被关联的类:

    在程序代码中,具有关联关系的类常常被声明为类的成员变量。在上面的例子中,Me类中有Girl类的指针声明。Me对象构造时该指针一般是NULL,Me对象存在时的一定时刻该指针被设置或被取消,也可能多次设置和取消,Me对象析构是一般不会理会该指针的析构,顶多是再被设置为NULL。

聚合

    什么是关联?UML中的聚合关系是指两个类之间有严格的所属关系,一般是整体和部分的关系,但是对方却不是唯一属于自己的那种关系。

    用人际关系来类比,这里指的是亲密爱人之间的关系。比如如果刚才那位公共汽车上的美女终于和你喜结良缘了,你拥有她,她也拥有你。这时你们之间的关系就是聚合,而且还是双向聚合。当然你要知道,你的爱人也是可以被其它对象共享的,比如你的子女。

    在UML中,聚合的图形表示是空心菱形+实线+箭头,箭头指向被拥有的类:

    在程序代码中,具有聚合关系的类常常被声明为类的成员变量,这同具有关联的类一样。因此在代码上区别关联关系和聚合关系是有些困难的。如果一定要从语义上有所属关系的是聚合,没有所属关系的是关联来区分的话也很勉强。爱人之间到底是平行关系还是所属关系呢?这应该和具体需求相关。我的建议是只在很确定的情况下进行区分,多数情况下根本不需要区分,反正代码也是一样的。

组合

    什么是组合?UML中的组合关系是指两个类之间有严格的所属关系,一般是整体和部分的关系,而且对方是唯一属于自己的那种关系。

    用人际关系来类比,这里指的是那种不求同年同月同日生,但求同年同月同日死的特殊情感了。这种超越了普通人情世故的强烈情感在现实世界中是可与而不可求的,但是在软件世界里却是随处可见的。

    在UML中,聚合的图形表示是实心菱形+实线+箭头,箭头指向被拥有的类:

    在程序代码中,具有组合关系的类常常被声明为类的成员变量,这同具有关联的类和具有聚合的类一样。区别在于对这个指针的处理上,因为组合关系中该指针不能被其它类共享,所以该指针的生 命期是完全被所属类控制的。在上面的例子中,Me类中有Wife类的指针声明。Me对象构造时该指针一般也要new出来,即使不是这样,也应该在Me对象初始化时尽早的new出来,Me对象析构时还要处理该指针的析构。 但是这并不绝对的,对象生命期的设计是一件技术活,需要考虑很多东西,不能用一个公式来套。

    刚才说了,在软件世界里组合关系随处可见。例如下面的Imp惯用法

头文件:


class AppImp;

class App
{
public:
    App();
    
~App();

    
void fun();

private:
    AppImp
* m_pImp;
};

实现文件:

class AppImp
{
public:
    AppImp()
    {
    }
    
~AppImp()
    {
    }
    
void fun()
    {
    }
};

App::App()
{
    m_pImp 
= new AppImp;
}

App::
~App()
{
    delete m_pImp;
}

void App::fun()
{
    m_pImp
->fun();
}

    关联,聚合,组合在代码上看起来很象,我们也可以得到这个公式“关联->聚合->组合”,即聚合是一种特殊的关联,组合是一种特殊的聚合。

    从耦合程度上考虑,也有一个公式“关联<聚合<组合”,即关联的耦合程度小于聚合的耦合程度,而聚合的耦合程度小于组合的耦合程度。

继承

    关于继承其实是不需要多说的,搞面向对象的连这个都搞不懂就干脆不要搞了。继承是一种is-A的关系,严格的受限于里氏替换原则:“任何使用基类的地方一定可以用该基类的子类替换”。

    需要特别指出的子对象的问题,考虑以下代码:

class Head
{
};

class Human
{
public:
    Head head;
};

    很明显,人肯定有一个头,就所属关系和生命期考虑,Human和Head之间的关系应该是组合关系。而且因为没有使用指针,这里也避免了对象生命期的管理问题。但是,这种组合关系是没有意义的,设计模式中有一条简单的原则:多用组合,少用继承。如果我们设计的组合是这个样子的,那么设计模式一点用处也没有了,因为这里的head不是指针,无法动态替换。所以这种情况其实是子对象,而不是组合关系。

    看过《深度探索C++对象模型》的都知道,派生和子对象的内存布局是一模一样的。所以多数时候我宁愿把派生关系当作是子对象的特例。即使是多派生也是可以用一个类中包含多个子对象 来模拟。最后,如果一个类中有几个同类型的子对象,按道理来说应该允许多派生来自同一个类,例如:

class Human : public Head, public Arm, public Arm
{
public:
};

    可惜C++语法并不支持这种语法,所以就内存布局来说,子对象甚至强于派生关系。

posted on 2011-10-04 11:45  oldsnow  阅读(165)  评论(0编辑  收藏  举报