delphi面向对象及数据库(300分)

  • 主题发起人 活化石
  • 开始时间
D

dache

Unregistered / Unconfirmed
GUEST, unregistred user!
太高声了,我现在只能作些简单的应用。
我考虑的主要是系统设计和应用方面的东西。先混口饭吃。
 
L

lqy

Unregistered / Unconfirmed
GUEST, unregistred user!
看不到 TmyDBClass TmyISSimpleObject 的定義,無法深入了解
看你的調用方式,
TtGoods.iInit 應該是在中間層初始化一個DataSet,定好表名和字段,
TtImeObj.GetData 的實質應該是生成一條 SQL 語句
看你的目的好像是儘量避免修改中間層,而由客戶端動態生成中間層的Dataset,這樣好像
丟失了中間層管控業務流程的作用
if iImeActive(e2,e2.Field.asString,v) then
begin
e2.Field.DataSet.Edit;
e2.Field.asString := v.iOut[0].Fields[1].AsString;
e2.Field.DataSet.Post;
e2.SelectAll;
end;

這種修改數據的方式,更看不到任何業務對象的痕跡,除非你在 OnafterPost
有相關的操作,但看你的目的是不動中間層,應該不會有這方面的操作

分析結論;封裝數據庫連接,和DataSet的建立和字段建立方法,在可預期的情況下可以
簡化一定的操作,並另代碼相對簡單明瞭,但在不可預期的情況下,你可能要修改基類來
適應.
初初看去好想彈性很大,但用下去會發現彈性其實很小.通過方法來生成SQL語句
的方法是最不可取,因為SQL語句千變萬化,一個方法是不可能完全實現,強行適應的話
這個方法的參數和越來越多,還不如直接傳一條SQL語句過去方便實際

 
L

lqy

Unregistered / Unconfirmed
GUEST, unregistred user!
選擇了Delphi作為開發工具,就請接受Delphi的開發模式,強行OO化開發只能是吃力
不討好,Borland在這幾代Delphi升級中都沒有改變這個開發模式,說明這種開發模式還是
有可取的地方.
有句話說的好,不要在一棵樹上吊死 如果你真的看不起這種開發模式,那麼最好選擇更合適開發工具,無謂在Delphi上苦苦折騰
拋開 Dataset->DataSource->DBGrid,全手動來做不是明智的選擇,因為有代碼量和DeBug的問題, Dataset->DataSource->DBGrid 經過多少高手Debug和改良,這些工作壓到你頭上的話,你的項目肯定會完蛋.
一個判斷方法:如果用你的方法寫出來代碼比用Delphi方法寫出來代碼多很多的話,請馬上停止,不然你會很慘很慘

讓我們都站在巨人的肩膀上.................................

 
B

barton

Unregistered / Unconfirmed
GUEST, unregistred user!
不喜欢这种悲观的论调,让你比较遗憾的是:不仅项目没有完蛋,反而运行得很好。如果
Dataset-DataLink-DBGrid真的那么好用,那Bold这样的产品还有市场吗?
 
D

delp

Unregistered / Unconfirmed
GUEST, unregistred user!
to lqy
您(这里不能不用您字来表示我的敬佩)分析的真是一针见血,这也是我设计这个结构的最大困难,各种SQL的组合越来越多,但是因为我是从无SQL的时代过来的程序员,这个问题出在我设计这个结构的早期,当时我一直陷入SQL的困惑中,但是经过思索,我重新考虑了我的业务需求,发现我的数据通信量不大,但是维护量大,要求系统能快速升级,但是如果使用了SQL,那么业务层将与数据层牢固的捆绑在一起,造证中间层与数据层的模糊不清,这正是程序的结症,虽然我的程序在性能上损失极大,但是我换回来了对象的高效开发过程,所以从业务层以后,我开始彻底抛弃了SQL,现在前途一片光明.....(至少现在感觉)
原来我一直不喜欢贴源代码,因为我认为不会有人认真看,谢谢.....
程序简单说明
TmyDBClass;
数据层
TmyISSimpleObject;
业务层
是最核心的两个类,下面俩个是基础
TmyDBInterface;
数据接口
TmyISSession;
业务会话
实际上 TmyISSimpleObject 就是业务对象,呵呵

 
T

TOTO

Unregistered / Unconfirmed
GUEST, unregistred user!
正在做一个系统,是在linux下面使用Kylix完成CGI。功能是和学校业务相关的。其中,我负责了技术方面的设计。
简单说说我们的解决方法,请 指点一二...
还要说明:一些业务层,业务对象层等概念我并不是十分的清楚。下面虽然也会提到这些概念,但实际的所指可能不同甚至是
错误请大家注意,不要受我的误导.不然我的罪过就大了 :)
请大家随我一起经历一次我们的项目过程吧:.....

我们同样规定:
1, 任何人不能直接访问数据库,所有对数据库的访问【执行SQL语句】必须通过组建完成
2, 要求技能型面向对象的封装。考虑到不同的员工对OOD思想的理解深浅不一,所以我们规定只要做到实体和控制相分离就可以。
但在项目进行中就变成了只要求公用组建符合这项规定就行了。
我们的面向对象设计很快的完成了。看起来像这样:
tStudentManager = calss
addstudent(name : String;
age : Int......);
delstudent(name : String);
overload;
delstudent() overload;
.....
end;

tstudent = class
name : String
age : int;
function getnamebyID(ID : Int) : String;
...
end;

刘润东的那本UML对象设计与编程【向大家推荐这本书,虽然有些名词写的不太标准,但哟些咚咚很是实用】
还有那本版更经典的著作《名字忘了 hehe :(》。都有一些这方面的论述。
书中讲到:实体的创建、销毁都要由控制类来实现。所以所有使用这个控制类的程序如果要创建一个学生实体,都必须
把学生所有的属性值当作参数传给控制类,然后控制类负责创建学生实体。
第一个问题就来了:学生的属性实在太多,有40个左右。如果把40个属性都当作参数传递过去......?????
为了解决这个问题,我们又为学生创建了一个record 里面又学生所有的属性。传递参数时,把这个record的指针传过去。
这个想法时参照JAVA的Value Object来得,也不知道我理解的Value Object对不对?
这样控制类变成了:
tStudentManager = calss
addstudent(&studentrecord);
getstudentbyid(id : Integer) : TStudent;
.....
end;
这样程序必须先创建一个学生实体,必须先生命一个学生的record。
虽然解决了参数太多,但是又来了一个疑问:我有了学生实体,为什么还要创建这个record?有必要吗?
解决了这个问题,项目得以继续。
很快,我们的公用组建部分开发完成。主要有
session,log, SQLDriver
//因为我们只能使用Apache,不能使用JAVA的东西。而kylix提供的session处理不能满足我们的要求,
//只好自己做了:( 羡慕死做JAVA的了
SQLDriver主要负责执行SQL语句,并返回一个结果集【dataset】
大家开始完成业务模块。这时开始感觉到大家的水平参差不齐,导致业务模块之间的衔接有一些隐含问题。为了保险
我们决定在业务模块不再使用面向对象的开发技术。而是大家最熟悉的直接完成CGI。但规定:所有的数据库访问必须铜鼓SQLDriver
。已经又公用组件的必须使用公用组建;不允许访问别人的数据表。
自然,学生时最长用的实体对象。这是第二个问题来了:
由于学生的属性众多,对他的查询方式也就最多。各种各样的查询很快提出来了。
开始我们没有意识到这个问题,只是简单的规定:负责公用组件的人负责为大家的每一中查询要求完成一个函数接口。
但随着越来越多的查询方式【后来更多的是其他的修改, 各种条件的删除】。我们的学生控制类必须要实现数百个函数了。
负责学生组建的人差点从二楼跳下去.....hehe
如果允许直接构造SQL语句访问数据表,那么我们前面做的工作就白做了:(
只好想别的方法解决。
找来找去,Delphi中没有发现十分好的解决方案【或许是我不知道】
又去JAVA中找。发现了O/R Mapping。不过不太好实现。而且我们对这个O/R Mapping理解也不深。
所以我们根据思路【实际上是根据别人实现的一段代码】做了一些设计修改。
TSQLManager = calss
addtiaojian(recordname, option, value : String);
getSQL : String;
end;

TStudentSQLManager = calss (TSQLManager )
//主要是载创建类的时候把某一个实体类和数据表重的关系映射添加进来
end;

tStudentManager = calss
private
SQLManager : TStudentSQLManager ;
public
addtiaojian(guanxi, recordname, option, value : String);
addstudent(&studentrecord);
getstudentbyid(id : Integer) : TStudent;
.....
end;

---------- -------------------- ---------------
| | | | | |
|record |-----|StudentSQLManager |----|Data table |
| | | | | |
|--------| |------------------| |-------------|
^
|
|
-----------------
| |
|StudentManager |
| |
| |
| |
|---------------|

上面,StudentManager使用SQLManager来处理生成SQL语句。事实上,StudentSQLManager知道record 和具体数据表字段的对应
关系。属于StudentManager的私有类。没有违反我们前面说得规定。

前面在解决第一个问题时,我们为学生创建了一个record。里面又学生所有的属性。实际上,这些属性肯定和数据表中的
某一个字段相关联。而这个关联关系就由SQLManager来维护。
SQLManager是一个抽象出来的类。主要实现的功能是根据条件生成相应的SQL语句。内部有一个record和fields的对应关系表。
外面的程序使用addtiaojian方法来添加SQL语句的where字句部分,当然也有其他的函数来添加 select部分,Order by部分。
外面的程序都是使用record中的名字来访问的。
比如record中有这样的定义:
studentrecord = record
name, address : String;
end;

数据表中这样存储
field_name varchar[40] not null default ''
field_address varchar[100] default ''
在 TStudentSQLManager 的构造函数中,添加上映射关系。然后业务模块的程序就可以这样使用了:
studentmanager.addtiaojian('and', 'name', '=', '张三');
studentmanager.addtiaojian('and', 'address', 'like', '%海淀%');
studentmanager.search();
就可以得到一个对象数组了。
在调用search时,studentmanager调用getSQL方法,根据条件列表生成SQL语句:
select * from tbl_student where field_name = '张三' and address like '%海淀%';
这样,业务模块仍然是不知道数据表结构,只知道record内容。而后台实际上可以根据需要把一个实体对应到多个数据表中;
因为这样做会增加不少的代码量,为了保证进度,我们规定:只有实体属性较多【5个以上,含5个】才需要继承TSQLManager添加
对应关系,在控制类中构造SQL语句。其他的只要求作出getNamebyID之类的接口就行了。
看了诸位的方法,感觉“人在昆明”的方法和我们用的很相似。不过在具体问题时,我们有一些不同,但思想是一致的。
我们之所以把实体和控制相分离最初是为了一个很简单的目的:不要直接访问数据表【最好能够做到数据表的无关性】。
更不要去访问别人的数据表。所以我们把数据表封装成一些控制类和实体。所有对实体的访问都必须通过控制类来完成。
类似把业务模块堪称显示层, 我们实现了一个MVC模式。
这样,Delphi的那些数据感知控件就用不上了。:( 好在我们是在linux下做CGI,不需要那些东东。如果是一个CS结构的程序,
我们八成是要用了。因为这样的开发效率高。而且对程序员的要求也不太高。只要能够保证开发效率和质量。是不是采用面向对象
并不是很重要。我们之所以定下如此的规定是因为我们在前面项目中的教训。同时我们也是为了以后开发效率和质量的提高,建立
一个寒夜那的开发模型。hehe 现在看这个目标可太大了!教各位耻笑了。
不过也是一条教训:在实施面向对象的开发方法时,不要一步迈的太大。
我得理论知识不足,希望没有犯什么概念错误。乱七八糟说了这么多,供大家拍砖。如果能够引起更多的讨论,那是最好。
要不大家指点以下我们的方案中不足之处,我就更感激不尽了...

 
D

delp

Unregistered / Unconfirmed
GUEST, unregistred user!
to TOTO
你的对象数据挺有意思
 
B

barton

Unregistered / Unconfirmed
GUEST, unregistred user!
to toto:恭喜你在oo上面走出可喜的第一步。
我感觉你的很多原则订得非常到位,只是后来因为各种原因而一步步降级而没有达到预期
的效果。我想指出几点,不对的地方就算我没说。
1.OOD不够充分。在完成对象建模后,没有制定一份明细的对象清单。你是总设计,显然对
模块的划分,模块之间可能的协作关系你应该了如指掌。你先安排好每个人做自己分工的
那一块的对象设计,然后将每个人的对象以及对其它组的协作要求提交一份完整的清单,
由你来综合,并分发到各组。这样才算完成了对象设计。设计完成后,在实施过程中可以
作一些适当的修改,但改动不能太大。你后期发现查询部分变动很大显然违背了这个原则。
2.你的对象封装过于简陋,没有发挥OOP的优势来。感觉你这种封装和用记录来处理没啥两
样。至少你对数据库访问部分的封装非常多余。看看我对你项目的分析(我做过学校及教育
系统的管理系统):(画得不规范,毕竟是文本方式)
A.业务对象管理者 (负责业务对象的聚合/枚举/建立/撤销/变更/与D的耦合)
======================================================================
^ ^
| | ..........
B.学生对象管理者 C.教职工对象管理者
================ ==================
D.数据管理者 (负责对数据库的所有操作)
========================================
^ ^
| | ..........
E.学生数据管理者 F.教职工数据管理者
================ ==================
G.业务对象 (负责管理业务对象属性及操作)
==========================================
^ ^
| | ..........
H.学生对象 I.教职工对象
========== ============
在顶层,所有的操作已经被你抽象,你每定义一个操作,就在子类中实现它。A、D、G三
者互相的属性及操作是开放的。但封装隐藏了子类的实现细节。这其中,我的看法DEF只
需要提供无返回的操作就够了。例如在列举学生的时候,向E提供一个参数表,E每次枚举
一个学生来就调用一次A的构建方法(因为A与D是耦合的,这种调用是合法的)。在删除学
生的时候,将要删除学生的ID号提供给E,E去做删除操作。如果你的模型足够合理,你不
必担心会因为后期条件变化而放弃模型。
3.关于参数传递。每个对象肯定有一个长长的参数表,否则还封装它干什么。传递参数时
Delphi肯定不同于Java。我给你出几个主意,定义结构也不是不好,有点多余。一是使用
常量数组。例如你定义:
function CreateBizObject(const Params: array of const): TBizObject;
override;
实现的时候先读参数个数,然后依次写入学生对象的成员变量中。调用的时候,你可以在
遍历数据集的时候直接写上:
Manager.CreateBizObject([Fields[0].AsInteger, Fields[1].AsString, ...]);
这种方式的缺陷很多,不容易抽象出来。
第二种是虚流。假定你定义了一个公开的对象,从该对象可以很轻松地依次取出数据来。
这样,无论是Get数据还是Put数据都可以利用这个操作,代码复用更好。虚流这个名字可
能取得不是很好,但真的非常好用,传递数据代码量极少。但关于虚流的操作可能需要相
当的篇幅才能介绍清楚。第三种是实流,例如XML。其实TFields本身也是一种实流(如果
你通过数据集访问数据库的话,我不怎么用这个东西的)。
4.关于查询。查询本身是一个数据库概念,在oop中你应该换一个概念。可是我发现你不仅
没换,反而将查询搞得和数据库紧紧相连。
studentmanager.addtiaojian('and', 'name', '=', '张三');
studentmanager.addtiaojian('and', 'address', 'like', '%海淀%');
studentmanager.search();
你能说你这样的形式不完全就是在构造SQL语句吗?你完全可以更大胆一些。我告诉你我的
一些方法。如果要实现按姓名查找,我就约定一个Index为1的查询,然后定义Index为1的
查询是一个基于字符串的查询。如果按年龄查询,我就约定一个Index为2的查询,然后定义
Index为2的查询是一个基于整数的查询。每一种不同基的查询我就建立一个类,这个类来判
断遍历的对象或者数据库行是否符合要求。如果是复合查询就将多个类聚合在一起。当然,
每个类提供有自己的解析操作,对应成与对象的比较或者生成数据查询语句。例如一个查询
包括两个查找类,一个是{Index=1;Type=Str;Key='张*';},另一个查找类是{Index=2;
Type=Int;Key='10,12'}共同构成一个查找者,表示:查询姓“张”且年龄为10或12岁的学
生。姓名、学生包含在Index中,Type=Str事实上只是说明是字符串查找类,年龄/学生也包
含在Index中,Type=Int也只是说明为整型查找类,关键字一律以串方式提供,'10,12'翻译
成SQL语句表示:Age=10 or Age=12。而'10-12'则表示Age>=10 and Age<=12。怎么约定完
全根据你的业务要求来。
 

特尔斐

Unregistered / Unconfirmed
GUEST, unregistred user!
受益非浅!衷心感谢barton等大侠!
 
W

wfzha

Unregistered / Unconfirmed
GUEST, unregistred user!
to TOTO
>>我们的学生控制类必须要实现数百个函数了。
>>负责学生组建的人差点从二楼跳下去....
我觉得可能是你的对象划分有问题,是不是应该拆分成多个类?
 
Y

yyanghhong

Unregistered / Unconfirmed
GUEST, unregistred user!
用record或Object做O/R Mapping不是太灵活, 表结构变化后必需修改代码.
可以array或collection好一点,出我觉得动态RTTI是发展方向, delphi的动态RTTI太简单, c#好一些.
 

畅雨

Unregistered / Unconfirmed
GUEST, unregistred user!
可以这样作 UI--BuinessObject--dataset(ADO.net)--database。
在BuinessObject与dataset之间作对象持久化关系对应,使用provider。
从object继承BuinessObject作为业务逻辑对象的通用父类,通过provider连接到dataset(Ado.net),provider利用.net的元类技术将BuinessObject的可持久化属性转化到dataset的表和列。表数据库的建立由类设计器自动实现(有点类似instantobject for delphi,不同点是充分利用.net的元类,而不是delphi的简单的RTTI)。
 
Y

yyanghhong

Unregistered / Unconfirmed
GUEST, unregistred user!
畅雨,
net的元类, 能详细说一下吗
 
B

barton

Unregistered / Unconfirmed
GUEST, unregistred user!
to yyanghhong:
>>用record或Object做O/R Mapping不是太灵活, 表结构变化后必需修改代码.
你的项目经常变化表结构吗?你如何在运行时解决DDL与DML间的冲突?而且,从前的SQL
语句也只能变动了。我认为数据库设计有误才需要经常变化表结构。正象举过的一个例
子,象工资项目经常变动这样的模型你非要用一个平面的方式来表达它,当然避免不了
变动结构。zzsczz说的好,正是因为平面数据库表达不了才需要O/R Mapping的,是吗?
其实表结构变化了,意味着业务关系发生变化,你无法避免地要修改代码,即使你不用O/R
Mapping。
>>可以array或collection好一点,出我觉得动态RTTI是发展方向, delphi的动态RTTI太简单, c#好一些.
正因为Delphi的RTTI比较薄,所以你有机会去按自己的意愿去扩充它了。
此外,TCollection是持久类,所背负荷较重。还是TList好。而array只适合能临时使用。
to 畅雨:
O/R Mapping在实施中不可避免地要弱化数据库机能,如果将BizObject强行与Dataset捆绑
的结果是在加重Biz层对数据库机能的依赖。事实上Biz层在绝大多数情况下与Dataset并不
是一一对应的。如果你能够找到一一对应之处,你还不如回到Dataset-DataLink-UI的模式
中,既有速度又有稳定性,O/R Mapping就显得有点多余了。
 
B

barton

Unregistered / Unconfirmed
GUEST, unregistred user!
.net咱没用过。不过偶想应该是与数据结构相关的meta类吧。它封装了一部分数据库约束
机能。
 

畅雨

Unregistered / Unconfirmed
GUEST, unregistred user!
to barton:
BizObject 与 Dataset并没有捆绑在一起,他们通过provider接口进行间接交互。
有点类似delphi中的
clientdataset->priovider->dataset。
在ADO.net中dataset实际上就是一个小型的内存数据库他可以容纳多个表及其关系。
priovider 充分利用.net的元类进行映射。
 
B

barton

Unregistered / Unconfirmed
GUEST, unregistred user!
畅雨:BizObject如果通过Meta类调用provider是一种强耦。只适合的数据库处理的内部使
用,与BizObject交互需要对Dataset进行方法一级的封装,直到只剩下业务操作为止。
 
B

barton

Unregistered / Unconfirmed
GUEST, unregistred user!
举例:
枚举业务对象:procedure EnumBizObjects(Objects: TBizObjects);//TBizObjects封装了TBizObject的构造方法
添加新的业务对象:function AddBizObject(Provider: TBizBuffer): TBizObject;//TBizBuffer与TBizObjects耦合,同时为Dataset提供数据
更新业务对象:procedure UpdateBizObject(Target: TBizObject;
Provider: TBizBuffer);//TBizObjects是TBizObject的聚合
删除业务对象:procedure DropBizObject(Target: TBizObject);
在业务层面上一点也找不到数据库的痕迹。
 

畅雨

Unregistered / Unconfirmed
GUEST, unregistred user!
大概类似:
BizObject.provider=CommonProvider
BizObject.new
BizObject.save
BizObject.delete
...
CommonProvider利用反射机制提供属性信息,根据不同的命令(new,delete,save...)
等为datset提供执行信息。
 
顶部