小山

付出最大努力,追求最高成就,享受最佳生活,收获无悔人生

博客园 首页 新随笔 联系 订阅 管理

· 类元

类元是模型中的离散概念,拥有身份、状态、行为和关系。有几种类元包括类、接口和数据类型。其他几种类元是行为概念、环境事物、执行结构的具体化。这些类元中包括用例、参与者、构件、节点和子系统。 图 4–1 列出了几种类元和它们的功能。元模型术语类元中包括了所有这些概念,因为类是我们所最熟悉的术语,所以先讨论它,再根据类与其他概念的区别来定义其他概念。

类元

功能

表示法

参与者

系统的外部用户

模型系统中的概念

状态类

局限于某个给定状态的类

类元角色

在合作中局限于某个使用的类元

构件

系统的一个物理组成单元

数据类型

无身份得一组原始值的描述符

Name

接口

刻划行为特征的操作命名集

节点

计算资源

信号

对象间的异步通信

子系统

作为且有规范、实现和身份的单元的包

用例

与外界代理交互中的实体行为说明

表 4–1 各种类元


· 类

类代表了被建模的应用领域中的离散概念—物理实体(如飞机)、商业事物(如一份订单)、逻辑事物(如广播计划)、应用事物(如取消键)、计算机领域的事物(如哈希表)或行为事物(如一项任务)。类是有着相同结构、行为和关系的一组对象的描述符号。所用的属性与操作都被附在类或其他类元上。类是面向对象系统组织结构的核心。

对象是具有身份、状态和可激发行为的离散实体。对象是用来构造实际运行系统的个体;类是用来理解和描述众多个别对象的个别概念。

类定义了一组有着状态和行为的对象。属性和关联用来描述状态。属性通常用没有身份的纯数据值表示,如数字和字符串。关联则用有身份的对象之间的关系表示。个体行为由操作来描述,方法是操作的实现。对象的生命期由附加给类的状态机来描述。类的表示法是一个矩形,由带有类名、属性和操作的分格框组成。如 图 4–1 所示。


图 4–1 类表示法

一组类可以用泛化关系和建立在其内的继承机制分享公用的状态和行为描述。泛化使更具体的类(子类)与含有几个子类公同特性的更普通的类(超类)联系起来。一个类可以有零个或多个父类(超类)和零个或多个后代(子类)。一个类从它的双亲和祖先那里继承状态和行为描述,并且定义它的后代所继承的状态和行为描述。

类在它的包含者内有唯一的名字,这个包含者通常可能是一个包但有时也可能是另一个类。类对它的包含者来说是可见性,可见性说明它如何被位于它的可见者之外的类所利用。类的多重性说明了有多少个实例可以存在,通常情况下,可以有多个(零个或多个,没有明确限制),但在执行过程中一个实例只属于一个类。

· 接口

接口是在没有给出对象的实现和状态的情况下对对象行为的描述。接口包含操作但不包含属性,并且它没有对外界可见的关联。一个或多个类或构件可以实现一个接口,并且每个类都可以实现接口中的操作。

· 数据类型

数据类型用以描述缺少身份的简单数据值。数据类型包括数字、字符串、枚举型数值。数据类型通过值来传递,并且是不可变的实体。数据类型没有属性,但是可以有操作。操作不改变数据值,但是可以把数据值作为结果返回。

· 含义分层

类可以存在于模型的几种含义层中,包括分析层、设计层和实现层。当表现真实世界的概念时,说明实际的状态、关系和行为是很重要的。但是像信息隐藏、有效性、可见性和方法这些实现方面的概念与真实世界的概念无关(它们是设计层的概念)。分析层的类代表了在应用域中的逻辑概念或应用本身。分析层模型应该尽可能少地表示建模的系统,并在不涉及到执行和构造的情况下,充分说明系统的必要逻辑组成。

当表示高层次的设计时,有些概念与类直接相关,这些概念包括特定类的状态定位、对象之间导航的效率、外部行为和内部实现的分离和准确操作的描述。设计层类表示了将状态信息和其上的操作封装于一个离散的单元,它说明了关键的设计决定、信息定位和对象的功能。设计层类包含真实世界和计算机系统两方面的内容。

最后,当实现程序代码时,类的形式与所选择的语言紧密相关。如果一个通用类的功能不能直接用语言来实现,那么不得不放弃它们。实现层类直接与程序代码相对应。

同一个系统可以容纳多个层次的类,面向实现的类会在模型中实现更逻辑化的类。一个实现类表示用特定程序设计语言声明一个类,它得到了一个按照语言所要求的准确格式的类。然而,在许多情况下,分析、设计和实现信息可嵌套在一个单独的类中。

· 关系

类元之间的关系有关联、泛化、流及各种形式的依赖关系,包括实现关系和使用关系(参见 表4–2 )。

关联关系描述了给定类的单独对象之间语义上的连接。关联提供了不同类间对象可以相互作用的连接。其余的关系涉及到类元自身的描述,而不是它们的实例。

泛化关系使父类元(超类)与更具体的后代类元(子类)连接在一起。泛化有利于类元的描述,可以不用多余的声明,每个声明都需加上从其父类继承来的描述。继承机制利用泛化关系的附加描述构造了完整的类元描述。泛化和继承允许不同的类元分享属性、操作和它们共有的关系,而不用重复说明。

实现关系将说明和实现联系起来。接口是对行为而非实现的说明,而类之中则包含了实现的结构。一个或多个类可以实现一个接口,而每个类分别实现接口中的操作。

流关系将一个对象的两个版本以连续的方式连接起来。它表示一个对象的值、状态和位置的转换。流关系可以将类元角色在一次相互作用中连接起来。流的种类包括变成(同一个对象的不同版本)和拷贝(从现有对象创造出一个新的对象)两种。

依赖关系将行为和实现与影响其他类的类联系起来。除了实现关系以外,还有好几种依赖关系,包括跟踪关系(不同模型中元素之间的一种松散连接)、精化关系(两个不同层次意义之间的一种映射)、使用关系(在模型中需要另一个元素的存在)、绑定关系(为模板参数指定值)。使用依赖关系经常被用来表示具体实现间的关系,如代码层实现关系。在概括模型的组织单元,例如包时,依赖关系很有用,它在其上显示了系统的构架。例如编译方面的约束可通过依赖关系来表示。

表 4–2 关系的种类

关系

功能

表示法

关联

类实例之间连接的描述

依赖

两个模型元素间的关系

在相继时间内一个对象的两种形式的关系

泛化

更概括的描述和更具体的种类间的关系,适用于继承

实现

说明和实现间的关系

使用

一个元素需要别的元素提供适当功能的情况



· 关联

关联描述了系统中对象或实例之间的离散连接。关联将一个含有两个或多个有序表的类元,在允许复制的情况下连接起来。最普通的关联是一对类元之间的二元关联。关联的实例之一是链。每个链由一组对象(一个有序列表)构成,每个对象来自于相应的类。二元链包含一对对象。

关联带有系统中各个对象之间关系的信息。当系统执行时,对象之间的连接被建立和销毁。关联关系是整个系统中使用的“胶粘剂”,如果没有它,那么只剩下不能一起工作的孤立的类。

在关联中如果同一个类出现不止一次,那么一个单独的对象就可以与自己关联。如果同一个类在一个关联中出现两次,那么两个实例就不必是同一个对象,通常的情况都如此。

一个类的关联的任何一个连接点都叫做关联端,与类有关的许多信息都附在它的端点上。关联端有名字(角色名)和可见性等特性,而最重要的特性则是多重性,重性对于二元关联很重要,因为定义 n 元关联很复杂。

二元关联用一条连接两个类的连线表示。如 图 4–2 所示,连线上有相互关联的角色名而多重性则加在各个端点上。


图 4–2 关联表示法

如果一个关联既是类又是关联,即它是一个关联类,那么这个关联可以有它自己的属性(如 图 4 – 3 )。如果一个关联的属性在一组相关对象中是唯一的,那么它是一个限定符(如图 4-4 )。限定符是用来在关联中从一组相关对象中标识出独特对象的值。限定符对建模名字和身份代码是很重要的,同时它也是设计模型的索引。

 
图 4–3 关联类


图 4 – 4 限定关联

在分析阶段,关联表示对象之间的逻辑关系。没有必要指定方向或者关心如何去实现它们。应该尽量避免多余的关联,因为它们不会增加任何逻辑信息。在设计阶段,关联用来说明关于数据结构的设计决定和类之间职责的分离。此时,关联的方向性很重要,而且为了提高对象的存取效率和对特定类信息的定位,也可引入一些必要的多余关联。然而,在该建模阶段,关联不应该等于 C++ 语言中的指针。在设计阶段带有导航性的关联表示对一个类有用的状态信息,而且它们能够以多种方式映射到程序设计语言当中。关联可以用一个指针、被嵌套的类甚至完全独立的表对象来实现。其他几种设计属性包括可见性和链的可修改性。 图 4 – 5 表示了一些关联的设计特性。


图 4–5 关联的设计特性


1. 聚集和组成

聚集表示部分与整体关系的关联,它用端点带有空菱形的线段表示,空菱形与聚集类相连接。组成是更强形式的关联,整体有管理部分的特有的职责,它用一个实菱形物附在组成端表示。每个表示部分的类与表示整体的类之间有单独的关联,但是为了方便起见,连线结合在一起,现在整组关联就像一棵树。 图 4 – 6 表示了聚集关联和组成关联。


图 4–6 聚集和组成

2. 链

链是关联的一个实例。链即所涉及对象的一个有序表,每个对象都必须是关联中对应类的实例或此类后代的实例。系统中的链组成了系统的部分状态。链并不独立于对象而存在,它们从与之相关的对象中得到自己的身份(在数据库术语中,对象列表是链的键)。在概念上,关联与相关类明显不同。而在实际中,关联通常用相关类的指针来实现,但它们可以作为与其相连的类分离的包含体对象来实现。

3. 双向性

关联的不同端很容易辨认,哪怕它们都是同一种类。这仅仅意味着同一个类的不同对象是可以相互联系的。正是因为两端是可区分的,所以关联是不对称的(除了个别的例子外),且两个端点也是不能互相交换的。在通常情形下这是一个共识:就像动词短语中的主语和宾语不能互换一样。关联有时被认为是双向性的,这意味着逻辑关系在两个方向上都起作用。这个观点经常被错误理解,甚至包括一些方法学家。这并不意味着每个类“了解”其他类,或者说在实现中类与类之间可以互相访问。这仅仅意味着任何逻辑关系都有其反向性,无论这个反向性容不容易计算。如果关联只在一个方向横穿而不能在另一个方向横穿,那么关联就被认为有导航性。

为什么使用基本模型,而不用编程语言中流行的指针来表示关联?原因是模型试图说明系统实现的目的。如果两个类之间的关系在模型中用一对指针来表示,那么这两个指针仍然相关。关联方法表明关系在两个方向都有意义,而不管它们是如何实现的。将关联转化成一对用于实现的指针很容易,但是很难说明这两个指针是彼此互逆的,除非这是模型的一部分。
· 泛化

泛化关系是类元的一般描述和具体描述之间的关系,具体描述建立在一般描述的基础之上,并对其进行了扩展。具体描述与一般描述完全一致所有特性、成员和关系,并且包含补充的信息。例如,抵押是借贷中具体的一种,抵押保持了借贷的基本特性并且加入了附加的特性,如房子可以作为借贷的一种抵押品。一般描述被称作父,具体描述被称作子如借贷是父而抵押则是子。泛化在类元(类、接口、数据类型、用例、参与者、信号等等)、包、状态机和其他元素中使用。在类中,术语超类和子类代表父和子。

泛化用从子指向父的箭头表示,指向父的是一个空三角形(如 图 4 – 7 表示)。多个泛化关系可以用箭头线组成的树来表示,每一个分支指向一个子类。


图 4–7 泛化表示法

泛化的用途

泛化有两个用途。第一个用途是用来定义下列情况:当一个变量(如参数或过程变量)被声明承载某个给定类的值时,可使用类(或其他元素)的实例作为值,这被称作可替代性原则(由 Barbara Liskov 提出)。该原则表明无论何时祖先被声明了,则后代的一个实例可以被使用。例如,如果一个变量被声明拥有借贷,那么一个抵押对象就是一个合法的值。

泛化使得多态操作成为可能,即操作的实现是由它们所使用的对象的类,而不是由调用者确定的。这是因为一个父类可以有许多子类,每个子类都可实现定义在类整体集中的同一操作的不同变体。例如,在抵押和汽车借贷上计算利息会有所不同,它们中的每一个都是父类借贷中计算利息的变形。一个变量被声明拥有父类,接着任何子类的一个对象可以被使用,并且它们中的任何一个都有着自己独特的操作。这一点特别有用,因为在不需要改变现有多态调用的情况下就可以加入新的类。例如,一种新的借贷可被新增加进来,而现存的用来计算利息操作的代码仍然可用。一个多态操作可在父类中声明但无实现,其后代类需补充该操作的实现。这种不完整操作是抽象的(其名称用斜体表示)。

泛化的另一个用途是在共享祖先所定义的成分的前提下允许它自身定义增加的描述,这被称作继承。继承是一种机制,通过该机制类的对象的描述从类及其祖先的声明部分聚集起来。继承允许描述的共享部分只被声明一次而可以被许多类所共享,而不是在每个类中重复声明并使用它,这种共享机制减小了模型的规模。更重要的是,它减少了为了模型的更新而必须做的改变和意外的前后定义不一致。对于其他成分,如状态、信号和用例,继承通过相似的方法起作用。

· 继承

每一种泛化元素都有一组继承特性。对于任何模型元素的包括约束。对类元而言,它们同样包括一些特性(如属性、操作和信号接收)和关联中的参与者。一个子类继承了它的所有祖先的可继承的特性。它的完整特性包括继承特性和直接声明的特性。

对类元而言,没有具有相同特征标记的属性会被多次声明,无论直接的或继承的,否则将发生冲突,且模型形式错误。换而言之,祖先声明过的属性不能被后代再次声明。如果类的接口一致(具有同样的参数、约束和含义),操作可在多个类中声明。附加的声明是多余的。一个方法在层次结构中可以被多个类声明,附在后代上的方法替代(重载)在任何祖先中声明过的具有相同特征标记的方法。如果一个方法的两个或多个副本被一个类继承(通过不同类的多重继承),那么它们会发生冲突并且模型形式错误(一些编程语言允许显式选定其中的一种方法。我们发现如果在后代类中重新定义方法会更简单、安全)。元素中的约束是元素本身及它所有祖先的约束的联合体,如果它们存在不一致,那么模型形式错误。

在一个具体的类中,每一个继承或声明的操作都必须有一个已定义的方法,无论是直接定义或从祖先那里继承而来的。

· 多重继承

如果一个类元有多个父类,那么它从每一父类那里都可得到继承信息(如图 4-8 )。它的特征(属性、操作和信号)是它的所有父类特征的联合。如果同一个类作为父类出现在多条路径上,那么它的每一个成员中只有它的一个拷贝。如果有着同样特征的特性被两个类声明,而这两个类不是从同一祖先那里继承来的(即独立声明),那么声明会发生冲突并且模型形式错误。因为经验告诉我们设计者应自行解决这个问题,所以 UML 不提供这种情形的冲突解决方案。像 Eiffel 这样的语言允许冲突被程序设计者明确地解决,这比隐式的冲突解决原则要安全,而这些原则经常使开发者大吃一惊。


图 4–8 多重继承

· 单分类和多重分类

在最简单的形式中,一个对象仅属于一个类,许多面向对象的语言有这种限制。一个对象仅属于一个类并没有逻辑上的必要性,我们只要从多个角度同时观察一下真实世界的对象就可以发现这一点。在 UML 更概括的形式中,一个对象可以有一个或多个类。对象看起来就好像它属于一个隐式类,而这个类是每个直接父类的子类 — 多重继承可以免去再声明一个新类,这可提高效率。

· 静态与动态类元

在最简单的形式中,一个对象在被创建后不能改变它的类。我们再次说明,这种限制并没有逻辑上的必要性,而是最初目地是使面向对象编程语言的实现更容易些。在更普遍的形式下,一个对象可以动态改变它的类,这么做会得到或失去一些属性或关联。如果对象失去了它们,那么在它们中的信息也就失去了并且过后也不能被恢复,哪怕这个对象变回了原来的类。如果这个对象得到了属性或关联,那么它们必须在改变时就初始化,就像初始化一个新对象一样。

当多重分类和动态分类一起使用时,一个对象就可以在它的生命期内得到或失去类。动态类有时被称作角色或类型。一个常见的建模模式是每个对象有一个唯一的静态的固有类(即不能在对象的生命期内改变的类),加上零个或多个可以在对象生命期内加入或移走的角色类。固有类描述了这个对象的基本特性,而角色类描述了暂时的特性。虽然许多程序设计语言不支持类声明中的多重动态分类,然而它仍然是一个很有用的建模概念,并且可以被映射到关联上。
· 实现

实现关系将一种模型元素(如类)与另一种模型元素(如接口)连接起来,其中接口只是行为的说明而不是结构或者实现。客户必须至少支持提供者的所有操作(通过继承或者直接声明)。虽然实现关系意味着要有像接口这样的说明元素,它也可以用一个具体的实现元素来暗示它的说明(而不是它的实现)必须被支持。例如,这可以用来表示类的一个优化形式和一个简单低效的形式之间的关系。

泛化和实现关系都可以将一般描述与具体描述联系起来。泛化将在同一语义层上的元素连接起来(如,在同一抽象层),并且通常在同一模型内。实现关系将在不同语义层内的元素连接起来(如,一个分析类和一个设计类;一个接口与一个类),并且通常建立在不同的模型内。在不同发展阶段可能有两个或更多的类等级,这些类等级的元素通过实现关系联系起来。两个等级无需具有相同的形式,因为实现的类可能具有实现依赖关系,而这种依赖关系与具体类是不相关的。

实现关系用一条带封闭空箭头的虚线来表示(如 图 4–9 ),且与泛化的符号很相像。

用一种特殊的折叠符号来表示接口(无内容)以及实现接口的类或构件。接口用一个圆圈表示,它通过实线附在表示类元的矩形上(如 图 4–10 )。


图 4–9 实现关系


图 4–10 接口和实现图标


· 依赖

依赖表示两个或多个模型元素之间语义上的关系。它只将模型元素本身连接起来而不需要用一组实例来表达它的意思。它表示了这样一种情形,提供者的某些变化会要求或指示依赖关系中客户的变化。

根据这个定义,关联和泛化都是依赖关系,但是它们有更特别的语义,故它们有自己的名字和详细的语义。我们通常用依赖这个词来指其他的关系。 表 4–3 列出了 UML 基本模型中的一些依赖关系。

依赖关系

功能

关键字

访问

允许一个包访问另一个包的内容

access

绑定

为模板参数指定值,以生成一个新的模型元素

bind

调用

声明一个类调用其他类的操作的方法

call

派生

声明一个实例可以从另一个实例导出

derive

友员

允许一个元素访问另一个元素,不管被访问的元素是否具有可见性

friend

输入

允许一个包访问另一个包的内容并为被访问包的组成部分增加别名

import

实例化

关于一个类的方法创建了另一个类的实例的声明

instantiate

参数

一个操作和它的参数之间的关系

parameter

实现

说明和对这个说明的具体实现之间的映射关系

realize

精化

声明具有两个不同语义层次上的元素之间的映射

refine

发送

信号发送者和信号接收者之间的关系

send

跟踪

声明不同模型中的元素之间存在一些连接,但不如映射精确

trace

使用

声明使用一个模型元素需要用到已存在的另一个模型元素,这样才能正确实现使用者的功能(包括了调用、实例化、参数、发送)

use

表 4–3 依赖关系种类

跟踪是对不同模型中元素的连接的概念表述,通常这些模型是开发过程中不同阶段的模型。跟踪缺少详细的语义,它特别用来追溯跨模型的系统要求和跟踪模型中会影响其他模型的模型所起的变化。

精化是表示位于不同的开发阶段或处于不同的抽象层次中的一个概念的两种形式之间的关系。这并不意味着两个概念会在最后的模型中共存,它们中的一个通常是另一个的未完善的形式。原则上,在较不完善到较完善的概念之间有一个映射,但这并不意味着转换是自动的。通常,更详细的概念包含着设计者的设计决定,而决定可以通过许多途径来制定。原则上讲,带有偏移标记的对一个模型的改变可被另一个模型证实。而实际上,现有的工具不能完成所有这些映射,虽然一些简单的映射可以实现。因此精化通常提醒建模者多重模型以可预知的方式发生相互关系。

导出表示一个元素可以通过计算另一个元素来得到 ( 而被导出的元素可以被明确包含在系统中以避免花费太多代价进行迭代计算 ) 。导出、实现、精化和跟踪是抽象的依赖 — 它们将同一个潜在事物的不同形式联系起来。

使用表示的是一个元素的行为或实现会影响另一个元素的行为或实现。通常,这来自于与实现有关的一些问题,如编译程序要求在编译一个类前要对另一个类进行定义。大部分使用依赖关系可以从代码中获得,而且它们不需要明确声明,除非它们是自顶向下设计风格的系统的一部分(如,使用预定义的构件或函数库)。特别的使用关系可以被详细说明,但是因为关系的目的就为了突出依赖,所以它常常被忽略。确切的细节可以从实现代码中获得。使用的构造型包括调用和实例。调用表示一个类中的方法调用另一个类的操作;实例表示一个类的方法创建了另一个类的实例。

若干种使用依赖允许某些元素访问其他元素。访问依赖允许一个包看到另一个包的内容。引入依赖能够达到更高要求,可以将目标包内容的名字加入到引入包的命名空间内。友员依赖是一种访问依赖,允许客户看到提供者的私有内容。

绑定是将数值分配给模板的参数。它是具有精确语义的高度结构化的关系,可通过取代模板备份中的参数实现。使用和绑定依赖在同一语义层上将很强的语义包括进元素内。它们必须连接模型同一层的元素(或者都是分析层,或者都是设计层,并且在同一抽象层)。跟踪和精化依赖更模糊一些,可以将不同模型或不同抽象层的元素连接起来。

关系(一个元关系,不只限于依赖关系)实例表示一个元素(如对象)是另一个元素(如类)的实例。

依赖用一个从客户指向提供者的虚箭头表示,用一个构造型的关键字来区分它的种类,如 图 4–11 所示。


图 4–11 依赖


· 实例

实例是有身份标识的运行实体,即它可以与其他运行实体相区分。它在任何时刻都有一个值,随着对实例进行操作值也会被改变。

模型的用途之一是描述一个系统的可能状态和它们的行为。模型是对潜在性的描述,对可能存在的对象集和对象经历的可能行为历史的描述。静态视图定义和限制了运行系统值的可能配置。动态视图定义了运行系统从一个配置传递到另一个的途径。总之,静态视图和建立在其上的各种动态视图定义了系统的结构和行为。

在某一时刻一个系统特定的静态配置叫做快照。快照包括对象和其他实例、数值和链。对象是类的实例,是完全描述它的类的直接实例和那个类的祖先的间接实例(如果允许,重分类对象可能是多个类的直接实例)。同样,链是关联的实例,数值是数据类型的实例。

对象对于它的类的每个属性有一个数据值,每个属性值必须与属性的数据类型相匹配。如果属性有可选的或多重的多重性,那么属性可以有零个或多个值。链包含有多个值,每一个值是一个给定类的或给定类的后代的对象的引用。对象和链必须遵从它们的类或关联的约束(其中既包括明确的约束又包括如多重性的内嵌的约束)。

如果系统的每个实例是一个形式良好的系统模型的一些元素的实例,并且实例满足模型的所有约束,则说明系统的状态是有效的系统实例。

静态视图定义了一组能够在单独快照中存在的对象、值和链。原则上,任何与静态图相一致的对象和链的结合都是一个模型可能的配置。但这不意味着每个可能的快照能够或将要出现。某些快照可能在静态下合法但在系统的动态图下可能不会被动态地达到。

UML 的行为部分描述了快照的有效顺序,快照可能作为部和内外部行为影响的结果出现。动态图定义了系统如何从一个快照转换到另一个快照。

· 对象图

快照的图是系统在某一时刻的图像。因为它包含对象的图像,因此也被叫做对象图。作为系统的样本它是有用的,如可以用来说明复杂的数据结构或一系列的快照中表示行为(如图 4-13 )。请记住所有的快照都是系统的样本,而不是系统的定义。系统结构和行为在定义视图中定义,且建立定义视图是建模和设计的目标。

静态视图描述了可能出现的实例。除了样本外,实际的实例不总是直接在模型中出现。


图 4–13 对象图

posted on 2005-10-15 21:32  小山  阅读(1705)  评论(0)    收藏  举报