数据库的一种完全面向对象设计模式 (实例已写好,欢迎大家讨论!) (0分)

  • 主题发起人 主题发起人 yyhhnn
  • 开始时间 开始时间
Y

yyhhnn

Unregistered / Unconfirmed
GUEST, unregistred user!
数据库的一种完全面向对象设计模式

1.1 完全面向对象和非完全面向对象

面向对象(OO)方法这个名字早已深入人心,它的科学性和合理性也已毋庸置疑。人
们动辄将自己开发的软件冠以“采用面向对象方法设计”以示其先进性就是一个极好的证明
。然而,一个先进的方法学必须有相应的工具支持才能实现,它的概念和方法如不落实程序
实现上,就不能真正掌握它的精髓而在实践中运用。诚然,SmallTalk语言已被公认是一个
面向对象语言,但是它对于开发者来说是多么的陌生!C++也可以说是一个OO语言,不过从
名字就可以看出他是C语言的一个变种。它实现了从过程式编程到面向对象编程的一个较好
的过渡。但是许多声称用C++制作的软件其实仍旧是C软件!这是因为没有真正掌握OO方法的
缘故。

即使开发者的开发环境,开发工具是支持OO的(如Delphi,VC),但开发者没有以OO的
观点去观察软件的问题域,或者以往的过程式设计思想根深蒂固,那么开发出的软件仍旧是
披着OO这件漂亮外衣的过程化软件。没有把OO作为一种确实实用的方法学。造成这种现象的
原因是没有认识到OO的精髓,尤其是在一些RAD开发工具下,更容易忽视以OO的观点去观察问题域。

RAD开发工具开发的软件常能分成两种形式:完全面向对象设计和非完全面向对象设计。一个
数据库软件系统的体系结构常能分为三层:用户接口层、中间件层和数据库层。在用户接口
层,常是一些具体与用户交互的对象,如:按钮、菜单、和对话框。在数据库层,则是从问
题域中找出描述实体的表。完全面向对象和非完全面向对象的最大区别在于中间件层,什么
是中间件层,中间件层是问题域中具体对象,商业规则,更高层次上的问题实体。完全面向
对象有中间件层,非完全面向对象没有中间件层。RAD工具开发时常会忽视中间件层。

举个例子:一个简单的记帐系统如果用RAD工具(如:Delphi)开发,用户接口层是一些
系统中所用的控件,如按钮,菜单等,这些控件对象由Delphi或其它开发工具中的类库封装
;数据库层则是在数据库系统(如SQL SERVER、Oracle等)建立的表,如顾客表、产品表、
订单表等。

完全面向对象设计中的中间件层为问题域的交易,商业规则等对象,以及更高层次上
的问题实体,如顾客,产品,订单。非完全面向对象则没有中间件层这些对象。换句话说,
非完全面向对象是基于用户接口层直接存取数据库层,即:基于控件。完全面向对象是将
中间件层封装数据库层,用户接口层使用中间件层,这样将数据库层完全透明,做到数据
与界面的分离,即:面向对象,基于控件。

在一些小型的系统中,非完全面向对象设计可以加快开发速度,但系统的灵活性,重用
性不好,大型系统必须采用完全面向对象的开发方法,否则,由于商业规则没有放在一起,
软件后期将不可控制,一个成功的可复用的商业软件应该是由中间件层的众多对象组件灵活
搭配而成。

由此,笔者提出一种数据库完全面向对象的设计模式。

1.2数据库的一种完全面向对象设计模式

用完全面向对象的方法做一般的应用程序比数据库程序容易一些,因为它不涉及到数据
库存取。但在做数据库程序时应该考虑类跟跟数据库是怎样联系的。

步骤1:分析问题域,找出所有问题域中相关事物,从中抽象出对象。

步骤2:从抽象出的对象中找出所有的持久对象(Persisitant Object),所谓持久对象就
是由数据库管理系统负责管理的,可以永久保留,将来可被提取的对象,将这些持久对象
以“一类一表格”的原则映射到数据库中,通过数据库管理系统建立表格。

步骤3:定义持久对象,所有持久对象均采用“双构造函数”的方法在程序中进行构造,其中
,一个构造函数参数为所有初始化该持久对象的值,封装数据模块中“存”操作,另一个构
造函数的参数为唯一标识该持久对象的值,封装数据模块中“取”操作。其它数据库操作由
持久对象相应方法封装。

步骤4:所有与数据库层发生交互的动作,均放在专门的数据模块中,由中间件层持久对象相
应方法封装,做到数据与界面的分离。

步骤5:定义其它非持久对象。具体软件功能由相应对象协同实现。

该设计模式的重点在于持久对象的定义,除了双构造外,持久对象如果一次获取数据
数量>1,那么可以定义“持久对象集”对象,“持久对象集”对象由持久对象组成,“持久
对象集”对象中的对象集可由数据模块中相应的SQL语言筛选,如果数据集中数据数量非常大,
那么在数据模块中相应的SQL语言可以以固定数量进行筛选数据集,分批筛选。对象集中的相
应持久对象可用链表的结构进行链接。

以这种方式定义的持久对象,完全封装了数据库存取,用户在使用持久对象的时侯
甚至感觉不到数据库的存在,因为相应的数据库操作已被持久对象的相应方法封装,用户
只需要建立相应的持久对象即可进行数据库的操作。
 
如果写出一个实例供参考就好了
 
有实例吗?
 
正在写实例,准备过几天帖上来
 
持久对象的定义和操作如果完全由自己实现,不容易。
不如参考一下EJB的实体BEAN,思路基本一致
 
关注此题,热切期盼。
 
2 数据库完全面向对象设计模式在银行储蓄管理系统中的实现
2.1问题域对象、持久对象与数据库表

根据上述设计模式的思想,我们首先找出问题域的所有对象
1. 帐户对象
2. 储蓄帐户对象
3. 定期储蓄帐户对象
4. 活期储蓄帐户对象
5. 银行卡帐户对象
其中储蓄帐户对象、银行卡帐户对象继承自帐户对象,帐户对象为一个虚基类,定期储蓄帐
户对象、活期储蓄帐户对象继承自储蓄帐户,储蓄帐户对象为一个虚基类
UML类层次图(图12)
图12:帐户对象UML类层次图

6. 储户对象
7. 柜员对象
8. 交易对象
9. 费用对象
10. 利率对象
11. 特殊操作对象
12. 系统信息对象
13. 银行功能对象
14. 银行服务对象
15. 储蓄服务对象
16. 银行卡服务对象
其中储蓄服务对象,银行卡服务对象继承自银行服务对象,银行服务对象是一个虚基类
UML类层次图(图13)

图13:服务对象类层次图


然后,我们分析问题域,将要持久存储在数据库的数据对象确立为持久对象。
所确立的持久对象为:
1. 储蓄帐户对象
2. 定期储蓄帐户对象
3. 活期储蓄帐户对象
4. 银行卡帐户对象
5. 储户对象
6. 柜员对象
7. 交易对象
8. 费用对象
9. 利率对象
10. 特殊操作对象

这些持久对象将以“一类一表格”的原则映射到我们选择的数据库SQL SERVER2000中,




2.2 持久对象的定义

(1) 双构造函数的使用。


限于篇幅,本文以持久对象——储户对象为例,说明持久对象的双构造函数方法,
其它持久对象的定义思想与之大致相同。

储户对象接口定义
type
TCustomer = class(TObject)
private
{ Private declarations }
protected
{ Protected declarations }
Cus_id:string;
Cus_name:string;
Cus_shenfenid:string;
Cus_addr:string;
Cus_phone:string;
procedure New_Cus_Info(c_name,c_sfid,c_addr,c_phone:string);
procedure Load_Cus_info(cusid:string);
public
{ Public declarations }
constructor create(c_name,c_sfid,c_addr,c_phone:string);overload; //(1)
constructor Create(cusid:string);overload; //(2)

function update_cus():boolean;
procedure Set_Cus_Id(C_Id:string);
procedure Set_Cus_name(C_name:string);
procedure Set_Cus_shenfenid(sfid:string);
procedure Set_Cus_addr(addr:string);
procedure Set_Cus_phone(phone:string);
function Get_Cus_Id:string;
function Get_Cus_name:string;
function Get_Cus_shenfenid:string;
function Get_Cus_addr:string;
function Get_Cus_phone:string;
published
{ Published declarations }
end;


根据数据库完全面向对象设计模式,持久对象将由两个构造函数(见(1)、(2)),
其中第一个构造函数参数为所有初始化该持久对象的值,封装数据模块中“存”操作,
constructor TCustomer.create(c_name,c_sfid,c_addr,c_phone:string);
begin
New_Cus_Info(c_name,c_sfid,c_addr,c_phone);
end;
procedure TCustomer.New_Cus_Info(c_name,c_sfid,c_addr,c_phone:string);
begin
Cus_name:=c_name; //(1)
Cus_shenfenid:=c_sfid; //(2)
Cus_addr:=c_addr; //(3)
Cus_phone:=c_phone; //(4)
dmodule.New_Cus_Info(self);
end;
这里,self指针代表持久对象自身,由于(1)—(4)已经对持久对象赋值,所以只需
将self代入数据模块上对数据库进行新增操作的方法上。

另一个构造函数(2)的参数为唯一标识该持久对象的值,封装数据模块中“取”操作。
constructor TCustomer.create(cusid: string);
begin
Load_cus_info(cusid);
end;
procedure TCustomer.Load_Cus_info(cusid: string);
begin
Cus_id:=cusid;
dmodule.load_cus_info(self);
end;
同样self代表将取得的数据持久对象,在取得该对象所有值之前,只知道标识该对象
的标识值(如储户对象中储户的编号),而数据模块上对数据库进行取操作的函数(本例中
为dmodule.load_cus_info(self);)所执行的功能就是对所代入的持久对象(此时只有该对
象的标识值被赋值)的其他属性值进行赋值。

procedure TDModule.Load_cus_info(var CUS: TCustomer);
begin
with DCUSquery do
begin
close;
sql.Clear;
sql.Add('SELECT * from 储户信息表'); // (a)
sql.add('WHERE 储户ID='+''''+CUS.Get_Cus_Id+''''); // (b)
open;
if not fieldbyname('储户ID').isnull then
begin
cus.Set_Cus_name(trim(fieldbyname('储户姓名').asstring)); //(c)
cus.Set_Cus_shenfenid(trim(fieldbyname('储户身份证号').asstring)); //(d)
cus.Set_Cus_addr(trim(fieldbyname('储户地址').asstring)); //(e)
cus.Set_Cus_phone(trim(fieldbyname('储户电话').asstring)); //(f)
end;
close;
end;
end;

从数据库取得数据是通过写SQL语言 (a)、(b) 实现,将SQL返回的数据赋给持久对象的操作
为(c)-(f),这样,函数执行完毕,持久对象将拥有从数据库查询所取得的值。

将对数据库的操作封装在数据模块中,有利于数据与界面的分离,数据模块可以作为
三层数据库中的应用服务层,使系统可以轻易转变为三层分布式数据库。同时,存取数据库
变得透明,用户甚至不知道数据库的存在。


如本例中,新存一个储户只需调用:
var newcus:Tcustomer;
newcus:=Tcustomer.Create(‘比尔*盖茨’,’100001’,’美国微软公司’,’(025)110’);

取得储户比尔*盖茨(比如储户ID为10001)的操作为
var newcus:Tcustomer;
newcus:=Tcustomer.Create(‘10001’);

这些操作的背后已完成了对数据库的相应操作,数据存取变得透明。而且程序的可读性很强。
然后这个储户的所有数据就可以读取这个持久对象来实现,如我们想知道这个储户的地址,我们
就可以这样操作:newcus.Get_cus_addr;

因为操作对象是中间件层对象,将使程序可读性非常强。

(2)“持久对象集”对象的定义
在上述持久对象的定义中,由于只是取得一条记录,所以没有用“持久对象集”的概念,而
通常应用中,经常会一次性取得后台数据库的多条记录到前台处理,虽然也可以像上述定义
持久对象那样一条一条记录进行存取,但这样每新读一条记录都要从头开始进行一次SQL数据
库的操作,造成资源开销太大,效率降低,所以如果需要一次读取多条记录,就需要建立一个
“持久对象集”对象。

以银行卡交易对象为例
一个储户通常会与银行发生多次交易,当我们需要取得该储户银行卡帐号的交易情况时,
就需要一次性取得该帐号所发生的所有交易对象记录,这时候取得的数据集不是单个的持久对象,
而是一个持久对象的集合,这个集合里有所有该帐号发生的交易对象。所以在这里就需要定义
“持久对象集”对象,“持久对象集”对象也是一个对象(Object),只是它的操作元素为持久对象。
在本例中,定义交易对象的持久对象集为交易集——Transacts,其声明如下:

TTranode=^TTransactnode;
TTransactnode=record
TRainfo:TTransactinfo;
Pnext:TTranode;
end;
TTRansacts = class(TObject)
private
{ Private declarations }
Pfirst:TTranode;
Pend:TTranode;
pos:TTranode;
protected
{ Protected declarations }
Transactnum:integer;
Id_ofaccount:string;
id_type:integer;
public
{ Public declarations }
function Get_TRas_acid:string;
function Get_TRas_actype:integer;
function Get_firsttrainfo:TTransactinfo;
function Get_Transactnum:integer;
function Get_lasttrainfo:TTransactinfo;
function Get_traninfo(tra_id:string):TTRansactinfo;
function Get_nexttraninfo():TTRansactinfo;
procedure set_first;
procedure TRas_add(TRa_info: TTransactinfo);
constructor create(acid:string);
end;
“持久对象集”对象—交易集Transacts 采用了链表结构链接集合中的所有交易对象
Ttransactinfo,其构造函数为链表初始化及集合元素的确定、添加过程。


constructor TTRansacts.create(acid:string);
begin
TRansactnum:=0;
Id_ofaccount:=acid;
Pfirst:=Nil;
pEND:=Nil;
dmodule.load_TRAs_info(self);
end;
数据模块中load_TRAs_info方法以交易集对象Transacts为参数,通过SQL语言筛选出
所需要的交易对象,加入交易集对象的链表

链表添加的操作为:
procedure TTRansacts.TRas_add(TRa_info: TTransactinfo);
var newnode:TTranode;
begin
new(newnode);
newnode.TRainfo:=TRa_info;
newnode.Pnext:=NIL;
if Pfirst=NIL then
begin
pfirst:=newnode;
pos:=pfirst;
end
else
begin
pend^.pnext:=newnode;
end;
pend:=newnode;
pend^.pnext:=NIL;
Transactnum:=Transactnum+1;
end;
只需要声明一个交易对象Transactinfo,就可以将该对象加入交易集对象Transacts的链表中。
这里先要介绍交易对象的定义方法,交易对象采用上述双构造函数方法进行对数据库单
个交易记录的封装,
其中完成“取”操作的构造函数定义如下:
constructor TTransactinfo.Create(of_acid: string);
begin
TRa_ACid:=of_acid; //of_acid交易对象对应的帐户ID,交易对象的标识值
dmodule.load_TRA_info(self)
end;
这里,dmodule.load_TRA_info(self),和交易集对象中dmodule.load_TRAs_info(self)
是不同的,前者封装了单个交易对象的数据库“取”操作,后者封装了交易集对象
的数据库“取”操作。

procedure TDModule.Load_Tra_info(var TRA:Ttransactinfo);
begin
with Dtinfoquery do
begin
TRa.SET_TRa_id(fieldbyname('储蓄帐户交易明细ID').asstring);
TRa.SET_TRa_ITid(trim(fieldbyname('柜员终端编号').asstring));
TRa.SET_TRa_Gyid(trim(fieldbyname('操作柜员编号').asstring));
TRa.SET_TRa_type(fieldbyname('交易类型').asinteger);
TRa.Set_tra_money(fieldbyname('交易金额').asfloat);
TRa.Set_tra_time(fieldbyname('交易时间').asdatetime);
TRa.SET_tra_result(fieldbyname('交易结果').asinteger);
TRa.SET_TRa_wdname(trim(fieldbyname('网点名称').asstring));
end;
end;
procedure TDModule.Load_TRas_info(var TRas: TTransacts);
begin
with Dtinfoquery do
begin
close;
sql.clear;
sql.add('SELECT * FROM 储蓄帐户交易明细表'); //(a)
sql.add('WHERE 储蓄帐户ID='+''''+TRas.Get_TRas_acid+''''); //(b)
open;
if not fieldbyname('储蓄帐户ID').isnull then
begin
while not eof do
begin
TRas.TRas_add(TTransactinfo.create(TRas.Get_TRas_acid,TRas.Get_TRas_actype));
//(c)
next;
end;
end;
close;
end;
end;
其中(a)、(b)为SQL语句筛选出所有指定帐号的交易记录集,(c)是该函数的重点,
先声明一个交易对象(调用交易对象“取”构造函数,通过dmodule.load_TRA_info(self)
完成对当前交易对象数据库取操作),然后通过TRas.TRas_add,将取得的当前交易对象加
入到交易集对象的链表中。


通过持久对象集对象,可以封装多个持久对象的数据库存取操作,在本例中,如果要取得帐
号为“1000000001”的帐户所有交易情况,那么只需调用操作
var Tras:TTransacts
Tras:=TTransacts.Create(‘1000000001’) 即可,所有的该帐号的交易情况已加入Tras的
链表中,并且链表头指针可通过Tras.Get_firsttrainfo得到,所有对链表的操作插入删除都
可以实现。如果持久对象集的集合元素数目巨大,那么可在数据模块中SQL语言筛选的时候加上
限制语句,以固定数量分批筛选。


因时间有限,实例叙述不是那么详尽,见谅!
 
好像只是面向对象的程序呀,不是面向对象的数据库吧。
 
呵呵,难为你写的这么长了。我泼泼冷水:)

1,话题要明确,你讲的应该是数据库程序的面向对象设计,而不是数据库的面向对象吧?:)
2,在你的所谓的持久对象的定义中,应该把所有的数据库字段都作为该对象的一个属性,这样
更符合“完全面向对象”。
3,在这种设计思路下,界面怎么办?比如数据感知控件怎么和你的对象连接?还是不用数据感
知控件?可能在你的银行系统中这个界面问题比较次要:)
4,本质上,你只是在界面和borland的数据存取体系之间多加了一层“对象”这个东西,多封装
了一下,但这样做的后果是,增加了大量不必要的代码(包括对象封装的代码、对界面控件
赋值的代码等等):)。

再给你一点小建议:
1,如果对象不能从borland的数据体系中完全独立出来,不妨把数据字段也就是对象的属性仍然
交给borland的数据体系来处理。这样做将使代码简单很多,又不失面向对象的设计思路:)
也就是说:不要太强调所谓的“完全”面向对象。
2,如果要真正的完全面向对象,可以参考objectsight的体系,在这套控件中已经实现了你的完
全面向对象的思路和体系。请参考http://www.objectsight.com。如果把这套控件的体系研究
完,你会知道什么是真正的完全面向对象的数据库设计模式:)
 
很感谢京大侠给我的批评指导
的确,我的话题不是很明确,应该是数据库程序的面向对象设计,呵呵
文章中,“一类一表格”的原则就是要把持久对象的属性映射到数据库中,所以
数据库每一个字段都是该对象的相应的属性。
以前看过大富翁上京大侠对于数据感知控件的看法,
http://www.delphibbs.com/delphibbs/dispq.asp?lid=709679
我的这种设计思路就是不用数据感知控件,
由相应界面调用对象的相应方法,进行赋值,编辑
关于数据感知控件的使用仁者见仁智者见智,但趋向是不用的,
不用数据感知控件的确增大了开发负担,但灵活性复用性提高了

总的来说,我的设计思路是提供了了一种对象的封装方法,
的确,不用业务对象的封装是可以提高开发效率,我在文章中也提到了,但弊端也是明显的,
”在一些小型的系统中,非完全面向对象设计可以加快开发速度,但系统的灵活性,重用
性不好,大型系统必须采用完全面向对象的开发方法,否则,由于商业规则没有放在一起,
软件后期将不可控制,一个成功的可复用的商业软件应该是由中间件层的众多对象组件灵活
搭配而成。“


objectsight的设计思路我正在看,希望能从中收益,
谢谢京大侠中肯的意见!


欢迎各位继续讨论!!!!

 
我觉得 yyhhnn 做了非常有益的尝试,真的非常好!Delphi 程序员被数据感知控件所限,
玩玩简单的 C/S 还行,却做不了正真 Enterprise 级的东西。
yyhhnn 试图突破它,因而得已进入一个更加广阔的 OO 世界。

yyhhnn,您目前的设计还是很初步,我建议继续深入。
下面这个贴子里有一个非常有趣的例子:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=946305
不过它和您的设计一样偏重于类继承,而现在的 Design Patterns 提倡面向接口编程。

Design Patterns 好处多多。
通过 Factory Pattern,SQL 可以完全从业务对象中分离出来。
通过 Abstract Factory Pattern,您的对象可以存入不同的载体,比如 XML 文件。
......
看看 J2EE 中的 Pattern 吧。http://java.sun.com/blueprints
 
J2EE在完全面向对象方面体现的更表层一些。
 
DELPHI对接口支持的太差,
 
看过孙老师提到的帖子了。yyhhnn可以借鉴一下。那位仁兄的做法不排斥
borland的数据体系,还可以继续使用数据感应控件。总的思路上和我们
现在的实现做法差不多,我们已经使用这种思路实现了一个开发体系,并
应用到了实际的项目中。我的意见是,不要太强调所谓的“完全”面向对
象,业务逻辑的封装是面向对象的设计思路问题,但并不一定绝对与数据
感知控件相冲突。思路正确的话完全可以实现两者的合理融合并用。当然,
这样做的结果,必将牺牲一些面向对象的特性。在我的实际应用中,基本
上放弃了业务类的属性(也就是数据字段),我把所有的属性用一个dataset
来表达(最合理的设计思路应该是实现一个datarecord类,然后用它来表
达属性,不过因为时间的原因我们没有这样做)。这样在业务逻辑上仍然
是面向对象的,而且因为这个属性dataset的存在,我仍然可以使用数据感
应控件来做界面:)同时,我的dataset使用的是clientdataset,这样,
后端的数据存取方法不会与特定的数据存取体系(比如BDE)捆绑起来,可
以很方便的改变数据驱动。

在objectsight的体系中,他封装了一整套设计方法,从record类到record
集合,包括与界面部分的整合,应该说在设计上实现了完全的面向对象。在
基于类继承的设计思路上,他的体系可能已经是最完全的面向对象了。:)

 
后退
顶部