





UML对很多人来说应该不是一个陌生的概念,这一两年来,UML被大家越来越多的讨论着。本来UML跟我这个主题似乎并不能扯上多大的关系(它是语言无关的,甚至可以说其本身就是一种语言——用于交流的)。我在此谈到它有两个目的:
1. UML是针对面向对象软件开发的,而C++正是这样的一种语言
2. UML在设计中被越来越多的使用着,而下一篇杂谈准备讨论设计模式,如果不了解UML,那么无法进行下去
UML,全称:Unified Modeling Language,其目的是为了对软件密集型的制品进行可视化、详述、构造和文档化的图形语言。UML是依据许多前人的思想总结出的成果,1997年被OMG通过并成为标准(所以在《设计模式》书中如果你看到与标准不一样的地方,不要奇怪,那本书是95年的)。关于UML的历史和更详细的描述,可以参考《UML 参考手册》。UML主要由一系列视图组成,其中包括静态视图(Static view),用例视图(Use case view)活动视图(Activity view)等,不同的图用处自然也不一样,而对开发人员来讲(或者说为我的下一篇来说),更重要的应该是静态视图中的类图(class diagram)和交互视图(Interaction view)中的顺序图(Sequence diagram),请注意view和diagram的区别。
类图
静态视图说明了对象的结构,其中最常用的就是类图,类图可以帮助我们更直观的了解一个系统的体系结构,有时侯,描述系统快照的对象图(Object diagram)也是很有用的。在这里,我们主要介绍类图,下面的图就是一个简单的类图:
在类图中,类由矩形框来表示,如上图中,定义了4个类,分别为Base、A、B、C,类之间的关系通过各种线条和其他符号来表示,在上图中,空心的三角表示继承关系,在UML的术语中,这种关系被称为泛化(Generalization),所以上面的类用等价代码表示为:
class Base{…};
class A:public Base{…};
class B:public Base{…};
class C:public Base{…};
我们再看下一幅图:
这幅图与上幅几乎没有什么区别,唯一的不同就是Base类中增加了成员,一个私有的integer _x(UML术语为property)和一个公有的fun()的函数(method),是否需要这些类的内部细节UML本身并没有限制,完全取决于你自己如何使用,UML的用处在于帮助你了解系统,所以只要你自己觉得足够清楚,那么够了,不要再复杂了。
接着看第三幅图:
上面图中的箭头表示一种关系,箭头另一边有一个菱形(空心)表示聚合(aggregation),聚合的意义表示has-a关系,其等价代码如下:
class A{…};
class B{ A* theA;…};
聚合是一种相对松散的关系,聚合类B不需要对被聚合的类A负责。
下面的图:
这幅图与上面的唯一区别是菱形为实心的,它代表了一种更为坚固的关系——组合(composition)。组合表示的关系也是has-a,不过在这里,A的生命期受B控制,通常情况,等价代码如下:
class A{…};
class B{A theA;…};
即A会随着B的创建而创建,随B的消亡而消亡。
下图:
这里B与A的关系只是一种依赖关系,这种关系表明,如果类A被修改,那么类B会受到影响,一个简单的例子就是:
class A{…};
class B{fun(A params);…};
常用的关系就是我们上面用的这些,通过这些关系和类表示的类图,我们可以用图形化的方式描述一个系统的设计部分,当你习惯使用UML后,你会发现,这往往比你告诉同伴某某类从某某类派生,派生类又和某某类具有什么关系容易的多。
顺序图:
UML中另外一个常用的图形就是交互视图中的顺序图,在以往的过程化语言中,我们通常使用流程图来描述一个函数(系统)是如何工作的,而在面向对象的系统中,这显然是不可行的,而顺序图正是来解决这个问题的。
假设有如下的伪代码:
class circle{
public:
void fillcolor()
{
// ...
}
void draw()
{
fillcolor();
}
};
class window{
public:
void drawcircle()
{
_circle.draw();
}
private:
circle _circle;
};
对于下面的调用:
window wnd;
wnd.drawcircle();
对应的顺序图如下:
图中上方的方块表示参与的对象,垂直的虚线表示对象的生命线,方框表示激活,其中箭头表示了一个调用消息(也可以有回送return),如果是异步的消息,则用半箭头表示,其中draw表示了一个自调用(self call)
至此,UML中最常用的(从开发人员的角度),当然UML的内容远远不只这些,这里的介绍只是一些简单的概括,并且UML本身也在不断的发展之中,无论怎样,我觉得UML会越来越多的深入我们的开发过程中,特别是对下一篇我们要介绍的设计模式而言,类图是主要的描述工具(到那个时候你会体会到UML描述的优越)。
如果你看过《设计模式》着本书,你会发现与我上面所描述的有一些细微的不同,不要紧张,《设计模式》是GOF95年的作品,那时候UML还没有形成,而且,其中也明确那是OMT方法(Jim Rumbaugh在通用电气发表的建模技术——Object Modeling Technique)和Booch方法。如果你觉得UML有些让你无所适从,也不必紧张,UML本身只是一个辅助工具,它的目的是帮助你描述系统,不是复杂你的工作,如果你的系统很简单,一句话可以说的很清楚,那么不要用UML,如果你只想说明类之间的关系,而不是类的接口描述,那么像第一副图那样简单的描述就很好,总之不要去追求细节,只要能说明问题,那么你的目的就达到了(甚至你没有必要完全遵守规范)。
参考书目:
《设计模式——可复用面向对象软件的基础》
作者: Erich Gamma 等 译者:李英军 马晓星 蔡敏 刘建中 机械工业出版社2000
《UML参考手册》作者: James Rumbaugh Ivar Jacobson Grady Booch
译者: 姚淑珍 唐发根 机械工业出版社2001
《UML精粹——标准对象建模语言简明指南(第2版)》
作者: Martin Fowler&Kendall Scott 译者: 徐家福 清华大学出版社 2002
《Design Patterns Explained》Alan Shalloway, James R. Trott
Addison-Wesley Pub 2001
面向对象技术是目前流行的系统设计开发技术,它包括面向对象分析和面向对象程序设计。面向对象程序设计技术的提出,主要是为了解决传统程序设计方法——结构化程序设计所不能解决的代码重用问题。
面向对象的编程方法具有四个基本特征:
1.抽象:
抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。比如,我们要设计一个学生成绩管理系统,考察学生这个对象时,我们只关心他的班级、学号、成绩等,而不用去关心他的身高、体重这些信息。抽象包括两个方面,一是过程抽象,二是数据抽象。过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值只能通过使用这些操作修改和观察。
2.继承: 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。这也体现了大自然中一般与特殊的关系。继承性很好的解决了软件的可重用性问题。比如说,所有的Windows应用程序都有一个窗口,它们可以看作都是从一个窗口类派生出来的。但是有的应用程序用于文字处理,有的应用程序用于绘图,这是由于派生出了不同的子类,各个子类添加了不同的特性。
3.封装:
封装是面向对象的特征之一,是对象和类概念的主要特性。封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,即哪些特性对外部世界是可见的,哪些特性用于表示内部状态。在这个阶段定义对象的接口。通常,应禁止直接访问一个对象的实际表示,而应通过操作接口访问对象,这称为信息隐藏。事实上,信息隐藏是用户对封装性的认识,封装则为信息隐藏提供支持。封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
4. 多态性:
多态性是指允许不同类的对象对同一消息作出响应。比如同样的加法,把两个时间加在一起和把两个整数加在一起肯定完全不同。又比如,同样的选择编辑-粘贴操作,在字处理程序和绘图程序中有不同的效果。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
面向对象程序设计具有许多优点:
1、开发时间短,效率高,可靠性高,所开发的程序更强壮。由于面向对象编程的可重用性,可以在应用程序中大量采用成熟的类库,从而缩短了开发时间。
2、应用程序更易于维护、更新和升级。继承和封装使得应用程序的修改带来的影响更加局部化