摘要:
UML是什么?是建模语言。本文就从语言和思维的关系谈起,说明UML对思维具有反作用——是促进思维还是阻碍思维,全凭UML的使用者对UML内涵的掌握程度了。那么,如何达到“UML促进思维”的境界呢?本文结合实例,说明图论思想在UML应用中的意义,希望能对读者有所启发。
人类用词汇表达一定的意义,这是件很有意思的事。比如,“模型”和“建模”这一对词汇,形式上有一字相同,意义上也密切相关;英文原词model和modeling亦如此,形式上后者多了一个ing后缀;其实,model和modeling词源上根本就是同一个词——model作动词时可以当“为……建模”讲。
例子远不止这些。心理学中,“语言”和“言语”关系紧密。“语言”是一种符号系统,由词汇和语法构成。人们使用语言进行思想交流,称为“言语”,它可以分为三种形式:口头、书面、内部言语。心理学的研究表明:语言是思维的基础,并对思维具有反作用;思维对事物的反映,总是借助语言进行的;思维过程通过内部语言进行,思维结果通过口头或书面语言表现。
统一建模语言(Unified
Modeling Language,UML)既然是一种语言,当然也会对思维有“反作用”——是促进思维还是阻碍思维,全凭UML的使用者对UML内涵的掌握程度了。
本文结合实例,说明图论思想在UML应用中的意义。希望能对读者达到“UML促进其思维”的境界,带来些许启发。
一、图的定义
顾名思义,图论就是研究图的理论。图是一种由两个集合——即一个顶点集合和一个边集合——定义的抽象数据结构。图的更形式化的定义如下:
称G=(V,E)是一个图,如果
(1)
V是一个非空有限集合,
(2)
E是V中元素的无序对所组成的有限集合,
并把V的元素叫做图的顶点,E的元素叫做图的边。
举个例子,下图是一个有7个顶点和5条边的图,vi标出了顶点,ei标出了边。
二、图的定义的UML应用——UML的图论观点
UML作为可视化建模语言,包括语法和语义两个方面。单从语法方面,用图论的眼光——把UML看作顶点和边——来学习UML,应当说是正本清源之道。下表以图论观点对UML语法进行了总结。
|
顶点
|
边
|
边属性
|
其它
|
用例图
|
参与者,
用例
|
关联,泛化,
包含,扩展
|
|
接口
|
包图
|
包,
接口
|
依赖,
实现
|
|
可嵌入类图
|
类图
|
类
|
关联,
泛化,
依赖
|
角色名,多重性,导航,组成符,聚集符,关联名,关联名方向
|
限定符,
参数化类,
关联类
|
对象图
|
对象
|
链
|
角色名,多重性,导航,组成符,聚集符,链名,链名方向
|
|
顺序图
|
对象
|
消息
|
消息名,条件,重复
|
参与者实例,生命线,激活
|
协作图
|
对象
|
链,
消息
|
消息号,所有顺序图的边(消息)属性,所有对象图的边(链)属性
|
参与者实例,位置,状态,变成流,拷贝流
|
构件图
|
构件,接口
|
依赖
|
|
可嵌入对象图
|
部署图
|
节点
|
连接
|
|
可嵌入构件图
|
状态图
|
状态
|
转换
|
条件,动作
|
复合状态
|
活动图
|
活动状态
|
完成转换
|
条件,分支,分叉,结合
|
泳道,对象流
|
数学中,有关“数学抽象度”的研究表明:抽象层次越高,切近事物本质越深。UML的图论观点,从更抽象的“图论”角度理解UML的语法,因此能够“切近事物本质更深”。UML
2.0即将全面到来,改动虽大,但决不会跳出图论范畴;总之,理解了UML的图论观点,对快速掌握UML新规范大有裨益,笔者的实践也证明了这一点。
三、图的定义的UML应用——关联类语法的理解
除了上面的基本总结以外,笔者发现UML中的关联类常被“误用”或“该用不用”,所以有必要谈一下。
语法方面,从图论中对图的基本定义,可以找到对关联类的“犀利”的理解。
首先,扩展一下图论的“经典”定义,如下图所示。
扩展之后,顶点可以由更多的“角色”来承担:除了通常的顶点外,边也可以充当顶点。这样以来,边就有如下三种情况:
l
连接顶点与顶点的边
l
连接顶点与边的边
l
连接边与边的边
然后,分析关联类本身的语法,它用到了上面扩展的第二种情况。如下图所示,关联类语法分为关联部分、类部分、关联部分和类部分之间的可视化连接部分,共三部分内容。
总之,虽然从语义来讲,关联类是一个独立的模型元素,但从语法角度,它既包含了关联的符号,又包含了类的符号。
四、图的定义的UML应用——说说序列图
值得补充说明的是,序列图中“生命线”和“激活”也是可以充当顶点的边,如下图所示(该图引用自《UML参考手册》)。
五、有向边
有向图是无向图的特殊情况,它们的定义有微妙的差异。
称G=(V,E)是一个有向图,如果
(1)
V是一个非空有限集合,
(2)
E是V中元素的有序对所组成的有限集合,
并把V的元素叫做图的顶点,E的元素叫做图的有向边。
举个例子,下图是一个有向图,描述的是足球赛的小组循环赛:a队3场全胜,b、c、d这3个队都是1胜2负。
六、有向边的UML应用——依赖关系
静态视图是UML的基础。模型中静态视图的元素是应用中有意义的概念,这些概念包括真实世界中的概念、抽象的概念、实现方面的概念和计算机领域的概念。静态视图中的关键元素是类元及它们之间的关系;类元是描述事物的建模元素,包括类、接口和数据类型等;类元之间的关系有依赖、泛化、实现和关联等。
依赖表示两个或多个模型元素之间语义上的关系,即提供者的某些变化会要求或指示依赖关系中客户的变化。例如,下图体现了一条架构设计的基本原则:问题领域层“不依赖于”其他任何层,而其他任何层“只依赖于”问题领域层。
七、有向边的UML应用——泛化、实现和关联的依赖思想
根据依赖关系的定义——依赖表示两个或多个模型元素之间语义上的关系,即提供者的某些变化会要求或指示依赖关系中客户的变化——泛化、实现和关联也是依赖关系,它们都包含了依赖的思想。
举个例子。下面是一道极为常见的笔试题(以Java语言为例):
写出下列程序的运行结果:
public class Test {
public static void main(String[] args) {
Child child = new Child();
}
}
class
Parent {
Parent() {
System.out.println("to
construct Parent.");
}
}
class
Child extends Parent {
Child() {
System.out.println("to
construct Child.");
}
Delegatee delegatee = new Delegatee();
}
class
Delegatee {
Delegatee() {
System.out.println("to
construct Delegatee.");
}
}
|
这道题考的是构造函数的执行顺序,输出结果如下(以Java语言为例):
to
construct Parent.
to construct Delegatee.
to construct Child.
|
其实,构造函数的执行顺序,何尝不是“依赖思想”决定的呢?具体而言,就是“被依赖的先构造,依赖于人的后构造”。继承关系包含了依赖思想,子类Child依赖父类Parent,所以Parent先于Child构造。关联关系包含了依赖思想,聚集是关联关系的一种,所以被聚集的类Delegatee先于Child构造。
相信至今还有人会画出类似下面的类图,它错在继承的箭头方向画反了!经过以上的讨论,我们知道了大师们把继承的箭头方向规定为指向父类有其深刻含义——它代表了依赖的方向!
八、有向边的UML应用——一个例子
图论是对现实世界中实际问题的高度抽象。有向图可以对具有特定依赖关系的实体群落,进行有效的抽象刻画,使人们能够忽略无关紧要的众多细节,而牢牢把握住本质性的依赖关系。
依赖关系在软件开发中的重要性,不管怎么强调都不过分。响应变化的能力往往决定一个项目的成败,而依赖关系的处理(其实不仅包括类与类等工件之间的依赖,还包括人之间的依赖和活动之间的依赖,本文不详细讨论)正是其中的关键。
著名的开放封闭原则(Open-Closed
Principle,OCP)规定,“软件实体应该是可以扩展的,但是不可修改”。本原则紧紧围绕变化展开,变化来临时,如果不必改动软件实体的源代码,就能扩充它的行为,那么这个软件实体的设计就是满足开放封闭原则的。如果我们预测到某种变化,或者某种变化发生了,我们应当创建抽象来隔离以后发生的同类变化。在Java中,这种抽象指抽象基类或接口;在C++中,这种抽象是指抽象基类或纯抽象基类。
比如,在开发一个需求跟踪工具的时候,起初可能仅需要支持保存为专有格式的“项目”文件,但后来又需要支持导出为HTML格式的网页。让我按照敏捷软件开发过程,来讲述这个故事。
最开始的设计如下图所示,CReqMatrixDoc调用CProjectSaver来保存自己。按照开放封闭原则(Open-Closed
Principle),这并不是一个好的设计。但此时,所有需求就是支持“保存为专有格式的项目文件”,而且我们并没有预见到将来还需要以更多的形式保存,所以这个设计“不多不少”刚刚好。
后来需求发生了变化,这个工具需要支持“导出为HTML格式的网页”的特性。是的,这个需求不管是客户新提出来的,还是设计人员在上一个迭代有意忽略了,总之在这个迭代周期需求发生了变化。于是,设计人员意识到,需求跟踪工具可能需要支持多种保存策略。
看来,代码出现了臭味(Smell),需要重构(Refactoring)。让我们谨遵Martin
Fowler“两顶帽子”的教诲——不要将重构和添加新功能同时进行——这一步我们仅进行重构。我们要做的就是采用依赖倒置原则(Dependency-Inversion
Principle)惯用的“用两个抽象依赖代替一个具体依赖”策略,重构之后的设计如下图所示。我们引入了一个接口CDocSaver,然后让CProjectSaver实现这个接口。
哈,新的设计满足开放封闭原则,究其原因,最关键的一点就是CProjectSaver对CDocSaver接口的单向依赖。新设计非常易于扩充,我们只需新写一个CHtmlSaver来实现接口CDocSaver,就离支持“导出为HTML格式的网页”不远了,如下图所示。咦,原来是GOF的Strategy模式。
|