使用EA画类图

本文介绍使用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:用于自定操作的并发属性。可能的选项有:

  1. sequential:同时只能有一个调用发生。如果并发调用发生,则结果不保证。

  2. guarded:允许并发调用发生,但同时只允许一个调用执行。

  3. 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,将事件转发给适当的设备。按照之前的说明我们可以建模如下。


系统按照以下方式运行:

  1. EventCreater生成Event并设置DeviceId

  2. EventCreater将生成的Event发送给EventDistributor

  3. EventDistributor根据DeviceId检索对应的Device

  4. 将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》关键字的依赖箭头。


尽量用左边这个吧。


更多更新文章欢迎关注微信公众号:【面向对象思考】




  • 25
    点赞
  • 2
    评论
  • 56
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页

打赏

面向对象思考

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值