在公司文档库中发现一篇关于数据库系统在面向对象分析设计中的应用,感觉蛮有意思的,给大家分享下。(篇幅有点长)
前言:
本人一直想真正用面向对象的方法分析一个系统,并带着问题学习过几本面向对象系统分析的书(大多为老外所写),可能是本人太笨,总是看到一半就看不下去。
大部分的面向对象方法,给我的感觉就如老外的厨房用刀与中国人厨房用刀,老外有很多种刀,剁肉有剁肉刀,砍骨有砍骨刀,可中国人只有一把刀,干什么都用它。同样,在面向对象领域,很多方法为解决某一问题(或为精确描述某一问题)而引进或创造了新的概念,这些新的概念令我很是困惑,总是与实际联系不起来。我想找一把中国人用的刀,因为最简单的就是最好的。
有幸学习了邵维忠、杨芙清所著《面向对象的系统分析》,该书仅用最基本的面向对象概念,解决了大部分的问题,因该书的可操作性极强,因此尝试应用该书的理论编写一个系统。
刚巧父亲的橡胶厂因业务扩大,考虑采用计算机管理,于是,决定用该书的理论尝试我的第一个面向对象系统分析。
在分析过程中,大多数的问题都可依据该书的理论得到解决,但也有该书中没有描述到的问题,其中之一,就是怎样将数据库的优势应用在面向对象分析领域。
在本版各位网友的热情帮助下,经过不断的实践,不断地在理论上提升,我探索出了一种将数据库系统应用在面向对象分析设计领域的方法,并利用此方法成功地实现了一个管理系统。
下面我将我的一些体会与想法写出来,请大家指正,由于我想将文章最终整理成册,大家可以自由转载本文,但在转载时,请注明作者,也请注明出自本论坛。
本文除本人自定义的概念外,其余概念均引出《面向对象的系统分析》。
一、 理解类与对象实体集合的关系
这一点,是必须在建模时首先要搞清楚的。
类的定义通常是这样的:类是具有相同属性的服务的一组对象的集合,它为属于该类的全部对象提供了统一的抽象和描述,其内部包括属性和服务两个部分可见,类仅是个对象属性的描述,它给出了属于该类的全部对象的抽象定义,而对象则是符合这种定义的实体。
类描述了全部的对象,类不能用来枚举已经实体化的实体集合,这些实体仅是全部对象的其中一部分,类不是一个系统中已有对象实体的集合。
这里提到已有对象实体,是指在一个系统中,从类实体化出来的有限的实体集合。
少量的对象实体,可以在程序中实例化。当系统中已有对象实体的数量很多(但永远不可能是全部)时,怎样管理(增加、修改、删除、查询)这些实体就是系统必须解决的问题了。
所以,在系统建模时,必须为管理这些已有对象实体而建模。这个模型是用于增加、修改、删除、查询已有对象实体的,我们且把这个模型叫做实体管理模型。实体管理模型在面向对象的分析设计中,也是一个类。
而数据的增加、修改、删除、查询功能正是数据库的强项,因此,实体管理模型对应着数据库。
举例来说,系统中有一“联系人”类,其模型为:
属性:姓名、性别、电话
服务:报价
当系统中需管理的联系人的数量较少时,可实例化出这些数量的对象,分别对它们赋值,形成有意义的对象。
当系统运行到某一时刻(或预计到某一时刻),系统中将出现大量的联系人实体时,在系统初始化时,对每个实体实例化并赋值,这样的实现方法显然是不切实际的。
而“联系人”类并不能描述这些已经实例化的实体,必须为管理这些实体另行建模。
在日常生活中,我们会将这些联系人记在本子上以实现保存、增加、修改、查找等功能。同样,在面向对象的系统中,我们也应该建立“联系人通讯录”这样的一个模型(类),这个类应该是这样的:
属性:联系人
服务:增加、删除、修改、查找、取出。
“联系人通讯录”类与“联系人”类是整体-部份关系,我们可以充分利用数据库系统提供的各种便利来实现“联系人通讯录”的服务。这个类,往往在建模时是容易忽略的。
值得指出的是,“联系人通讯录”类中的“取出”服务,是从通讯录中取出某联系人。在设计上,它应该是一个返回联系人实体的函数。这个函数,是连接数据库概念与面向对象设计的关键函数之一。
二、数据库可以是对象属性的永久保留形式
数据库显然是可以用来保存系统中已实例化的一些对象特征的,如属性。但是,数据库不能直接将整个对象存贮起来(至少数据库不能直接存贮对象的服务),也就是说,数据库不能直接作为永久对象的保留媒介。
这是因为:对象是包括了属性与服务的整体,属性与服务是对象不可分割的两部份。而永久对象是生存期可以跨越程序的执行时间而长期存在的对象。
怎样处理那些不能直接永久化的部份呢(主要是服务),解决这个问题有两个思路:
1、 以数据库作出发点考虑
就是说,把对象、类的那些不能直接保留在数据库中的部份(如服务),以数据库的概念及方法间接地保留在数据库中,从数据库中取出后再还原出来。
比如:我们可以这样考虑:“联系人”的属性是一个数据库,在这个数据库中增加一个字段“类名”(或类编号),记录属性所属的类,再新建一个数据库,叫“服务库”,将每一个类的服务记录下来,
这样,就建立了属性-方法的对应关系,每一个独立的联合记录也就构成了一个完整的对象。
2、 以面向对象的眼光考虑
数据库仅记录对象的属性,仅完成属性的增加、修改、查找、删除功能,至于对象中的“服务”等,不放入数据库中,而在面向对象的分析及设计以至最终的实现中解决。从这个意义上来说,数据库仅是永久
对象属性的保留,而不是整个永久对象的保留。
在仔细考虑了这两个出发点,并作了一定量的模型试验后,得出的结论是:第二种思路是比较现实,也比较可操作的。这是因为:
1、 第一种思路破坏了面向对象概念的完整性,那些保留在数据库中间接部份及其转换的过程在面向对象的分析及设计时又如何建模呢?
而第二种思路不仅保留了面向对象概念的完整,而且非常清晰。
2、 第一种思路无论是在设计上还是在实现上,都存在很大的复杂性,而第二种思路实现起来非常容易,目前绝大多数的OO语言都支持。
3、 从充分发挥数据库与面向对象语言的长处来看,第二种思路显然是最合适的。
基于第二种思路,将数据库与对象的属性联系起来必须解决从对象到数据库的存贮,及从数据库到对象的恢复两个方法,这两个方法可以这样解决:
1、 从对象到数据库的存贮。
对象属性的改变方式可以划分为种,一是录入性的改变,如增加、修改一个联系人,二是程序触发性的,如开关的“开”“关”状态,无论哪种方式,都必须把这个改变永久性地记录到数据库中。
对于录入性的改变,直接设计一个录入画面并将录入的属性存盘即可。
对于程序触发性的属性改变,在这些属性所在的类模型(类)中设计改变这些属性的服务。如记录开关状态的State属性,可在开关的模型中设计ChangeState 的服务,调用 ChangeState服务时,除立即修改内
存中State 属性的状态外,还要查找这一开关对象在数据库中的记录,更新该记录。
2、 从数据库到对象的恢复。
这就要用到前面所提出的“取出”函数。在永久属性所在的模型中设计“取出”服务,该服务首先根据属性所在的类,实例化出一个空对象(所有属性值均为初始值的对象),再根据取出的条件,从数据库中取出符合条件的对象的属性值,将这些具体的属性值赋予该实例的对应属性,最终返回这一具有实际意义的对象实体。
注意,当“取出”服务根据属性所在的类,实例化出一个对象时,这个空对象已经具有了该类所有的一切服务。
三、 在数据库中体现类属性的相互关系
如前所述,数据库中应该只记录对象的属性值,数据库不处理对象的服务,对象的服务是交给面向对象的分析、设计以至最终实现中解决的问题。因此类的关系中,数据库只需解决它们之间属性的关系。
数据库是对象属性的永久保留,而对象所在的类可能存在继承关系,属性可能是从其它类继承而来的,因此,类的继承关系必须在数据库中得到体现。(根据本文的方法,类的继承关系的体现是必须解决的。一些面向对象的数据库理论,从各自的理论出发,有的避开了这个问题;有的纯从数据库的概念出发,另类解释继承的概念;有的只是其中的字段可以面向不同的对象而已)
类之间不仅仅存在继承关系,还有其它的关系。同样,所有的这些关系,在数据库设计中都是必须得到体现的。避开这些关系、另类解释这些关系的概念,都将破坏面向对象分析设计或数据库理论的完整性、统一性,都不能正确将面向对象的分析设计与数据库理论真正地结合起来。
所以,在讨论解决数据库体现类属性的相互关系前,我们必须先讨论一下类与类之间的关系。
按邵维忠教授的理论:类与类之间的关系,可以分为四种:整体-部分结构、一般-特殊结构、实例连接关系、消息连接关系。其中消息连接关系与属性的关系不大,在此不予讨论。
1、 整体-部分结构
整体-部分结构的定义如下:如果对象A是对象B的一个组成部分,则称B为A的整体对象,A为B的部分对象,并把B和A之间的关系称作整体-部分关系。
整体-部分结构表现的是"has a "的关系,如:客户与联系人、电脑与CPU、电话与按键等。
2、 一般-特殊结构
一般-特殊结构的定义如下:如果类A具有类B的全部属性和全部服务,而且具有自己特有的某些属性或服务,则A中心任务B的特殊类,B 叫A的一般类。
一般-特殊结构表现的是"is a kind of "的关系,也就是我们所说的继承关系。如:MODEM与56KMODEM、窗户与铝合金窗、广场与安门广场等。
3、 实例连接
实例连接用于表达对象之间的静态联系。所谓静态联系是指最终可通过对象属性来表示的一个对象对另一个对象的依赖关系。
实例连接表达的两个对象之间依赖关系,如上级与下级间的领导关系,教师与学生的授课关系,显示卡与显示器的显示关系等等。
值得指出的是,邵教授在整体-部分结构与一般-特殊结构的关系中,有这么一段描述:"一般-特殊结构是使特殊类通过继承而拥有一般类的特征,整体-部分结构是使整体对象通过组装而拥有部分对象的特征。尽管途径不同,着眼点不同,结果却是一样的:一些对象拥有另一些对象的特征。正像我们所说:'子女通过遗传(继承)而拥有父亲和母亲的血统',也可以说:'子女的血统,是由父亲和母亲两种血统构成的'。说法不同,实质内容是一样的"。这就是为什么"冷藏车"既可以是通过继承"汽车"与"制冷设备"中的属性与服务而形成的新类,也可以是具有"制冷设备"的一种特殊的"汽车",是组装了"汽车"与"制冷设备"中属性与服务而成的新类。
为了更好地讲述数据库对类的关系的实现,先把在没有数据库的情况下,邵教授介绍的实现以上的三种关系的方法介绍一下:
1、 整体-部分结构
有两种实现方式:一种方式是用部分对象的类作为数据类型,静态地声明整体对象中这个代表部分对象的属性变量,这样部分对象就被嵌入到整体对象的属性空间中,形成嵌套对象。另一种方式是把整
体对象中的这个属性变量定义成指向部分对象的指针,或定义成部分对象的对象标识,运行时动态创建部分对象,并使整体对象中的指针或对象标识指向它。
2、 一般-特殊结构
利用OO语言的继承实现。
或者将一般-特殊结构在建模时转化为整体-部分结构,用整体-部分结构的方法实现。
3、 实例连接
实例连接的概念相比之下比较难以理解。尤其是复杂的实例连接。比如:用户使用某台工作站的关系,可能要求附加表明"优先级","使用权限"等与这个关系密切相关的其它信息。
OMT方法学引进一种新的概念来解决这个问题(又多了一把刀),叫"作为类的关联",认为关联是一种类,既有属性,又有操作。链(即实例连接)是关联的对象实体。
邵教授认为:对象不仅可以用于表示有形的事物(如用户、工作站),也可以用于表示无形的事物(如使用权)。当两类对象之间的实例连接比较复杂时(带有一些属性或操作),说明在它们之间存在 某种尚未用对象加以描述的事物。在上面的例子中,用户与工作站之间在建模时应建立一个"使用权"类,该类的属性有"优先级"等,每种使用权可以给多个用户,也可以给多个工作站。
有了以上的基础,只要解决了上面上面的三种关系,将数据库理论应用于面向对象的分析、设计与实现是完全可能的。下面,我将我的实现方法提出如下:
1、 整体-部分结构
先举个例子:一个公司客户有多个联系人,"客户"类与"联系人"类是整体-部分结构。
联系人的数目有限时,可以直接用嵌入的方法将联系人嵌入到客户中,或用指针方法在程序中实例化出来。当联系人的数目达到相当数量时,必须考虑这些联系人的增加、删除、修改等管理功能。
于是,应该在客户的模型(类)中,建立一个"联系人列表"属性,将已经实例化的联系人资料(属性)永久保留下来。"联系人列表"是"联系通讯录"类的一个实例。
如前所述,"联系人通讯录"是一个基于数据库操作的类。其内部有一个对应数据库的表,在此表上建立增加、删除、修改的服务。最重要的,用"取出"服务及录入、修改对应对象数据库属性的方法实现对象与数据库间的保存与恢复。
所以,在对"联系人"这一数据库进行设计时,必须设计一个"客户"(或客户编号)字段,描述这一联系人所从属的客户。当从"联系人通讯录"类中实例化出"联系人列表"时,就从联系人数据库中根据客户编号 SELECT出联系人,这些联系人集合,就是该客户已经实例化所有联系人的集合。
2、 一般-特殊结构
这是我们讨论的重点。为了更好地解释我的方法,先假定有一个这样的系统:系统中有一个"人员"类,其模型是这样的:
属性:姓名、性别
服务:为便于理解,暂无
系统中还设计了"员工"模型与"联系人"模型,它们都从"人员"中继承而来,除了"人员"中所有的属性与服务外,特殊化的属性与服务是:
员工:属性:籍贯、身份证、出差状态
服务:出差
联系人:属性:所在单位、电话、手机
服务:报价
联系人类又有一个子类,子类名为"区域联系人",与联系人类不同的是,有一个叫"所在区域"的属性,其模型为:
区域联系人:属性:所在区域
服务:暂无
显然,员工、联系人、区域联系人的实例化对象达到一定数量时,必须管理这些已实体化对象。为简化描述,只对联系人、区域联系人的已实体化对象建模,叫"联系人通讯录"、"区域联系人通讯录"
现在以上面的模拟系统为例,解释本文解决数据库实现继承关系的策略:
★ 将父类的属性组装到子类中实现继承
将一般-特殊结构与整体-部分结构是实现"一些对象可以拥有另外一些对象的特性"的两种方式的理论推而广之,将父类的属性组装到子类中是有理论根据的。
在面向对象的分析与设计中,分析与设计是主要的,数据库仅是实现永久属性保存的一种辅助。因此,在分析阶段,该用继承的概念还是继承,无需转化为数据库可以实现的整体-部分关系。在数据库的设计阶段,将具有继承关系的、而又需要永久保留的属性考虑用组装的方法。
在分析时,"联系人"继承"人员"的特征,这种描述显然比"联系人"有"人员"的特性这一描述准确、自然得多,因此,分析时,"联系人"与"人员"仍是继承关系,但在数据库设计量,可将人员的属性组装在"联系人"的属性
中,共同形成"联系人"数据库数据库结构。
组装父类、子类属性的方法可以分为紧密组装与连接组装。
紧密组装,是指父类的属性直接与子类的属性组装在一起,共同形成子类的数据库结构。连接组装指的是父类的属性与子类的属性并不在同一个实际数据库中,它们各自所在的数据库通过某个字段相联系,就如数据库理论中常见的Master-Detial结构一样。
"联系人"与"人员"数据库显然可以紧密组装"人员"的属性。但考虑这样一个从联系人中派生出来的类"区域联系人",这个类除了联系人所有的特性外,还有一个"所在区域"属性,而且很有可能某个区域联系人是在系统中某个已经实例化了的"联系人",这个时候,要用连接组装。
考虑紧密组装还是连接组装可以依据下面的原则:
A、 父类的抽象程度
父类的抽象程度越大(离现实生活越远),考虑紧密组装的可能性就越大。如"人员"这一比较抽象的概念,其子类"联系人"的数据库设计应该考虑的是紧密组装。
B、 要组装的父类的属性数量
如果经过仔细地审查,只有一两个属性的父类仍然有存在的必要,那么,完全可以考虑在数据库设计时将全部的父类属性组装到子类中。――因为现在的数据库技术与存贮容量,完全有能力承受。
C、 父类的已有对象实体是否已用数据库管理
如果父类的已有对象实体已经用数据库管理,尤其是子类的对象实体可能是父类已有实体的某些特性扩充时,显然子类与父类间应该用连接组装。正如"区域联系人"与"联系人",为区域联系人再建一个与联系人数据库的数据结构几乎一样的数据库显然是不合理的,更何况其中可能出现某区域联系人实体实际上就是联系人的某个实体的情况。
按照连接组装的概念,在数据库设计时,除了要在子类的属性数据库中有对象实体的唯一标识字段外,还要有标定该对象实体与父类属性数据库中相应对象实体相连接的字段。"区域联系人"的属性数据库结构(字段)应该是这样:
区域联系人编号
(父类)联系人编号
所在区域
下面再来讨论讨论对象的恢复问题。在子类的"取出"服务中,首先实例化出一个子类空对象(所有属性值均为初始值的对象),此时,这一实例化的子类空对象,已经从父类中继承了所有的属性。
对于紧密组装,由于父类与子类的属性值全部都紧密地组装在一个数据库中,将数据库中相应对象的各字段的值赋予子类可能拥有的所有属性即可。这里所说的"可能拥有的所有属性",包括了从父类继承过程的属性,从而最终返回这一具有实际意义的对象实体。
对于连接组装,由于子类数据库中只有相对父类特殊的属性,所以除了从子类属性的保存数据库中取出子类的特殊属性值外,还要用连接找到父类对应的属性保存数据库,取出其属性值,给这一实例中的相应属性赋值。
这样,一个包括全部父类、子类的属性的对象就产生了。
当"取出"服务根据属性所在的类,实例化出一个对象时,这个空对象已经具有了该类所有的一切服务,包括从父类中继承的所有服务。当这个类所有的属性值(包括从父类中继承的)赋值完成后,一个可跨越系统的运行期而存在的、系统中曾经实例化的已有的对象实体就被完完整整地还原出来了。组装连接可以解决多重继承的关系。对于组装连接,"联系人通讯录"与"区域联系人通讯录"没有任何属性方面的直接连接,一方面,使各自的对象可以封装得更好,另一方面,也产生了新的问题,考虑这样的一种情况:在"区域联系人"的"增加"服务中,由于"区域联系人"的对象实体数据库除了"所在区域"属性外,"姓名"等都 在"联系人"对象实体数据库中,是否先调用"联系人"的"增加"服务(画面),完了后再录入这个"所在区域"呢?这个方法的实际操作将是:一个画面录入联系人的资料,关闭这个画面后再录入唯一的一个资料"所在区域",显然,这是极不合理的。
将这种情况推而广之:怎样解决在组装连接中,修改(无论是录入性修改还是程序触发性修改)子类中从父类继承下来的、已经永久保留的属性。这就是下面所要论述的:
★ 所有要修改永久属性的服务均应是多态的。
这个问题可能已经不是系统分析中的问题,可能已经涉及到设计甚至到编码阶段的问题。但是,借用设计阶段或编码阶段的概念,可以解决分析中出现的一些实际问题。分析时只是借用,并不是完全套用。
所谓"多态",是指一个服务有多种算法或实现。就是说,一个相同的服务名,可以具有不同的实际实现。
在C++中,可以用静态编联的"重载",也可以用动态编联的"虚拟函数",在PASCAL中,可以用"Override"。
对于永久对象属性的录入性修改,重新设计录入画面,将属性录入并存盘即可。比如上面所提到的"联系人"的"增加"服务和"区域联系人"的"增加"服务,"联系人"的"增加"服务是针对联系人对象数据库的,而"区域联系人" 的"增加"服务是针对区域联系人对象数据库的,两者的录入画面不同,且区域联系人的录入画面已经包括了联系人的资料录入。
对于永久对象属性的程序触发性修改,如前面所提到的"开关"类的ChangeState 服务,修改的服务必须是虚拟的,如果属性所在的实际数据库与原服务所处理的数据库不同,必须针对这个实际的数据库另行设计算法,即是实现虚拟。举例如:假定有一个"电流开关",其与"开关"是继承关系,系统存在2个数据库,一个是一般类开关数据库,记录的"开关"对象的已有实体,另一个是"电流开关"数据库,记录的是"电流开关"对象的已有实体,电流开关数据库对父类的"开关状态"属性采用紧密组装,系统要求对所有的开关初始化置"关"状态。在OO语言实现时,只需用一般开关(最层父类)的ChangeState服务遍历所有开关(包括一般开关与电流开关)即可。这样,就必须分别为一般开关设计一个ChangeState服务,这个服务是虚拟的,修改是一般类开关的数据库,子类"电流开关"必须针对"电流开关"的数据库结构重新编写ChangeState服务,当系统通过父类的ChangeState遍历到电流开关(子类)的实体时,OO语言的编译系统将根据多态的原则,认为子类(电流开关)的ChangeState是真正要调用的服务。
3、 实例连接
实例连接的实现用数据库实现非常容易,实现的方法是将实例连接类建库,将实例连接的属性组成数据库结构,同时将实例连接的连接两端也加入到数据库结构中,使两个数据库通过实例连接数据库连接起来。
如上面所说的操作员与工作站之间的"使用权"实例连接,使用权数据库除了要有"使用权限"等属性外,还应加上:"操作员编号"及"工作站编号"两个属性。
值得注意的是,操作员编号及工作站编号并不是"使用权"类的属性,就是说,在分析建模时,这些属性并不是"使用权"的类属性,但在"使用权"的已实例化对象实体中,必须表明"谁用什么使用权使用哪台工作站的
关系,因此记录已实例化对象实体的数据库必须有"操作员编号"及"工作站编号"两个属性。
至此, 数据库在面向对象数据库中的应用已经论述完毕,下面再将本文
的要点简述如下:
★ 正确认识类、对象、对象实体、已实例化对象实例的概念;
★ 必须为管理系统已实例化的对象实体建模
★ 管理已实例化的对象实例可用数据库手段;
★ 数据库仅保留已实例化对象实体之属性;
★ 数据库在面向对象的分析设计以至实现中都仅是一种辅助;
★ 永久对象属性的任何变化必须保留到数据库中;
★ 必须设计"取出"函数从数据库中恢复对象;
★ 整体-部分结构的已实例化对象实体可在整体对象模型中建立包容部分对象的属性,这个属性可以是部分对象的已实例化对象集合。
★ 一般-特殊结构可用组装代替继承,组装分为紧密组装与连接组装,要修改永久属性的服务均应是多态的。
★ 实例连接的已实例化对象实例必须要有被连接的两个对象。
利用本文提出的方法,已经成功地实现了一个系统。这个方法成功地解决了数据库在类的整体-部分关系、一般-特殊关系、实例连接中的应用。数据库在面向对象的分析、设计中,定位清晰,结合紧密,可操作性强,且容易实现。实现了数据库、面向对象中的相关概念的统一。同时,由于此方法仅利用了数据库最基本、最简单的功能,因而适应性强,且可靠性很高。