Work Better Than Yesterday!

zhangge's stupid and messy life


Home| Life| Technique Concentrate On One Thing.

作为面向对象的程序员,不懂UML,不如回家种番薯

17 Jul 2012

1. UML是什么?

当你还没有写代码的时候,你已经对这个系统是胸有成足了,其实就是对这个软件系统进行了建模,把抽象的代码转换成了可视化的模型,那么就需要一种建模语言来表示,建模语言有很多种,UML(Unified Modeling language统一建模语言)就是统一了大部分而成为了标准的。
它是帮助面向对象程序设计(Object-Oriented Programming, OOP)的一种建模方法。那么问题来了,什么是面向对象编程?

2. 面向对象编程

它既是一种自上而下的编程思想,又是一种自下而上的程序设计方法。

现实中的对象是某种可能被人感知的事物,也是思维、感觉或动作所能作用的物质或精神体。而所谓软件对象,是一种将状态和行为有机结合起来形成的软件架构模型,它可以用来描述现实世界中的一个对象。

其实它就是类,也是类的实例化。它具有抽象,多态性,封装性,继承这些绝对的优势。这让我们很容易混淆面向过程的结构化编程,因为它也是自上而下的,划分成多个函数去实现,模块化,细想并没有多大的区别。其实不用想太多,都是编程而已,只不过思想不一样罢了,本质都是一样的。它们不可能完完全全分开的,是有非常多的交叉的。

最大的区别在于设计软件系统的时候大家的方法不一样(方法论不一样),面向过程的就不断地分析软件的运行过程改怎么样怎么样,每一步设计都非常的详细,划分出很多个模块,模块之间的调用顺序都非常明确。而面向对象的设计方法就是,对这个软件世界进行建模,然后建立多个对象,也就是类,定义好这些类的属性,这些类可以执行什么动作,有哪些状态,会接受什么消息或者事件而出发动作。所以说,面向对象是一种以对象为基础,以事件或消息来驱动对象执行处理的程序设计技术。

我个人的经验,其实两者是结合着来使用的,一开始对软件进行需求分析,建立软件框架的时候,用到面向对象的方法论,利用四个特性是非常的有效,非常的快速,能定位到需求和实现。抽象,信息隐藏,模块化(对象),高内聚,低耦合,绝对的设计准则。但是在每一个对象具体实现一些功能的时候,还是需要用到面向过程的编程方法,毕竟,这非常适合我们的思考方式,明确快捷,一步一步来编写代码。其实,这不就是同步和异步的结合么?异步来编程是很痛苦的,不适合我们普通人类的思维方式,而且很难发现问题,同步就不一样了,思路简单明确。

所以说一个好的软件框架,肯定是用面向对象的方法论实现的,能快速分析复杂的需求,完成设计,肯定也是用到面向对象的思想,但是到了具体去写每一行业务逻辑代码的时候,你肯定是按照面向过程的套路去写的。你想想,架构要面向对象的,但是算法实现总不能也是面向对象了吧?然后还有一个重要问题就是要思考到效率问题,有时候可能面向过程的实现更好,而且性能效率好,那就认真思考怎么优化了。

好吧,这个东西非常高大上,博大精深,思想永远说不完,我这里就总结了一下自己的经验心得和用得非常多的,也是对自己成长有绝对帮助的东东了。因为它确实在我工作中起到了非常大的作用,使用它使得每个需求都解决得心应手。

UML的核心是图,建模就是画图,因为图很明显地表现出来了你的软件世界是怎么样的。下面我就总结一下。

3. 用例图

第一个要用到的是用例图,对于一个软件,你首先想到的就是,它是干嘛的?那么它是给谁用的?不管你的用户是真实的人,还是被别的模块调用,总得有用户用某一个功能。分析需求的过程就是这样的了,你从高层分析软件有哪些功能,或者功能的集合,其实就是用例,功能无非就是一些列操作的集合,有输入和输出;然后分析哪些人来使用这个功能(用例)。对于功能的定义需要经验的积累。

只需要确定四个元素就可以了:参与者(角色),用例,系统边界,关系。

参与者(actor)是指存在于系统外部并直接与系统进行交互的人、系统、子系统或类外部实体的抽象。

我们很容易就可以确定参与者,而参与者之间的关系是泛化(继承)关系。参与者与用例之间的是通信关联关系,即双向的关联关系。

系统边界是指系统与系统之间的界限。

如果系统比较庞大,由多个子系统组成就需要画出系统边界。

用例是参与者可以感受到的系统服务或功能单元,定义了系统是如何被参与者使用的,描述了参与者为了使用系统所提供的某一完整功能而与系统发生的一段对话;即站在用户的角度描述系统的功能。

一般而言,大而复杂的系统的用例粒度比较高,即一个用例包含的功能比较多,小系统就相反了,根据实际情况而定。如果系统比较大的话,我们也可以划分模块来画用例图,最后再画一个主用例图。

用例之间存在包含(include),扩展(extend)和泛化关系。包含和扩展用虚线和箭头表示,两者方向刚好相反。例如:添加资源和修改资源就包含了预览资源的用例(添加和修改指向预览);还书的用例可能就扩展出一个缴纳罚金的用例(缴纳罚金指向还书);电话订票和网上订票用例都继承自订票的用例(电话订票和网上订票指向订票)。

如图所示:

alt usecase

4. 类图

需求分析完了以后就是系统的设计了,整个系统的架构就要出现了(像是概要设计)。你需要分析系统有哪些对象,哪些类,不管是用户,还是用例功能,任何一个地方,都是一个对象,它包含了属性和能做的事情(方法函数,不用定义详细接口)。这时候我们不需要确定类的调用关系,只需要确定类之间的静态关系(依赖,泛化,关联,实现)就可以了。别忘了,工具类也是一个对象。

类图最重要的就是确定有哪些类或接口了,然后就是确定这些类或接口之间的关系,依赖,关联,泛化,实现。类图是静态的,从类图我们看不出逻辑,更看不出实现算法,我们只知道类的关系是如何的,就是了解整体的架构是怎么样的,具体你的实现逻辑我们依然不得而知,这也是我们经常犯的错误,以为从类图可以看懂全部,就死活纠结在这里。例如你看一个开源网络框架的时候,只有类图不要以为能看懂人家怎么实现的。

类图例子如下,我没有自己画了,选取了网上别人画的volley这个框架的类图,实践分析更好。这个图有几点不够好的就是,类没有把关键的方法写出来,类之间的关系基本都是采用了组合的关系,没有区分依赖,聚合这些关系:

alt volley_class

5. 序列图

建立了系统的对象世界以后,那么就是具体每一个用例的执行流程了(详细设计)。类与类之间,或者说对象与对象之间,怎么按照时间的顺序进行调用来完成一个用例功能。如果一个用例包含分支清楚,不需要在一个序列图把分支画出来,我们可以画出主要分支即可,其他情况交给状态图来处理,当然我们也可以给不同的分支都画一个序列图。这里你还是不需要考虑到每一个接口的参数如何,只需要关键的即可。序列图是最直观的,便于思考和讨论,平时我们口头上交流的最多使用到的就是这个序列图了。

直接调用或者递归调用都是用实线普通箭头,返回消息用虚线普通箭头(这两个用得最多了),过程调用采用实线实心三角形箭头,异步调用使用实线单箭头。除了调用以后,还可以发送消息,创建对象,销毁对象这三个操作,分别有不同的效果。

因为序列图简单,就是一个用例的过程,所以,下面就直接给出一个网上搜的例子:

alt sequencediagram

6. 状态图

完成每个用例序列图以后,基本上也就明白清楚了系统的流程了,可以写出主要的逻辑代码了;但是这样写出来的代码是很脆弱的,很不安全的,因为你还没考虑到异常的情况,不能想当然认为是对的。很可能对象处于一个非正常的状态下,按序列图出来的结果就是错误的了。每个对象都会有不同的状态,在不同的状态下该做什么操作,状态之间的转换方程是什么,我们必须清晰的考虑到,并作出相应的处理,我们的系统才够健壮。这就是我们平时所说的状态机了,作为一个程序员,状态机都玩得不熟,就是码农了!状态机就是对象的完整生命历程的模型了。

本质上一个状态图就是一个状态机,或者说状态图是状态机的特殊情况。它跟序列图一样,要给对象,用例等去画很多个图,所以我们只需要画出核心的,比较复杂的状态图即可。

实心圆表示初始状态,半实心圆表示结束状态,菱形表示分支选择,圆角方形表示状态,实现箭头表示状态的转换。一个状态除了名字以外,包括入口动作,执行动作,出口动作,一般我们只关心进入这个状态后会去做什么动作。触发状态转换的情况是,收到一个事件触发,或者对事件进行判断条件,还可能是在该状态下执行的某个动作导致状态转换。

下面给出我自己的一个上传例子,这个例子比较简单,还有很多状态和状态的事件没有画出来,但是已经比较全面的了,平时自己可以回顾一下:

alt upload_state_chart

完成以上四个建模以后,数据库设计,详细的接口设计其实都是清晰可见的了,这个时候写代码就简单多了,不需要过多考虑业务逻辑了,我们考虑更多的是怎么实现,采取什么数据结构和算法,不用考虑异步的过程了,直接同步地去愉快编程吧!

7. 活动图(流程图)

当一个用例比较复杂的时候,序列图还是可以应付几个对象的情况。但是如果一个业务单元级别上的控制流程用例,设计到两个或者多个用户的时候,用序列图来画就很繁杂了,例如出现分支情况,我们往上抽象一层,建立活动图。活动图出来以后,再按照分支或者单个流程分别建立序列图。

活动图是状态机的一个特殊例子,它强调计算过程中的顺序和并发步骤,用来描述动作和动作导致对象状态改变的结果,而不用考虑引发状态改变的事件,主要是阐明业务用例实现的工作流程。

其实活动图就是我们平时的最爱画的流程图,但是他比流程图更加规范,有更多的规则,更多的约束,而流程图只是简单描述业务的流程或者算法的流程,在以前面向过程编程的时候,流程图是必备的。一般来说,如果上面的四个图都不足以理解这个系统的话,才需要继续去画活动图,否则我们一般只要一个简单的流程图足以。

8. 协作图,包图,构件图,部署图

这些图没那么重要,协作图和序列图一致,只不过侧重点是在于对象之间的交互,不强调顺序,纯粹是描述对象之间的消息交互,所以一般有了序列图就够了。其他的几个图实在和我现在的工作关系少得可怜,就不在此总结上面了。

9. 关系

关系是指支配、协调各种模型元素存在并相互使用的规则。

依赖关系

一般用在类与类之间,用虚线箭头表示,如下图所示:classA依赖classB。即B发生变化是会影响到A的,依赖关系变现为:classA中的属性变量,形参和方法调用。

alt AdependencyB

泛化关系(Generalization)

泛化关系是事物之间的一种特殊(一般)关系,特殊元素(子元素)的对象可代替一般元素(父元素)的对象,即继承。

泛化关系可以用在类之间,接口之间,Actor之间等。用实线三角形箭头表示,如下图所示:

alt AextendB

实现关系

两个地方用到实现关系,一种是用在接口和实现接口的类或者构件之间,另一种是用在用例和实现用例的协作之间。用虚线三角形箭头表示:

alt AimplementB

关联关系

关联关系比较复杂,可以是聚集(Aggregration)或组成(Compose),也可以是没有方向的普通关联关系。

弱关联关系,只是一个普通的相关关系,例如我和我的朋友关系,代码上如ClassA使用到了ClassB的一个全局属性等。有方向的关联关系用实线箭头表示,没方向的关联关系只用实线表示即可。

聚合关系是强关联关系。聚合是整体和个体之间的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期;例如雇主和雇员的关系。用空心菱形加实线箭头表示。

组合关系也是关联关系的一种特例,这种关系比聚合更强;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;例如飞机和引擎的关系。用实心菱形加实线箭头表示。

关联关系和依赖关系在java代码上面感觉没多大的区别,都是一个类包含另外一个类的引用,但是语义上有很大的区别,依赖关系是会受到影响的,和组合关系很相似,另外两个关联关系不会发生影响。另外组合关系是有生命周期这个特点的。箭头上的数字表示该端的类的对象个数。

关联关系如下图所示:

alt associate

10. 工具使用StarUML

这个是最简单的了,也比较好用,上面的图都是staruml画的,你懂的!

虽然设计系统可以用uml来建模,反过来,其实我们分析系统,或者别人的开源框架的时候也可以按照这个套路来分析,试想一下,这几个图都出来了,这个系统/框架你还不懂么?


Sunday don't come easily! Subscribe to RSS Feed