UML 统一建模语言

UML 是一种开放的方法,用于说明、可视化、构建和编写一个正在开发的、面向对象的、软件密集系统的制品的开放方法。UML 展现了一系列最佳工程实践,这些最佳实践在对大规模,复杂系统进行建模方面,特别是在软件架构层次已经被验证有效。

类图

面向对象程序设计 (Object-Oriented Programming, 缩写为 OOP) 是一种范式, 其基本理念是将数据块及与数据相关的行为封装成为特殊的、 名为对象的实体, 同时对象实体的生成工作则是基于程序员给出的一系列蓝图, 这些蓝图就是类,在 UML 中,类图专门是用来描述这种实体的。如下图所示:

类之间是有相互关系的,就像猫属于动物,动物和植物又属于生物体:

类之间的关系可以更细分的划分为这几种:依赖,关联,聚合,组合。

依赖

依赖是类之间最基础的、也是最微弱的关系类型。 如果修改一个类的定义可能会造成另一个类的变化,那么这两个类之 间就存在依赖关系。当你在代码中使用具体类的名称时, 通 常意味着存在依赖关系。 例如在指定方法签名类型时, 或是通过调用构造函数对对象进行初始化时等。 通过让代码依赖接口或抽象类 (而不是具体类),你可以降低其依赖程度。

通常情况下,UML 图不会展示所有依赖 —— 它们在真实代码中的数量太多了。 为了不让依赖关系破坏 UML 图, 你必须对其进行精心选择,仅展示那些对于沟通你的想法来说重要 的依赖关系。

在 UML 中,依赖关系用一个虚线的箭头表示,从依赖者到被依赖者,例如教授依赖课程资料:

关联

关联是一个对象使用另一对象或与另一对象进行交互的关系。 在 UML 图中, 关联关系用起始于一个对象并指向其所使用 的对象的简单箭头来表示。 顺带一提, 双向关联也是完全正常的, 这种情况就用双向箭头来表示。 关联可视为一种特殊类型的依赖,即一个对象总是拥有访问与其交互的对象的权限,而简单的依赖关系并不会在对象间建立永久性的联系。

一般来说, 你可以使用关联关系来表示类似于类成员变量的东西。这个关系将一直存在,因此你总能通过订单来获 取其顾客。 但是它并非一定是成员变量。如果你根据接口来创建类,它也可以表示为一个可返回订单顾客的方法。

为了区分关联和依赖,举如下的例子,Professor 依赖 Course 提供的 c.getKnowledge 方法获取知识,将其传授给关联的 Student

1
2
3
4
5
class Professor is
field Student student //...
method teach(Course c) is
// ...
this.student.remember(c.getKnowledge())

如果有人修改了 课程 类的 getKnowledge (获取知识) 方法 (修改方法名或添加一些 必须的参数等),代码将会崩溃。这就是依赖关系。

让我们来看看名为 student (学生) 的成员变量,以及如何在 teach 方法中使用该变量。我们可以肯定学生 (Student) 类是教授类的依赖:如果 remember 方法被修改, 教授的代码也将崩溃。但由于教授的所有方法总能访问 student 成员变量,所以学生类就不仅是依赖,而也是关联了。

UML 图中,经常用一个单向的箭头表示关联关系:

聚合

聚合是一种特殊类型的关联, 用于表示多个对象之间的一对多多对多整体对部分的关系 。 普通关联仅用于描述两个对象之间的关系。通常在聚合关系中, 一个对象拥有 一组其他对象, 并扮演着容器或集合的角色。组件可以独立于容器存在, 也可以同时连接多个容器。 在 UML 图中,聚合关系使用一端是空心菱形,另一端指向组件的箭头来表示。

组合

组合是一种特殊类型的聚合, 其中一个对象由一个或多个其他对象实例构成。 组合与其他关系的区别在于组件仅能作为容器的一部分存在。 在 UML 图中,组合与聚合关系的符号相同,但箭头起始处的菱形是实心的。

OOP

面向对象程序设计的四个基本概念使其区别于其他程序设计范式,他们分别是:抽象,多态,封装以及继承。

抽象

当使用面向对象程序设计的理念开发一款程序时, 我们会将大部分时间用于根据真实世界对象来设计程序中的对象。但是,程序中的对象并不需要能够百分之百准确地反映其原型 (极少情况下才需要做到这一点)。实际上,你的对象只需模拟真实对象的特定属性和行为即可,其他内容可以忽略。

例如, 飞行模拟器和航班预订程序中都可能会包含一个飞机 Airplane 类。 但是前者需包含与实际飞行相关的详细信息,而后者则只关心座位图和哪些座位可供预订。

抽象是一种反映真实世界对象或现象中特定内容的模型, 它 能高精度地反映所有与特定内容相关的详细信息, 同时忽略其他内容。

封装

封装是指一个对象对其他对象隐藏其部分状态和行为, 而仅向程序其他部分暴露有限的接口的能力。

如果想要启动一辆车的发动机, 你只需转动钥匙或按下按钮即可, 无需打开引擎盖手动接线、转动曲轴和气缸并启动发动机的动力循环。 这些细节都隐藏在引擎盖下,你只会看到一些简单的接口:启动开关、方向盘和一些踏板。

对象的接口 —— 它是对象的公有部分,能够同其他对象进行交互。

封装某个内容意味着使用关键字 private 来对其进行修饰, 这样仅有其所在类中的方法才能访问这些内容。 还有一种限制程度较小的关键字 protected 保护 , 其所修饰的对象仅允许父类访问其类中的成员。

绝大部分编程语言的接口和抽象类 (或方法) 都基于抽象和 封装的概念。 在现代面向对象的编程语言中, 接口机制 (通常使用 interfaceprotocol 关键字来声明) 允许你定义对象之间的交互协议。这也是接口仅关心对象行为,以及你不能在接口中声明成员变量的原因之一。

假如航空运输 FlyingTransport 接口中有一个 fly(origin, destination, passengers) 方法 (即以起点、 终点以及乘客为参数的飞行方法)。 在设计航空运输模拟器时, 你可以对机场 Airport 做出限制, 使其仅与实现了航空运输接口的对象进行交互。此后,你可以确保 传递给机场对象的任何对象 —— 无论是飞机直升机、还是可怕的家养狮鹫 —— 都能到达或离开这种类型的机场。

继承

继承是指在根据已有类创建新类的能力。 继承最主要的好处是代码复用。如果你想要创建的类与已有的类差异不大, 那也没必要重复编写相同的代码。你只需扩展已有的类并将额外功能放入生成的子类 (它会继承父类的成员变量和方法) 中即可。

使用继承后,子类将拥有与其父类相同的接口。如果父类中声明了某个方法,那么你将无法在子类中隐藏该方法。你还必须实现所有的抽象方法,即使它们对于你的子类而言没有意义。

在绝大多数编程语言中, 子类仅能对一个父类进行扩展。 另一方面, 任何类都可以同时实现多个接口。 但是正如我之前提到的那样, 如果父类实现了某个接口, 那么其所有子类都 必须实现该接口。

多态

绝大部分动物 Animals 可以发出声音。 我们需要所有子类都重写基类的 makeSound 发出声音方法,让每个子类都发出正确的声音,因此我们可以马上将其声明为抽象。 这让我们得以忽略父类中该方法的所有默认实现,从而强制要求所有子类自行提供该方法的实现。

假如将几只猫和狗放入一个大袋子中。 然后,我们闭上眼睛,将动物一个一个地从袋中取出。 我们并不知道自己取出的是何种动物。 但如果我们好好地摸摸它们的话, 它就会根据自己的具体类发出特殊的欢快叫声。

1
2
3
4
5
6
bag = [new Cat(), new Dog()]; 
foreach (Animal a : bag)
a.makeSound()

//喵喵!
//汪汪!

程序并不知道 a 变量中所包含的对象的具体类型,但幸亏有被称为多态的特殊机制, 程序可以追踪对象的子类并调用其方法,从而执行恰当的行为。

多态是指程序能够检测对象所属的实际类,并在当前上下文不知道其真实类型的情况下调用其实现的能力。

UML

UML 图经过各种大型工程的实践,被证明是一种极其有用的方式来描述可视化系统,数据可系统及其他软件系统,所有的软件开发人员都应该学习并掌握它。从种类上来说,它有以下几种类型:

类图(Class Diagram)

Class 经常被用用来描述某个具体的或者抽象的对象,这个对象经常具有一些属性和方法,而且这些属性和方法都具有不同的访问限制规则,例如一个用来描述 Dog 的类图:

可以看到的是在每个属性或者方法之前都有一个 + 或者 -,这个就是访问限制规则,一共有四种:

  • +:Public,可公开访问
  • -:Private
  • #:Protected
  • ~:Package Local

而且对于方法而言,可以通过 ininout 或者 out 标识参数的意义:

  • in: 该参数仅作为输入参数,不应该被修改;
  • inout: 该参数即可作为输入参数,也可以被内部修改;
  • out: 该参数仅用于作为输出参数存储库,就像传入一个地址,然后内部可以对其进行赋值;

就像现实世界中不同对象之间有不同的关系一样,类图中也有描述不同类之间关系的方式,一般存在六种关系,分别是关联,继承,实现,依赖,聚合和组合。

例如,学生从老师那里学习知识,这种关系就可以表示位简单的关联关系,在表示的时候使用一个联系表示即可,也可以适当添加描述信息:

继承有时候也可以称为泛化,用一个空心箭头从子类指向父类,父类可能是抽象类或者具体类:

实现一般是指具体类型和接口之间关系,用一个虚线实心箭头表示:

当一个对象在其方法中使用另一个类的对象,并且未存储在任何字段中时,就表示存在依赖关系,例如,Person 有一个方法 Read 接收一个 Book 的实例 book 作为参数,调用 Bookgetknowledge 方法获取知识:

聚合和组合比较相近,都表示一对多的关系,而聚合通常用来表示用类的聚合,而组合表示整体和部分的关系;例如学校有很多老师,这种表示聚合关系;而学校也有很多不同的学院,计算机学院,土木工程等,这种表示组合关系,不同类型;在表示上聚合使用空心菱形箭头,组合使用实心菱形箭头表示。

状态图(State Diagram)

UML 状态图是图表本身的名称,主要用于描述对象具有的各种状态、状态之间的转换过程以及触发状态转换的各种事件和条件。UML 状态图描述了一个状态机,可以被定义为一台机器,它定义了一个对象,这些状态控制外部或内部事件的不同状态,状态机由状态、转换、事件、活动和动作五部分组成。

  • 状态:状态指的是对象在其生命周期中的一种状况,处于某个特定状态中的对象必然会满足某些条件、执行某些动作或者是等待某些事件。一个状态的生命周期是一个有限的时间阶段;
  • 转换:转换指的是两个不同状态之间的一种关系,表明对象在第一个状态中执行一定的动作,并且在满足某个特定条件下由某个事件触发进入第二个状态;
  • 事件:事件指的是发生在时间和空间上的对状态机来讲有意义的那些事情。事件通常会引起状态的变迁,促使状态机从一种状态切换到另一种状态,如信号、定时器,某个条件被处罚等;
  • 活动:活动指的是状态机中进行的非原子操作;
  • 动作:动作指的是状态机中可以执行的哪些原子操作。所谓原子操作,指的是他们在运行的过程中不能被其他消息中断,必须一直执行下去,以至最终导致状态的变更或者返回一个值;

一个状态图(Statechart Diagram)本质上就是一个状态机,或者是状态机的特殊情况,它基本上是一个状态机中元素的一个投影,这也就意味着状态图包括状态机的所有特征。

状态图描述了一个实体基于事件反映的动态行为,显示了该实体是如何根据当前所处的状态对不同的事件作出反应的。

在 UML 中,状态图由表示状态的节点和表示状态之间转换的带箭头的直线组成。状态的转换由事件触发,状态和状态之间由转换箭头连接。每一个状态图都有一个初始状态(实心圆),用来表示状态机的开始。还有一个中止状态(半实心圆),用来表示状态机的终止。状态图主要由元素状态、转换、初始状态、中止状态和判定等组成。

状态

状态用于对实体在其生命周期中的各种状况进行建模,一个实体总是在有限的一段时间内保持一个状态。状态由一个带圆角的矩形表示,状态的描绘素应该包括名称、入口和出口动作、内部转换和嵌套状态。如下图,为一个简单状态:

  • 状态名指的是状态的名字,通常用字符串表示,其中每个单词的首字母大写。状态名可以包含任意数量的字母、数字和除了冒号 “:” 以外的一些字符,可以较长,甚至连续几行。但是一定要注意一个状态的名称在状态图所在的上下文中应该是唯一的,能够把该状态和其他状态区分开。
  • 入口和出口动作一个状态可以具有或者没有入口和出口动作。入口和出口动作分别指的是进入和退出一个状态时所执行的 “边界” 动作。
  • 内部转换指的是不导致状态改变的转换。内部转换中可以包含进入或者退出该状态应该执行的活动或动作。
  • 嵌套状态状态分为简单状态(Simple State)和组成状态(Composite State)。简单状态是指在语义上不可分解的、对象保持一定属性值的状况,简单状态不包含其他状态:而组成状态是指内部嵌套有子状态的状态,在组成状态的嵌套状态图部分包含的就是此状态的子状态。
转换

在 UML 的状态建模机制中,转换用带箭头的直线表示,一端连接源状态,箭头指向目标状态。转换还可以标注与此转换相关的选项,如事件、监护条件和动作等,如下图所示。注意:如果转换上没有标注触发转换的事件,则表示此转换自动进行。

在状态转换机制中需要注意的五个概念如下:

  • 状态源(Source State):指的是激活转换之间对象处于的状态。如果一个一个状态处于源状态,当它接收到转换的触发事件或满足监护条件时,就激活了一个离开的转换。
  • 目标状态(Event State):指的是转换完成后对象所处的状态。
  • 事件触发器(Event Trigger):指的是引起源状态转换的事件。事件不是持续发生的,它只发生在时间的一点上,对象接收到事件,导致源状态发生变化,激活转换并使监护条件得到满足。
  • 监护条件(Guard Condition):是一个布尔表达式。当接收到触发事件要触发转换时,要对该表达式求值。如果表达式为真,则激活转换:如果表达式为假,则不激活转换,所接收到的触发事件丢失。
  • 动作(Action):是一个可执行的原子计算。
初始状态

每个状态图都应该有一个初始状态,它代表状态图的起始位置。初始状态是一个伪状态(一个和普通状态有连接的假状态),对象不可能保持在初始状态,必须要有一个输出的无触发转换(没有事件触发器的转换)。通常初始状态上的转换是无监护条件的,并且初始状态只能作为转换的源,而不能作为转换的目标。在 UML 中,一个状态图只能有一个初始状态,用一个实心圆表示。

终止状态

终止状态是一个状态图的终点,一个状态图可以拥有一个或者多个终止状态。对象可以保持在终止状态,但是终止状态不可能有任何形式的和触发转换,它的目的就是为了激发封装状态上的转换过程的结束。因此,终止状态只能作为转换的目标而不能作为转换的源,在 UML 中,终止状态用一个含有实心圆的空心圆表示。

判定

活动图和状态图中都有需要根据给定条件进行判断,然后根据不同的判断结果进行不同转换的情况。实际就是工作流在此处按监护条件的取值发生分支,在 UML 中,判定用空心菱形表示。

序列图(Sequence Diagram)

序列图也叫时序图,是交互图的一种,用于捕获系统运行中对象之间有时间顺序的交互,是由生命线和消息组成。时序图将交互关系表示为一个二维图。纵向是时间轴,时间沿竖线向下延伸。横向轴代表了在协作中各独立对象的类元角色。类元角色用生命线表示。当对象存在时,角色用一条虚线表示,当对象的过程处于激活状态时,生命线是一个双道线。消息用从一个对象的生命线到另一个对象生命线的箭头表示。箭头以时间顺序在图中从上到下排列。

  • Actor(角色:代表某个由人或者设备扮演的角色,它不一定代表某个具体的实体,一般由一个小人代替;

  • Lifetime(生命线):在时序图中表示为从对象图标向下延伸的一条虚线,表示对象存在的时间。

  • Activation(活动条 / 激活):在生命线的徐线上可以用活动条表示某种行为的开始和结束,一般用小矩形来表示。

  • 同步消息:意味着阻塞和等待。如:A 向 B 发送一个消息后,对象 A 必须一直等到 B 执行完成后返回才能继续往下执行。这就是同步消息。用实心箭头表示

  • 异步消息:就意味着是非阻塞。如:A 向 B 发送消息后,直接可以执行下面代码,无需等待 B 的执行。

  • 返回消息:在从 A 发送消息到达 B 之后,B 的回复消息称之为返回消息。

UML 在 2.0 时在时序图中加入了交互框。交互框用来解决交互执行的条件和方式,它允许在序列图中直接表示逻辑组件,用于通过指定条件或子进程的应用区域,为任何生命线的任何部分定义特殊条件和子进程。组合片段共有 13 种,名称及含义如下:


举几个示例:

参考资料

  1. UML Diagrams Full Course (Unified Modeling Language)
  2. UML 建模之状态图(Statechart Diagram)
  3. UML 状态图
  4. UML 时序图 / 序列图