UML类图学习笔记(《游戏人工智能编程案例精粹》附录B)

940 阅读9分钟

概述

该笔记是取自《游戏人工智能编程案例精粹》中对UML的介绍,由于是翻译过来的,有一些术语可能不太一样。
具体会有以下不同:

  1. 关联关系在本书中用了适航性这种不常见也难理解的描述,常用的说法是将双向关联和单向关联统称为关联,以双向和单向划分
  2. 共享聚集和组合聚集的说法也被分别称为聚合关系和组合关系
  3. 概括也被称为继承关系

UML类图

通用建模语言(UML)是一个有用的工具,用于面向对象的分析和设计。UML的一部分———类图被频繁贯穿应用在本书中,因为这种类型的图擅长清楚而简洁地描述对象之间的静态关系。

类名、属性和操作

首先,我们从一个类的名字、属性和操作开始。它是通过一个被划分成三部分的长方形表示的。用粗体字表示类的名字,它位于长方形的顶端,属性被卸载名字的下面,操作被列在底部。

例如,如果游戏中的一个对象是一辆RacingCar(赛车),它的详细说明被显示在图3中。

当然,一个RacingCar对象可能比这更复杂,但我们只需要列出我们感兴趣的属性和操作。如果需要的话,在后面的阶段中,这个类能够很容易地被充实。(我往往根本不现实它的任何属性或操作,仅仅使用类图简单地显示对象之间的关系)。注意一个类的操作定义了它的接口。

一个属性的类型可能在它的名字后面列出来不,并且由冒号分开。一种操作的返回值可以用同样的方式显示,可以作为参数的类型。参见图4.

整本书我很少使用“名字:类型”格式的参数,因为它会使图太大,以至于不能合适地放在一页中。相反,我仅仅列出类型,或有时用一个描述性的名字,如果类型可以从中推断出来。

属性和操作的可见性

每个类的属性和操作有一个可见性与它相联系。例如,属性要么是公众的、私有的或保护的。这些属性用标志“+”表示公共的、“-”表示私有的、“#”表示保护的。图5显示了RacingCar对象,带有属性和操作的可见性。

对于类型,当画类图的时候没必要列出它的可见性;只是在它们直接对你正在构建的设计部分非常重要时,才需要把它们显示出来。

当所有的属性、操作、类型、可见性等等都被详细地说明时,把类转换成代码是非常容易的。例如,RacingCar对象的C++代码看起来如下所示:

class RacingCar
{
private:
    vector m_vPosition;
    vaetor m_vVelocity;
public:
    void Steer(float amount){...}
    void Accelerate(float amount){...}
    vector GetPostition() const {return m_vPosition;}
};

关系

类本身没有多大的用处。在面向对象的设计中,每个对象通常与一个或者更多个其他对象有关系,譬如父子类型的继承关系、或类方法与它们的参数之间的关系。下面描述的UML符号,用来表示各种特殊类型的关系。

关联

两个类之间的关联代表一个连接,或那些类的实例之间的连接,可以用一条实线来表示它。如果两个类之间,其中一个包含到另一个的永久引用则说两者之间存在关联。

图6显示了一个RacingCar和Driver对象之间的关联。

这幅类图告诉我们,一辆赛车被司机驾驶,并且司机驾驶一辆赛车。它还告诉我们,一个RacingCar的实例保有对Driver实例的一个永久引用(通过一个指针、实例或引用),反之亦然。在这个例子的两端都被明确地用被称为角色名字的描述性标签命名。虽然许多时候,这不是必要的。因为通常是隐含地给出类的名字和连接它们的关联类型。我更喜欢只带名字的角色,因为我认为名字是绝对必要的,因为我觉得它能使一个复杂的类图更加简单,更容易被人理解。

多重性

关联的末端也许具有多重性,它指出了参与到关系中的实例数目。例如,一辆赛车可能只有1个或者0个司机,一个司机或开车或不开车。这可被显示在图7中,用0到1的数据指定范围。

图8显示了用普通书写方式说明一个无限的范围以及一个0到1的范围。但经常(当然是在本书中)你将看到这些关系是用速记来表示的。被显示在图9中。单个的星号表示一个在0和无限之间的无边界的范围,如果在一个关联的末端没有任何数字或一个星号,暗指一种单一的关系。

多重性可以代表离散值得一个集合。例如,汽车可能有2个或4个门。在图9中仅仅给出关联。我们可以推断RacingCar类的一个接口可能看起来如下:

class RacingCar
{
public:
    Driver* getDriver() const;
    void SetDriver(Driver* pNewDriver);
    bool isBeingGriven() const;
    void AddSponsor(Sponsor* pSponsor);
    void RemoveSponor(Sponsor* pSponsor);
    int GetNumSponsors() const;
};

适航性

到目前为止,你看到的关联时双向的:一个RacingCar了解Driver的一个实例,并且哪个Driver的实例也了解RacingCar的这个实例。一个RacingCar了解每个Sponsor实例,并且每个Sponsor也了解RacingCar。然而经常的,你需要去表达一个单向的关联,如RacingCar是不需要知道观众在看它,但重要的是,观众知道他要看的赛车。这是一个单向的关系,可以通过给关联的相应末端添加一个箭头来表示它。参见图10.

也要注意这个数字清楚地表述了一个Spectator可能看到任何跑车的数目。

共享聚集与组合聚集

聚集是关联的一种特殊情况,它表示关系的一部分。例如,胳膊是身体的一部分。有两种类型的聚集:共享的和组合的。共享聚集是部分可以在整体之间的共享,组合聚集是部分由整体拥有。例如,Mesh(3D多边形模型)描述一辆赛车的形状并添加纹理渲染成一个展示品,可以被许多跑车所共享。因此这可以表示为一个共享聚集,在图中是用空心菱形来表示。参见图11。

注意,共享聚集意味着当RacingCar被毁坏时,它的Mesh不被毁坏。还要注意怎么用图来表示一个Mesh对象对RacingCar对象一无所知。

组合聚集时一种更强的关系,并且意味着部分与整体生死相依。我们仍然用RaicngCar的例子,我们可以说底盘和车之间就有这种类型的关系。底盘是由RacingCar完全拥有的,当车被毁坏时,它也被毁坏。这关系用一个实心的菱形来表示,显示在图12中。

共享聚集和关联之间有非常微妙的差别。例如,迄今在设计中谈论的一个Spectator跟一个RacingCar之间的关系,这个关系已经被显示为关联,但是由于许多不同的Spectators可以看同一辆车,你可能会认为,用共享聚集来显示这种关系也是可以的。但是,一个Spectator不是一辆跑车这个整体的一部分,因此,他们之间的关系时一个关联而不是聚集。

概括

概括时描述具有公共特性的类之间关系的一种方法。关于C++,概括描述的是一种集中继承的关系。例如,设计也许需要不同的类型的RacingCar,它们是RacingCar的子类,能为特定类型的比赛提供车辆,比如公路车赛用车、方程式1赛车或巡回比赛用车。这种关系用一个空心三角来显示在关联末端的基类上,如图13所示。

在面向对象的设计中,我们经常使用一个抽象类的概念去定义一个接口,可以被其所有子类来实现。这可以明确地由UML来描述,其中斜体字用于描述类名以及任何它的抽象操作。所以如果RacingCar是作为一个带有纯虚函数的Update的抽象类来实现,它和其他跑车之间的关系显示在图14中。

注意,某些人更喜欢在类名下任何抽象操作名字的后面增加"{抽象}"而使关系更加明确,这种做法被显示在图15中。

依赖

通常你会发现由于种种原因,一个类依赖于另一个类,然而他们之间的关系又不是关联。发生这种情况有各种各样的原因。例如,如果类A的一个方法的参数就是对类B的引用的话,那么A对B有一个依赖。依赖的另外一个例子就是A通过第三方给B发一则消息。这将成为设计组合事件处理的一个典型代表。

使用一个末端带箭头的虚线去显示一个依赖,并且可以选择合适的标记。图16显示了RacingCar对RaceTrack(跑道)具有一个依赖。

批注

批注是一个附加的特征,在特定的特征上,如果需要以某些方式进一步解释的话,你可以使用它放大。例如我用批注在本书的类图中需要之处添加了伪代码。批注被描述为一个带有折角的长方形并用一条虚线与你感兴趣的地方相连接。图17显示批注是如何用于解释一个RacingCar的UpdatePhysics方法是如何遍历它的四个轮子的,调用每个轮子的Update方法。