本文介绍使用EnterpriseArchtect进行建模是,类图的使用方法。篇幅较长,请慢慢阅读。示例中使用的EnterpriseArchtect的版本为13.0,其他版本的区别也应该不大,请自行调整。
准备工作
创建EA工程
启动EA后选择【New Project】菜单项,在出现的【New Project】对话框中输入文件名后按下【保存】按钮。
为简单起见,在出现的【Model Wizard】中不做任何选择,直接选取消。我们还是可以得到下面的工程文件。
注意右上角的【Modle】节点,就是工程的根节点。
创建View
在Model节点的上下文菜单中选择【Add】-【Add View】菜单。就可以调出CreateNewView对话框,具体如下图。
输入Name并选择Icon Style。这里不必太纠结,事后都可以改的,比如IconStyle就可以通过DemoView节点的上下文菜单中的【Set
View Icon】项目修改。
创建类图
接下来我们从DemoView节点的上下文菜单中选择【Add Diagram】,调出【New Diagram】如下的对话框。
输入/选择必要的信息。
1.在【Diagram】栏中输入Class Diagram1
2.在【Select Form】列表中选择【UML Structral】
3.在【Diagram Type】列表中选择【Class】
点击【OK】按钮,就可以得到一个名为Class Diagram1的类图。画面大致如下。
添加类/删除类
添加类
添加新类的操作如下图所示从软件的工具栏中向类图中拖动Class图标。
释放鼠标后会自动出现类属性对话框。
在【General】分类页面中输入类名Person并确定Language中选中的是C++以后,按下【确定】按钮。画面会变成以下的样子。
这里选择C++有两个作用,一是决定生成代码的语言,二是有些选项(比如私有继承)会随着本设定而改变。其实也不一定要问为什么,做对的事情就好了。
除了左侧出现黄色的Person类方框以外,右上部分的Model树上会出现一个Person节点。严格来讲,这个Person节点才是我们在模型中增加的那个类。左边类图中的Person只是一个链接。
删除链接
为了说明这点,我们可以删除类图中的Person类,这时画面会变成下面的样子。
类图中的Person类虽然不见了,Model树上的Person还好好的在那里。
粘贴链接
我们再将Person节点拖到ClassView中,这时会出先【Paste Person】对话框。
目前的【Drop as】项目的选项是Link,会在画面上增加一个Person类的链接。
现在回复到了删除前的状态,没有损失任何东西。追加说明一点,在ClassView中有了Person类,如果继续上面的操作,EA会拒绝。
粘贴实例
这还没有完,我们继续向ClassView拖动Person节点,但这次我们选In
stance(Object)增加一个Person类的实例,画面变成下面这样。
请同时关注ClassView的变化和Model树的变化。我们可以继续增加实例,增加一个实例,Model数的节点也会增加。它们目前名字相同,但是是不同的实例,这个场景下,就是不同的人。
我们还可以通过属性对话框来修改实例名,由于篇幅和流量的原因,这里省略。
粘贴子类
到这里还没有完,我们继续向ClassView拖动Person节点,但这次我们选Child(Generalization)增加一个Person类的派生类,画面就会变成下面这样。
你大概注意到了,软件为我们可以自动添加了泛化连接线。子类也可以重复添加,每次都是增加另外一个子类,虽然目前的名称相同,但是都是不同的子类,这一点可以从Model树上看到结果。
删除类/实例
选择Model树上的对应节点,打开上下文菜单,选择【Delete ‘???’】即可,这回可是真删,要慎重!
添加类属性
属性是OO中的一个词汇,在C++语法中,应该叫数据成员。这里我们尽量使用OO中的属性一词。
打开属性窗口有两种方法:
1.双击类图中的对应类框图,选择【General】以后,点击【Attributes】按钮。
2.从模型树中选择对应的类节点,打开上下文菜单并选择【Attributes】菜单项。
无论哪种方法都可以打开下面的属性设定对话框。严格讲这并不是属性自己的属性对话框,而是属性和操作共同的对话框。本文只关注属性部分。
添加新属性的操作主要是在红绿两个矩形框中进行的。我们在这里只说明有(zhi)关(dao)的项目。
基本项目
基本项目通过红框中的列表控件来设置。
Name:变量名,可以自由输入文本。
Type:数据类型,可以自由输入文本,也可以点从下拉菜单中选择。在下拉菜单的最下面,还有一个【Select
Type...】选项,提供了从工程中选择类型的功能。但是选择的结果也只是作为文本保存,起到的作用仅限于输入辅助。如果你期待变量类型和被参照的类型名联动,那你想多了。
Scope:从下拉列表中选择,一共有四个选项:Public/Protected/Private/Pack
age。Package也是可选的,表示也没有问题。但是在生成代码时当作Public处理。
Initial Value:为属性设置初期值。可以就地输入或打开对话框输入。区别不详。
扩展项目
属性列表中有属性被选中时,可以设定该属性的扩展属性。
Static:表明该属性是静态属性,或称静态数据成员。
Property:定义Property操作方法(Getter/Setter)。具体画面如下:
Const:定义常量数据成员。
下面来看一个实例:
在本例中创建了三个属性(扩展项目部分省略表示):
1.私有的int型变量m_age,并为其设置了Proerpty属性。
2.保护的string变量m_name。
3.静态,公开的int型变量MARRY_AGE
作为上述操作的结果,类图变成下面这个样子。
请注意观察在类图中属性的表达方式。另外也可以看到SetAge和GetAge两个方法。
关于初期值的补充说明:
在基本项目中有一个初期值InitialValue需要补充说明一下。
在C++11之前这个项目只能应用于静态常量数据成员。
在C++11以后,可以为每个变量设定缺省值。当构造函数的初始值列表中没有为数据成员制定初始值的时候,编译器会利用这个缺省值来初始化数据成员。
添加操作和方法
也许有人会问,操作和方法不是一回事么?还真不是一回事。
操作
操作指明了目标对象状态的转换或返回给操作调用者值的查询。它有名称和参数列表,包括返回参数。操作指定了行为的结果,而不是行为本身,行为可以是一个方法,一次状态机转换或其他。
方法
方法是一个过程,它实现了一个操作,它有一个算法或过程描述,调用如果解析为一个方法,将导致该过程被执行。
以上是ULM2.0对操作的方法的说明。你看懂了么,反正我是琢磨了好一会。那我就举个例子吧。
说有一个驾驶者基类,它有有两个派生类,分别是车主和小偷。驾驶者类声明了一个启动汽车的操作,车主类使用(实现)的方法是拧车钥匙,小偷类使用(实现)的方法是直接接发动机电源线(电影里常有的)。
怎么样,好点没,下面继续今天的话题。
属性窗口的打开方法:
1.双击类图中的对应类框图,选择【General】以后,点击【Operation】按钮。
2.从模型树中选择对应的类节点,打开上下文菜单并选择【Operations...】菜单项。
无论哪种方法都可以打开下面的操作设定对话框。严格讲这并不是操作自己的对话框,而是属性和操作共同的对话框。本文只关注操作部分。
添加新操作的操作主要是在红绿蓝三个矩形框中进行的。和上篇文章一样我们在这里只说明有(zhi)关(dao)的项目。
基本项目
基本项目通过红框中的列表控件来设置。
Name:变量名,可以自由输入文本。
Parameters:在参数设定部分详细说明,此处省略。
Retun Type:数据类型,可以自由输入文本,也可以点从下拉菜单中选择。在下拉菜单的最下面,还有一个【Select
Type...】选项,提供了从工程中选择类型的功能。但是选择的结果也只是作为文本保存,起到的作用仅限于输入辅助。这一点和属性类型一样。
Scope:设定操作的可视性。从下拉列表中选择,一共有四个选项:Public/Protected/Private/Package。Package也是可选的,表示也没有问题。但是在生成代码时当作Public处理。
Stereotype:可以设定一些分类信息,比如property set,property get,或者constructor等。
扩展项目
操作列表中有操作被选中时,可以设定该操纵的扩展属性。
Concurrency:用于自定操作的并发属性。可能的选项有:
sequential:同时只能有一个调用发生。如果并发调用发生,则结果不保证。
guarded:允许并发调用发生,但同时只允许一个调用执行。
concurrent:允许并发调用发生,并保证可以正确地并发执行。
Virtual:用于指定抽象操作(虚函数)。
Static:表明该操作是类操作(静态函数),而非实例操作。
参数设定项目
参数定义主要是通过蓝框中的【Parameters】表单来进行的。可以定义多个参数,并设定他们的属性。方法和类设定属性的方法基本一致,此处省略。
我们试着为Person类追加了两个方法,一个是静态方法GetMarryAge,一个是虚函数Show。Person类变成了如下的样子。
可以看到静态方法GetMarryAge的下面有一条横线,而Show操作被表示成斜体。这就是UML中静态方法和抽象操作的表达方式。
如果这还不够,还可以再往前走一步,生成代码。在Person类上点击鼠标右键调出上下文菜单选中【Code
Engineering】-【Generate
Code】可以调出如下的生成代码对话框。
在选择路径之后,按下【Generate】按钮,我们既可以得到以下代码。
首先是头文件
然后是cpp
不做重复的事情,这才是正确的方法应该有的样子。
模板和泛型编程也是C++中很重要的一部分,相信很大一部分程序员都用过某种容器类。但一般来说也就是用用而已,并不会自己构建类模板或者在建模中使用类模板。
类模板
接下来介绍EA中类模板的创建类模板和使用类模板的方法。
创建类模板
假设我们要创建一个映射类(假设而已,可别真去创建)MyMap,它有两个参数,一个是Key,一个是元素T。
首先创建一个普通的类,设定类名为MyMap。
一定有人在输入类名的时候直接输入MyMap<class Key, class T>,这时候生成的类图就像下面这样。
看起来也是那么回事,但是并不能生成正确的代码。所以还是回到原先的轨道上来吧。只要输入MyMap就好。
接下来选择【Templates】分类,并在【Template Parameter(s)】列表空间中增加Key和T两个参数,Type都指定为class。
按下【确定】按钮返回后,类图会变成下面这样。
生成的代码如下:
使用类模板
作为例子接下来利用MayMap实例化一个类名为PersonMap的类,负责管理从整数到Person*的映射。
首先创建一个普通的类,名为PersonMap。
从工具栏的【Class Relationsships】组里选择下面图标。
然后从PersonMap类向MyMap类拖动鼠标,以建立两个类的连接关系。
鼠标双击《bind》连接线打开属性对话框并选择【Binding】分类,然后按下【Add】按钮在【Parameter
Substitution(s)】列表中添加参数。
如上图所示,Formal列可以选择Key和T参数。它们都是在MyMap类模板中定义的。继续操作,指定Key和T参数的内容。
类图会变成下面这样。请关注红圈中的变化。
对应代码如下。
生成的有效代码很少,但这确实是正确的代码。在UML中这种方式叫显示绑定。
关联入门
在定义了类属性,类操作等限制在单个类内部的内容之后,接着说明类之间的关系,今天是关联的基础篇。
什么是关联
关联是两个或多个特定类之间的关系,它描述了这些类的实例之间的连接。在问题陈述中,关联经常以动词(或动宾)形式出现。
比如学生和老师之间的关联,如果以学生为起点,老师为终点,那么这种关联就可以称为获取知识(AquireKnowledge)。如果以老师为起点,学生为终点,那么这种关联就教授知识(TeachKnowledge)。有教就有学,一体两面。
关联本质上都是双向的。但是在读的时候要按从起点到终点的方向来读。
下面是AquireKnowledge在UML中的表现方式。
关联就是连接Student类和Teacher类之间的那条线,上面带有关联名AquireKnowledge。下面介绍关联的表示/设定方法。
在增加关联关系之前,首先打开类图并增加两个类:Student和Teacher。
接下来点击工具栏中的Associate图标(如下图),然后在Student类上按下鼠标并拖动鼠标到到Teacher类后释放。这里的方向是有意义的,拖动开始的类就是关联的起点。
在生成的直线上双击鼠标以打开如下的AssociateProperty对话框。
在名称栏中输入AquireKnowledge,同时确认右边的属性列表,可以看到Source项目的内容为Student,Target项目的内容为Teacher。
接下来选择Role(s)分类,在SOURE和TARGET两边的列表中都可以看到Multiplicity项目,这个项目叫多重度,后面会讲到,先都输入选择【*】。
这样就可以得到本文一开始的那张图了。
多重性
多重性指定了一个类与其关联类的单个实例可能相关的实例数目。也不知道为什么这种定义总是那么难以理解。还是结合上面的例子来说明吧。先假设这里的一个类是Student类,那么它的多重性就指定了一个Teacher类的实例可能与多少个Student类的实例相关。
多重性的标准格式为:minimum..maxmun,minimum和maxmun都是整数,maxmum也可以是“*”,表示无限多。例如:[1..*]就表示1个到无限多个。
区间还可以一个单独的整数来表示。
先看下面的例子。
有两个多重度的设定值。首先Student类侧的1,表示的是一个Book类的实例只能和1个Student类的实例相关。Book类则为[0..*]表示一个Student类的实例可以和0到无限多个Book类的实例相关。
关联端名
我们也可以给关联的两端指定名称,例如在上面的Has关联中,可以指定Student端的名称为owner,指定Book端的名称为belongings。
关联端名的设定也是通过下面的AssociationProperty对话框来进行的。
设定关联端名以后,类图就变成下面这样。
关联端名一般以名词出现,大多数场合关联端的命名会比关联的命名更容易一些。一旦指定了关联端名,就可以省略关联名。
有序性
昨天的文章算是关联的基本内容,不大好理解,但是非常重要。对于面向对象的建模,识别类当然是第一步,接下来就是要识别类之间的关系,也就是关联。可能会觉得有点虚,但是这是设计向上游发展的表现,请务必认真体会并加以练习。
当某个关联端的多重度被指定为一以上时,并没有强调这些对象是不是有序的,也没有明确对象的值是不是可以重复。这样的建模结果不够精确。其实很多场合是需要确定这些信息的。在UML中,把这种信息成为有序性,有序性关键词可以放在属性串的后面。
下面就以一个事件处理系统为例来说明。
首先是按照发生先后处理事件的情况,这时候事件是按照发生的时间次序排列(Ordered=True)的,又因为同样的事件可能多次发生,所以队列中的值是可以重复(Allow
Duplicates=True)的。这种情况UML称之为{sequence},类图是下面这样的。
另一种情况是按照事件优先级进行处理。这时候需要两方面的信息。一个是EventHandler,管理所有发生的事件,这些事件是无序(Ordered=False)的,允许重复(Allow
Duplicates=True)的;另一个是优先级信息队列EventPriorityQueue,这个队列管理的是事件的优先级,是有序(Ordered=True)的,不允许重复(Allow
Duplicates=False)的。EventHandler向EventPriorityQueue询问优先级后按照结果处理事件。EventHandler的情况UML称之为{bag},EventPriorityQueue的情况UML称之为{ordered}。以下是类图。
接下来说明这两种信息的设定方法。进入关联端的设定对话框后,通过下图红框中的项目,就可以分别设定是否有序和是否允许重复的选项了。有一点需要注意的是,只有在指定了多重度以后,设定结果才会在类图中表示出来。
一共有两个设定项目,四种组合,归纳起来就是下面这张图。
你一定注意到左下角的空白,UML并没有像其他三种情况一样给个说法,而是作为初始(缺省)状态,在这个状态下,元素是无序的(Ordered=False),同时每个元素又是唯一(Allow
Duplicates=False)的。这实际上就是集合。
关联类
正如可以使用属性描述类的对象一样,也可以用属性来描述关联。UML用关联类来表示这样的信息。关联类(association
class)是一种关联,也是一个类。
-----------UML面向对象建模与设计
也不知道你是懂了呢还是懂了呢?还先从一个简单的例子开始说起吧。
有一个温度控制系统,通过传感器测量温度。传感器的输出是1v到5v,对应的温度为0到100摄氏度。
控制器每0.1秒获取一次温度值,然后根据实际温度和期望的温度的偏差来决定输出值,计算的周期为1秒,输出值的范围为0%到100%。这个输出值发送给一台加热器来控制温度。
加热器的控制端在输入1v时的输出功率为0KW,输入5v是输出功率为10KW。
这个系统应该如何建模呢?先来第一步,识别类和关联。
接下来将全部信息都反映到模型上。
不要被满篇的属性吓倒,耐心地,慢慢地读下去,你会理解的。
传感器Sensor
传感器Sensor的功能其实就是将现实世界中的0度(m_tempLow)到100度(m_tempHigh)的温度经过线性变换转换成1v(m_outputLow)到5v(m_outputHigh)的电压信号。
控制器Controller
控制器每0.1秒(m_sampleCycle)获取一次Sensor的电压输出,将这个电压值从范围[m_sensorOutoutLow,m_sensorOutputHigh]线性变换到[m_tempLow,m_tempHight]之间,然后由控制器根据实际温度的和期望温度之间的偏差来决定输出值,计算的周期为1秒(m_controlCycle),输出值的范围为0%(m_controllerOutoutLow)到100%(m_controllerOutoutHigh)。这个输出值再经过线性变换变成一个[m_heaterInputLow,m_heaterInputHigh]之间的值,发送给加热器来控制温度。
加热器Heater
加热器的控制端接受到1v(m_intputLow)到5v(m_intputHigh)之间的电压值以后经过线性变换转换成0KW(m_outoutLow)到10KW(m_outputHigh)之间的功率。
谢谢你坚持读到这里。回头来审视一下模型,有没有发现什么问题?
是的,Controller类太大,功能也太多了。
这个问题的解决方案就是本文的话题:关联类。当关联足够复杂,复杂到必须需要利用属性来描述细节,利用操作来定义动作的时候就该关联类出场了。
在本例中取得温度值(GetTemperature),输出(SetOutput)两个关联,要进行线性变换,各自需要四个属性(实际上还应该有操作),都可以定义为关联类。就像下面这样。
前面废了那么多口舌,到主角的时候反而简单了。
如果要总结的话,关联类的内容就是补充描述关联的那些信息。
限定关联
从一个例子开始今天的说明。
假设有一个系统,收到外界的事件通知以后,根据设备Id,将事件转发给适当的设备。按照之前的说明我们可以建模如下。
系统按照以下方式运行:
EventCreater生成Event并设置DeviceId
EventCreater将生成的Event发送给EventDistributor
EventDistributor根据DeviceId检索对应的Device
将Event发送给Device
对于每一个EventDistributor,可能有多个EventCreator向它发送Event。也可能有多个设备接收由它转来的Event。
为了提高检索速度,我们将SendEvent关联的Device端的有序性设定为{ordered},即:结构有序,而且在这个列表中每个Device只能出现一次。
我们知道,有序性为{ordered}的数据结构,可以是数组,也可以是链表。查询是一般采用的线性查询。这种设计可以实现功能,而且被大量使用着。
怎么样,够了么?
应该有很多人想到了,还不够快,可以哈希表,B树嘛!对了就是这个。我们今天的话题:限定关联。利用限定关联以后,类图会变成下面这个样子。
注意观察EventDistributor右边的小框。这种表达方式就是限定关联。图中的EventDistributor和Device之间的SendEvent关联可以理解为:在EventDistributor中通过deviceId可以决定唯一的一个Device。
进一步讲,引入deviceId限定符以后,除了通过deviceId取得唯一的Device这件是意外,它还附带了另外的含义:应该让这种操作更有效率,差不多就等于要求采用更有效率的数据结构。
限定关联还是通过关联端属性设定对话框进行的。
图中有两处变化,一是红框中Qualifiers项目设成了deviceId,而是绿框中多重度从“*”改到了1。
泛化(generalization)
类和类之间,除了存在关联/聚合/组合这种协作关系以外,还有泛化关系,也就是C++中的继承关系。
定义
泛化是指一个较特殊的类到一个较普通的类之间的关系。较特殊的类也叫子类(subclass);较普通的类也叫超类(superclass)。子类继承了超类的所有特性(属性和操作),任何使用超类的地方,都可以用子类代替。
表示法
泛化表示为从子类到超类的实线,超类端带有空心三角形。
在本例中,File类的功能已经很完整,可以独立使用,但是我们需要支持文本文件和Utf文件的行读写功能,于是增加了两个子类TextFile和Utf8File,它们一方面完整继承了File的所有特性,一方面又为用户提供了利用者需要的读写文本文件和Utf8文件的便利功能。
这种泛化关系虽然可以满足利用者的需求,但是没有人会在使用File的地方替换使用TextFile或者Utf8File,而是把它们作为另外的类来使用。还有一点:很难找到漂亮的方法避免用户使用File类的Write/Read方法带来的混乱。可以说这种泛化是没有经过认真设计的泛化,或者说是被动的泛化。
抽象类和具象类
还有另外一种情况,在设计时就考虑好超类,子类的分工,共同的部分由超类实现,特殊的部分由子类实现。
在上图中,图形尺寸,位置的处理由Shpe类负责;表示的部分则在Shape定义Show操作,具体的Show方法由各个子类实现。因为Shape类没有实现所有的功能,所以不应该被实例化。关于这一点,UML提供了方法,就是将Shape定义为抽象类。在EA中表示为斜体的类名。设定方法是在类属性的【detail】页中,选中Abstract选项。具体如下图:
在这种场景下,我们称Shape为抽象类(abstract class),各子类为具象类(concrete
class)。
实现(realization)
前面讲到了抽象类和具象类。其中抽象类是不能被实例化的类。这即可能是因为类的实现还不完整(如缺少某些操作的方法),也可能是因为功能不完整而不想被实例化。与之相对的就是具象类。
接口
但是一般来说,抽象类还是有一些功能(属性,方法)的。我们继续简化(抽象化),直到只剩下公开的抽象操作,而没有了属性和方法,这种状态UML有一个专门的名字:接口(interface)。
接口用来定义一组公共的特性和服务,是服务提供者和利用者之间的协议,定义接口的目的就是为了替换由不同的服务提供者提供的实现;抽象类抽取了具象类的共通特性,并通过具象类实现完整的功能。目的在于抽取共通而不是定义行为。二者的使用场景有很大的不同。
实现(realization)
具象类到抽象类的关系叫泛化,接口的实现到接口的关系就叫实现(realization)。
表示法
在类图中,接口和类的表示基本一致,只是在类名上多了一个《interface》关键字。实现则有两种表现形式:一是指向接口类的顶端带有三角形的虚线;另一种方式是带有《interface》关键字的依赖箭头。
尽量用左边这个吧。
更多更新文章欢迎关注微信公众号:【面向对象思考】
|