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

时间:2021-08-13 03:59:33

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语言可以以固定数量进行筛选数据集,分批筛选。对象集中的相应持久对象可用链表的结构进行链接。
以这种方式定义的持久对象,完全封装了数据库存取,用户在使用持久对象的时侯甚至感觉不到数据库的存在,因为相应的数据库操作已被持久对象的相应方法封装,用户只需要建立相应的持久对象即可进行数据库的操作。

 

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;                 file://(1)
     Cus_shenfenid:=c_sfid;               file://(2)
     Cus_addr:=c_addr;                  file://(3)
     Cus_phone:=c_phone;                file://(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语言筛选的时候加上限制语句,以固定数量分批筛选。