- UML 建模、设计与分析:从新手到高手
- 夏丽华
- 6539字
- 2025-02-27 18:54:23
4.1 类图的概念
构建面向对象模型的基础是类、对象以及它们之间的关系,可以在不同类型的系统(例如商务软件、嵌入式系统和分布式系统等)中应用面向对象技术。不同系统中所描述的类是各种各样的,例如,在某个商务信息系统中,包含的类可以是顾客、协议书、发票、债务等;在某个工程技术系统中,包含的类可以有传感器、显示器、I/O卡、发动机等。
在面向对象的处理中,类图处于核心地位,它提供了用于定义和使用对象的主要规则,同时,类图是正向工程(将模型转化为代码)的主要资源,是逆向工程(将代码转化为模型)的生成物。因此,类图是任何面向对象系统的核心,随之也成了最常用的UML图。本节将详细介绍与它相关的知识,包括类图的概念、类的表示和如何定义一个类等相关内容。
4.1.1 类图概述
类图是描述类、接口以及它们之间关系的图,是一种静态模型,显示了系统中各个类的静态结构。类图根据系统中的类以及各个类的关系描述系统的静态视图,可以用某种面向对象的语言实现类图中的类。
类图是面向对象系统建模中最常用和最基本的图之一,其他许多图(如状态图、协作图、组件图和配置图等)都是在类图的基础上进一步描述了系统其他方面的特性。类图可以包含类、接口、依赖关系、泛化关系、关联关系和实现关系等模型元素。另外,在类图中也可以包含注释、约束、包或子系统。
UML类图中常见的关系有以下几种:泛化、实现、关联、聚合、组合以及依赖。其中,聚合和组合关系都是关联关系的一种。这些关系的强弱顺序不同,排序结果为:泛化=实现>组合>聚合>关联>依赖。
类图用于对系统的静态视图(它用于描述系统的功能需求)建模,通常以如下所示的某种方式使用类图。
□ 对系统的词汇建模 在进行系统建模时,通常首先构造系统的基本词汇,以描述系统的边界。在对词汇进行建模时,通常需要判断哪些抽象是系统的一部分,哪些抽象位于系统边界之外。
□ 对协作建模 协作是一些协同工作的类、接口和其他元素的共同体,其中元素协作时的功能强于它们单独工作时的功能之和。系统分析员可以用类图描述图形化系统中的类及它们之间的关系。
□ 对数据库模式建模 很多情况下都需要在关系数据库中存储永久信息,这时可以使用类图对数据库模式进行建模。
下图列举了一个简单的类图示例,它起到一个引导的作用,目的在于使读者对类图有一个直观浅显的了解。

类图通过分析用例和问题域,可以建立系统中的类,然后再把逻辑上相关的类封装成包,这样就可以直观清晰地展现出系统的层次关系。
但是,使用类图时也需要遵循如下原则。
□ 简化原则 在项目的初始阶段不要使用所有的符号,只要能够有效表达就可以。
□ 分层理解原则 根据项目开发的不同阶段使用不同层次的类图进行表达,以方便理解,不要一开始就陷入实现类图的细节中。
□ 关注关键点原则 不要为每个事物都画一个模型,只把精力放到关键的位置即可。
4.1.2 类
类是构成类图的基础,也是面向对象系统组织结构的核心。要使用类图,需要了解类和对象之间的区别。类是对资源的定义,它所包含的信息主要用来描述某种类型实体的特征以及对该类型实体的使用方法。对象是具体的实体,它遵守类制定的规则。从软件的角度看,程序通常包含的是类的集合以及类所定义的行为,而实际创建信息和管理信息的是遵守类的规则的对象。
类定义了一组具有状态和行为的对象,这些对象具有相同的属性、操作、关系和语义。其中,属性和关系用来描述状态。属性通常用没有身份的数据值表示,如数字和字符串;关联则用有身份的对象之间的关系来表示。行为由操作来描述,方法是操作的实现。
为了支持对身份、属性和操作的定义,UML规范采用一个具有3个预定义分栏的图标表示类,分栏中包含的信息有:名称(Name)、属性(Attribute)和操作(Operation),它们对应着类的基本元素,如下图所示。

当将类绘制在类图中时,名称分栏是必须出现的,而属性分栏和操作分栏则可以出现或不出现。上图显示了所有的分栏,另外3种形式如下图所示。

当隐藏某个分栏时,并非表明某个分栏不存在。只显示当前需要注意的分栏,可以使图形更加直观、清晰。
类在它的包含者(可以是包或者另一个类)内必须有唯一的名称。类对它的包含者来说是可见的,可见性规定了类能够怎样被位于可见者之外的类所使用。类的多重性说明了类可以具有多少个实例,通常情况下可以有0个或多个。
下面将详细介绍类的名称、属性和操作在类图中的具体表示方法和含义。
1.名称
类名书写在名称分栏的中部,它通常表示为一个名词,既不带前缀,也不带后缀。为类命名时最好能够反映类所代表的问题域中的概念,并且要清楚准确,不能含糊不清。类名可分为简单名称和路径名称。简单名称只有类名,没有前缀;路径名称中可以包含由类所在包的名称表示的前缀,如下图所示。

其中,Student是类的名称,Person是Student类所在包的名称。
2.属性
类的属性也称为特性,它描述了类在软件系统中代表的事物(即对象)所具备的特性,这些特性是该类的所有对象所共有的。类可以有任意数目的属性,也可以没有属性。在系统建模时只抽取那些对系统有用的特性作为类的属性,通过这些属性可以识别该类的对象。例如,可以将学生姓名、编号、出生年月、所在班级、职务等特性作为Student类的属性。
从系统处理的角度来看,事物的特性中只有其值能被改变的那些才可以作为类的属性。UML中描述类属性的语法格式如下所示:
[可见性] 属性名 [:类型] [=初始值] [{属性字符串}]
上述语法中,属性包含5部分:可见性、属性名、类型、初始值和属性字符串。除了属性名外,其他内容都是可有可无的,可以根据需要选用上面列出的某些项。
1)可见性
可见性用于指定它所描述的属性能否被其他类访问,以及能以何种方式访问。在UML中并未规定默认的可见性,如果在属性的左边没有标识任何符号,表明该属性的可见性尚未定义,而并非取了默认的可见性。
最常用的可见性类型有3种,分别为公有(Public)、私有(Private)和被保护(Protected)类型。
□ 被声明为Public的属性和操作可以在它所在类的外部被查看、使用和更新。在类里被声明为Public的属性和操作共同构成了类的公共接口。类的公共接口由可以被其他类访问及使用的属性和操作组成,这表示公共接口是该类与其他类联系的部分。类的公共接口应尽可能减少变化,以防止任何使用该类的地方进行不必要的改变。
□ 被声明为Protected的属性和操作可以被类的其他方法访问,也可以被任何继承类所声明的方法访问,但是,非继承的类无法访问Protected属性和操作。即使用Protected声明的属性和操作只可以被该类和该类的子类使用,而其他类无法使用。
□ Private可见性是限制最为严格的可见性类型,只有包含Private元素的类本身,才能使用Private属性中的数据,或者调用Private操作。
注意
对于是否应该声明为Public属性是有不同观点的。许多面向对象的设计者对Public属性存在抱怨,因为这会将类的属性向系统的其余部分公开,违反了面向对象的信息隐蔽的原则。因此,最好避免使用Public属性。
除了以上3种类型的可见性之外,其他类型的可见性可由程序设计语言定义。需要注意的是,公有和私有可见性一般在表达类图时是必需的。UML中Public类型用符号“+”表示,Private类型用符号“-”表示,Protected类型用符号“#”表示。这几种类型符号在类中的表示如下图所示。

在上图中,属性studentNo和studentBirth是类Student的私有属性,studentName是类的公有属性,studentClass和studentPosition属性是类被保护的属性,这些属性的可见性是由它们名称左边的符号指定的。
2)属性名
类的属性是类定义的一部分,每个属性都应有唯一的属性名,以标识该属性并以此区别于其他属性。属性名通常由描述所属类的特性的名词或名词短语组成,单字属性名小写,如果属性名包含了多个单词,则这些单词可以合并,且从第二个单词起,每个单词的首字母都应是大写,如上图中属性可见性的右边为属性名。
3)类型
每个属性都应指定其所属的数据类型。常用的数据类型有整型、实型、布尔型、枚举型等。这些类型在不同的编程语言中可能有不同的定义,可以在UML中使用目标语言中的类型表达式,这在软件开发的实施阶段是非常有用的。
除了上面提到的类型外,属性的数据类型还可以使用系统中的其他类或者用户自定义的数据类型。类的属性定义之后,类的所有对象的状态由其属性的特定值所决定。
4)初始值
开发人员可以为属性设置初始值,设置初始值可以防止因漏掉某些取值而破坏系统的完整性,并且为用户提供易用性。为Student类的相关属性指定数据类型和初始值后,效果如下图所示。

从上图中可以看出属性与数据类型之间要用冒号分隔,数据类型与初始值之间用等号分隔。使用Microsoft Visio画图时,冒号和等号都是该软件自动添加的。
5)属性字符串
描述类属性的语法格式中的最后一项是属性字符串。属性字符串用来指定关于属性的其他信息,任何希望添加属性定义字符串但又没有合适地方可以加入的都可以放在属性字符串里。
除了上面的介绍外,还有一种类型的属性,它能被所属类的所有对象共享,这就是类的作用域属性,或者叫作类变量(例如,Java类中的静态变量)。这类属性在类图中表示时要在属性名的下面加一条下画线。例如,开发人员可以将Student类中的studentPosition属性更改为类变量或者重新添加一个新的类变量。
属性可以代表一个以上的对象,实际上,属性能代表其类型的任意数目的对象。在程序设计时,属性用一个数组来实现体现了面向对象中对象之间关联的多重性。多重性指允许用户指定属性实际上代表一组对象集合,而且能够应用于内置属性及关联属性。如下图列出了属性对应的多重性,由于一名学生可以借阅多本图书,所以一个Student类可以对应多个Book类。

3.操作
属性仅仅描述了要处理的数据,而操作则描述了处理数据的具体方法。类的操作是对其所属对象的行为的抽象,相当于一个服务的实现,且该服务可以由类的任何对象请求,以影响其行为。属性是描述对象特征的值,操作用于操纵属性或执行其他动作。操作可以看作是类的接口,通过该接口可以实现内、外信息的交互,操作的具体实现被称作方法。
操作由返回值类型、名称和参数表进行描述,它们一起被称为操作签名。某个类的操作只能作用于该类的对象,一个类可以有任意数量的操作或者根本没有操作。UML中用于描述操作的语法形式如下:
[可见性] 操作名 [(参数表)] [:返回类型] [{属性字符串}]
上述语法形式中有可见性、参数表和返回类型等内容,其具体说明如下。
类操作的可见性类型包括公有(Public)、私有(Private)、受保护(Protected)和包内公有(Package)几种类型,UML类图中,它们可以分别用“+”“-”“#”和“~”来表示。如果某一对象能够访问操作所在的包,那么该对象就可以调用可见性为公有的操作;可见性为私有的操作只能被其所在类的对象访问;子类的对象可以调用父类中可见性为公有的操作;可见性为包内公有的操作可以被其所在包的对象访问。
在为系统建模时,操作名通常用描述类的行为的动词或者动词短语,操作名的第一个字母通常使用小写,当操作名包含多个单词时,这些单词要合并起来,并且从第二个单词起所有单词的首字母都是大写。
参数用来指定提供给操作以完成工作的信息,它是可选的,即操作可以有参数,也可以没有参数。如果参数表中包含多个参数时,各参数之间需要使用逗号隔开。当参数具有默认值时,如果操作的调用者没有为该参数提供相应的值,那么该参数将自动具有指定的默认值。例如,类15中deleteStudent操作表示根据学生的编号删除一个Student对象,执行该操作时只需要知道学生编号信息即可,如下图所示。

操作除了具有名称与参数外,还可以有返回类型。返回类型被指定在操作名称尾端的冒号之后,它指定了该操作返回的对象类型,如下图所示。如果某个操作返回时可以不注明返回值的类型,那么在具体的编程语言中可能需要添加关键字void来表示无返回值的情况。

除了上图中提供的每一个参数名及其数据类型外,还可以指定参数子句in、out或者inout。其中,in是默认的参数子句,通过值传递的参数使用in参数子句,或者不使用任何参数子句。通过值传递参数意味着把数据的副本发送到操作,因而操作不会改变值的主备份。如果希望修改传递到操作的参数值的主备份,需要使用inout类型的参数子句标记参数,这意味着值通过引用传递,操作中任何对参数值的修改也就是对变量主备份的修改。除此之外,还有一种out参数子句,使用该参数子句时,值不是被传递给操作,而是由操作把值返回给参数。
当需要在操作的定义中添加一些预定义元素之外的信息时,可以将它们作为属性字符串。
4.职责
所谓的职责,是指类或者其他元素的契约或义务,可以在类标记中操作分栏的下面另加一个分栏,用于说明类的职责。相关人员在创建类时需要声明该类的所有对象具有相同的状态和相同的行为,这些属性和操作正是要完成类的职责。描述类的职责可以使用一个短语、一个句子或者若干句子。
5.约束和注释
在类的标记中说明类的职责是消除二义性的一种非形式化的方法,而使用约束则是一种形式化的方法。约束指定了类应该满足的一个或者多个规则。约束在UML规范中是用由花括号括起来的文本表示的。如下图为Teacher类所添加的约束。

除约束外,还可以在类图中使用注释,以便为类添加更多的说明信息,注释可以包含文本和图形。如下图所示为Teacher类所添加的注释。

4.1.3 定义类
由于类是构成类图的基础,所以在构造类图之前首先要定义类,也就是将系统要处理的数据抽象为类的属性,将处理数据的方法抽象为类的操作。要准确地定义类,需要对问题域有透彻准确的理解。在定义类时,通常应当使用问题域中的概念,并且类的名字要用类实际代表的事物进行命名。
通过自我提问并回答下列问题,将有助于在建模时准确地定义类。
□ 在要解决的问题中有没有必须存储或处理的数据,如果有,那么这些数据可能就需要抽象为类,这里的数据可以是系统中出现的概念、事件,或者仅在某一时刻出现的事务。
□ 有没有外部系统,如果有,可以将外部系统抽象为类,该类可以是本系统所包含的类,也可以是能与本系统进行交互的类。
□ 有没有模板、类库或者组件等,如果有,这些可以作为类。
□ 系统中有什么角色,这些角色可以抽象为类,例如用户、客户等。
□ 系统中有没有被控制的设备,如果有,那么在系统中应该有与这些设备对应的类,以便能够通过这些类控制相应的设备。
通过自我提问并回答以上列出的问题,有助于在建模时发现需要定义的类。但定义类的基本依据仍然是系统的需求规格说明,应当认真分析系统的需求规格说明,进而确定需要为系统定义哪些类。另外,分析完用例和问题域后可以建立系统中的类,然后再把逻辑上相关的类封装成包,这样就可以直观清晰地展现出系统的层次关系。
4.1.4 接口
对Java或C#等高级语言不陌生的读者一定知道:一个类只能有一个父类(即该类只能继承一个类),但是如果用户想要继承两个或两个以上的类时应该怎么办?很简单,可以使用接口(Interface)。
接口是对对象行为的描述,但是它并没有给出对象的实现和状态,且接口是一组没有相应方法实现的操作,非常类似于仅包含抽象方法的抽象类。接口中只包含操作,而不包含属性,且接口没有对外界可见的关联。
一个类可以实现多个接口,使用接口可避免许多与多重继承相关的问题,因此使用接口比使用抽象类要安全得多。如在Java和C#等新型编程语言中允许类实现多个接口,但只能继承一个通用类或抽象类。
接口通常被描述为抽象操作,即只是用操作名、参数表和返回类型说明接口的行为,而操作的实现部分将出现在使用该接口的元素中。可以将接口想成非常简单的协议,它规定了实现该接口时必须实现的操作。接口的具体实现过程、方法对调用该接口的对象而言是透明的。在进行系统建模时,接口起到十分重要的作用,因为模型元素之间的协作是通过接口进行的。相关人员可以为类、组件和包定义接口,利用接口说明类、组件和包能够支持的行为。一个结构良好的系统,通常都定义了比较规范的接口。
UML中,接口可以使用构造型的类表示,也可以使用一个“球形”来表示,如下图所示演示了接口实现的两种方法。

接口与抽象类一样,都不能实例化为对象。在UML中,接口可以使用一个带有名称的小圆圈来表示,并且可以通过一条Realize(实现关系)线与实现它的类相连接,如下图所示。

如果使用构造型表示接口,则由于实现接口的类与接口之间是依赖关系,所以用一端带有箭头的虚线表示这个实现关系,如下图所示。

如果某个接口是在一个特定类中实现的,则使用该接口的类仅依赖于特定接口中的操作,而不依赖于接口实现类中的其他部分。如果类实现了接口,但未实现该接口指定的所有操作,那么此类必须声明为抽象类。使用接口可以很好地将类所需要的行为与该行为如何被实现完全分开。