OOP 和 UML






4.57/5 (58投票s)
2000 年 6 月 21 日

612968
UML 建模 OOP 设计概念简介
OOP 和 UML
引言
本文档的目的是向您提供有关 UML 及其使用方法的信息。
什么是 UML?
UML(统一建模语言)是一种标准的表示法,用于在开发面向对象的程序的第一步中对现实世界对象进行建模。它描述了一种统一的语言,用于指定、可视化、构建和记录软件系统的构件。
为什么要建模?
在开始实际编写软件代码之前为软件系统开发模型,可以看作是为你想要建造的一栋大建筑准备蓝图。拥有蓝图是必不可少的。尽管这并不意味着每次在软件中引入一个简单的类时都必须绘制模型。您需要自行判断是否需要模型。
一些表示法规则
图形及其内容
大多数 UML 图都是由路径连接的节点组成的图形。信息主要体现在类型上,而不是符号的大小或位置上。有三种重要的视觉关系:
-
连接(通常是线条)
- 包含(带有边界的二维形状)
- 视觉附加(一个对象靠近另一个对象)
UML 表示法旨在绘制在二维平面上。
UML 表示法基本上使用了四种图形构造:
-
图标 – 图标是固定大小和形状的图形。它不会扩展以容纳内容。图标可能出现在区域符号内、路径的终止符处或作为独立的符号,这些符号可能连接到路径也可能不连接到路径。
-
二维符号 – 二维符号具有可变的长度和宽度,并且可以扩展以容纳其他内容,例如列表、字符串或其他符号。其中许多被划分为相似或不同类型的隔间。拖动或删除一个二维符号会影响其内容和连接到它的任何路径。
-
路径 – 端点连接的线段序列。从概念上讲,路径是单个拓扑实体,尽管其线段可以在图形上进行操作。线段不能独立于其路径而存在。路径的两端始终连接到其他图形符号(没有悬空线)。路径可以有终止符,这些是出现在路径末端的图标,用于限定路径符号的含义。
-
字符串 – 以“未解析”的形式呈现各种信息。UML 假定表示法中字符串的每次使用都具有一个语法,该语法可以解析为底层模型信息。例如,为属性、操作和转换提供了语法。字符串可以作为符号或符号隔间的单个元素、列表中的元素、附加到符号或路径的标签,或者作为图中的独立元素。
-
关系
属性和行为
每个对象都有各种属性。属性是名称/值对。例如,我的年龄是 22。属性名称是“age”,值是“22”。对象也有行为。我可以站立、行走、睡觉等。
关系
存在不同种类的关系:
依赖关系是指一个对象必须了解另一个对象。
在简单的依赖关系中,我们只知道一个对象了解另一个对象。例如:如果一个类需要包含另一个类的头文件,这就建立了依赖关系。
我们使用虚线箭头绘制依赖关系。如果 a 依赖于 b,请确保箭头指向 b。
在关联关系中,对象的状态取决于另一个对象。
在关联关系中,我们说,在了解一个对象状态的过程中,您必须了解与第二个对象的关联。存在许多类型的关联,它们模拟了现实世界的关系,例如“拥有”(Arjen 拥有这辆自行车)、“为……工作”(Raymond 为 Harry 工作)等等。
在关联关系中,两个对象有很强的联系,但它们都不是彼此的一部分。这种关系比依赖关系更强;存在影响关系双方的关联。
类 a 和类 b 之间的关联由连接两个类的线条表示。
如果线上没有箭头,则假定关联是双向的。单向关联显示如下:
为了提高类图的清晰度,两个对象之间的关联可以命名。
聚合(Aggregation)模拟整体/部分关系。
对象通常由其他对象组成。汽车由方向盘、发动机、变速箱等组成。其中每个组件本身都可以是一个对象。汽车与其组件之间的特殊关联称为聚合。
聚合关系通过在关联末端(靠近聚合类的一侧)放置一个白色菱形来表示。如果 b 聚合 a,则 a 是 b 的一部分,但它们的生命周期是独立的。
组合(Composition)模拟一个对象是另一个对象不可或缺的一部分的关系。
通常,对象的组件只有在整个对象存在时才会出现。例如,一个人可能由心脏、肺等多个部分组成。如果您正在对一个人进行建模,那么心脏和肺的生命周期将直接受聚合人(该人)的生命周期控制。我们将这种特殊关系称为组合。
在聚合中,部件可以独立存在。虽然我的汽车由车轮、轮胎和收音机组成,但这些组件中的每一个在汽车被制造之前就已经存在。在组合中,包含对象的生命周期与包含对象的生命周期绑定。
组合通过在关联末端(靠近组合类的一侧)放置一个黑色菱形来表示。如果 b 由 a 组成,则 b 控制 a 的生命周期。
继承
继承是对象之间的特化/泛化关系。
我们(人类)继承了根据周围事物的行为和特征来创建类别的能力。这最好通过例子来说明:如果某个东西会呼吸并且会移动,我们就说它是动物。如果这些会移动和呼吸的东西之一会产仔并哺乳,我们就说它是哺乳动物。我们知道哺乳动物是动物的一种,因此我们可以预测,如果我们看到一只哺乳动物,它很可能也会呼吸并且会到处走动。
如果一只哺乳动物会叫并且摇尾巴,我们就说它是狗。如果它不停地叫,并且在你脚边跑来跑去要求关注,我们就认为它是一只梗犬。这些分类中的每一个都为我们提供了额外的信息。完成后,我们就创建了一个类型的层次结构。
有些动物是哺乳动物,有些是爬行动物。有些哺乳动物是狗,有些是马。每种类型都会共享某些特征,这有助于我们理解它们并预测它们的行为和属性。
只有一个正确的方法可以绘制它。

一旦我们有了这种分类,我们就可以看到,沿着动物层次结构向上阅读揭示了共享特征的泛化。
同样,我们可以创建一个汽车模型。为此,我们必须问自己一些问题:
什么是汽车?汽车与卡车、人、岩石有何不同?面向对象编程的乐趣之一在于,这些问题变得与我们相关;理解我们如何感知和思考现实世界中的对象直接关系到我们如何在模型中设计这些对象。
从一个角度来看,汽车是其各部分的总和:方向盘、刹车、座椅、前灯。在这里,我们从聚合的角度思考。从第二个同样正确的角度来看,汽车是一种交通工具。
因为汽车是一种交通工具,所以它会移动并运载东西。这是交通工具的本质。汽车从其“父”类型“交通工具”继承了“移动”和“运载东西”的特征。
我们也知道汽车特化了交通工具。它们是一种特殊的交通工具,一种符合联邦汽车规格的交通工具。
我们可以通过继承来模拟这种关系。我们说汽车类型公共继承自交通工具类型;汽车是一种交通工具。
公共继承建立了“is-a”(是一种)关系。它创建了一个父类(交通工具)和一个派生类(汽车),并意味着汽车是交通工具类型的一种特化。所有适用于交通工具的事情都应该适用于汽车,但反之则不然。汽车可能会特化其移动方式,但它应该能够移动。
什么是机动车?这是一个不同层级的特化。机动车是指任何由发动机驱动的车辆。汽车是其中一种类型,卡车是另一种。我们也可以通过继承来模拟这些更复杂的关系。

哪个模型更好?取决于您要建模的内容!您如何决定使用哪个模型?问问自己。关于“机动车”是否有我想建模的内容?我是否要建模其他非机动车辆?如果您这样做了,您应该使用第二个模型。举例说明:假设您想为马车创建两个类。

公共继承
公共继承的一个关键方面是它应该只模拟特化/泛化,而不能模拟其他任何东西!如果您想继承实现,但并未建立“is-a”关系,则应使用私有继承。
私有继承建立了“implemented-in-terms-of”(按……实现)关系,而不是“is-a”关系。
多重继承
C++ 提供的一个功能是多重继承。多重继承允许一个类继承自多个基类,从而引入两个或多个类的成员和方法。
在简单的多重继承中,两个基类是不相关的。下面展示了一个多重继承的例子。同时请注意函数在本模型中的显示方式。
在这个相当简单的模型中,Griffin 类继承自 Lion 和 Eagle。这意味着 Griffin 可以 eatMeat()、roar()、squawk() 和 fly()。当 Lion 和 Eagle 共享一个共同的基类(例如 Animal)时,就会出现问题。
这个共同的基类 Animal 可能有 Griffin 现在会继承两次的方法或成员变量。当您调用 Griffin 的 Sleep() 方法时,编译器不知道您想调用哪个 Sleep()。作为 Griffin 类的设计者,您必须了解这些关系,并准备好解决它们造成的歧义。C++ 通过提供虚拟继承来促进这一点。
|
|
没有虚拟继承 |
有虚拟继承 |
使用虚拟继承,Griffin 只继承 Animal 的一个副本的成员,从而解决了歧义。问题在于 Lion 和 Eagle 类必须知道它们可能参与多重继承关系;virtual 关键字必须放在它们继承的声明上,而不是 Griffin 的声明上。
聚合
当您需要聚合时使用多重继承
您如何知道何时使用多重继承以及何时避免使用它?汽车应该继承方向盘、轮胎和车门吗?警车应该继承市政财产和交通工具吗?
第一个指导原则是,公共继承应始终模拟特化。对此的常见说法是,继承应模拟“is-a”关系,而聚合应模拟“has-a”(拥有)关系。
汽车是方向盘吗?显然不是。您可能会争辩说,汽车是方向盘、轮胎和一套车门的组合,但这并不在继承中建模。汽车不是这些东西的特化;它是这些东西的聚合。汽车有一个方向盘,它有车门,它有轮胎。您不应该让汽车继承自车门的另一个好理由是,汽车通常有多个车门。这不是一种可以用继承建模的关系。
警车既是交通工具又是市政财产吗?显然两者都是。事实上,它特化了两者。因此,多重继承在这里非常合理。
基类和派生类
派生类应该知道它们的基类是谁,并且它们依赖于它们的基类。而基类不应该知道它们的派生类。不要将派生类的头文件放在基类文件中。
您应该非常警惕任何需要向下转换继承层次结构的设计。当您询问指针的“真实”(运行时)类并将其强制转换为派生类型时,您是在向下转换。理论上,基类指针应该是多态的,找出指针的“真实”类型并调用“正确”的方法应该留给编译器。
向下转换最常见的用途是调用基类中不存在的方法。您应该问自己的问题是,为什么您会处于需要这样做的情况下。如果运行时类型的知识应该是隐藏的,那么为什么您要向下转换?
单实例类
您还应该非常注意那些总是只有一个实例的派生类。不要将其与单例混淆,单例是指应用程序只需要一种类型的单个实例,例如只有一个文档或只有一个数据库。
绘制类
显示成员
假设您想创建一个 CFloatPoint 类,它有两个成员:x 和 y,它们的类型都是“float”,还有一个函数“Empty()”,该函数将两个成员重置为 0.00000。
首先,绘制类本身。
现在,我们希望成员 x 和 y 在模型中可见。
如您所见,x 和 y 都是私有的(锁形符号),类型为“float”。
现在,我们希望函数 Empty() 在模型中可见。
注释
假设您需要为您的类提供一些额外信息。您可以通过添加一个注释轻松做到这一点,注释如下所示:
结论
使用的软件
Visual Modeler
请记住,这些模型是使用 Visual Modeler 创建的,Visual Modeler 随 Visual Studio Enterprise Edition 一起提供,因此您可以尝试自己绘制它们。我不会在这里解释 Visual Modeler 的工作原理,请查阅手册或 MSDN 库以获取更多信息。
本文档的未来版本
我认为本文档是 OOP 和 UML 的一个良好开端。与文档“不同的编程风格”一起,它们为面向对象编程世界提供了良好的入门教程。
UML 的许多功能并未包含在此文档中。其中之一是所谓的“用例”,这是一个单独的话题。这将在一个尚未撰写的新文档中进行解释。
Alex Marbus
参考文献
什么是 UML?
http://www.whatis.com/uml.htm
OMG 统一建模规范(1.3 版,1999 年 6 月)
www.rational.com/uml/resources/documentation/index.jtmpl
面向对象分析与设计入门
Jesse Libery
ISBN 1-861001-33-9
Wrox Press Inc