探讨“数据库系统的面向对象开发” (0分)

这几天把Martin Fowler的ISA里面的模式大致看了一遍(555...,假期就这样没了),觉得
受益非浅。上文这个例子是属于Table Module模式的。就是用“一表一对象”的原则实现
对象和数据表的对应关系。但这种模式还可以继续细化:
1) 上面的例子虽然分离了界面和业务规则,但数据库处理和商业规则还在同一个对象里。
按照Martin Fowler的观点,随着业务规则越来越复杂,还可以把商业规则和数据库处理
进一步分离。成为 Table Module + Table Data Gateway 模式,所有数据库操作放在
Table Data Gateway对象中,从而实现商业规则的完全独立。对于这种模式,我现在正在
试用,感觉还可以。对于楼上所提到的“手工写的代码很长”问题,解决办法是先写一个
程序根据数据库结构自动生成部分源代码。
2) 为了适应企业极度复杂的业务规则,Martin推荐的模式是Active Record 或
Domain Model + Data Mapper的组合。简单来说,所谓Active Record,就是按
“一记录一对象”的原则来构建类。而如果把数据库处理从Active Record中分离处来,则
成为Domain Model (处理业务规则)+ Data Mapper(处理对象到数据库的映射)。对于这
种模式,我则感到有点问题:SQL的基本概念是面向表操作的,但如果使用“一记录一对象”
的方式,则在编写业务规则时是面向记录。到底如何衔接这两种不同层面上的思维方式?
而且使用这样的模式在进行批量的数据处理时,到底是撇开数据库,直接使用对象来处理
(可能导致创建大量对象和效率问题)?还是绕过记录对象,使用面向表的SQL命令来处
理?(但写进了记录类中的业务规则也被绕过了) 希望有朋友对“一记录一对象”
的模式提提自己的看法。
 
对于zhongs的疑问,在这里做点补充。其实这两种方法的区别不但在于写法,而在于
业务规则在系统中的位置。在这里要考虑的是一个三个部分的关系:界面,业务规则,
数据库。
问题的焦点是,在分离了界面和业务规则之后,数据库到底和该和哪部分联系。界面知道
何时使用数据,而业务对象知道如何使用数据。虽然把数据库操作写在界面代码里比较直
接、方便。但其实这样做和界面里包含业务规则有着同样的缺点:数据库的改动会影响界
面代码。而界面代码是根据不同控件来组织的,关于同一业务的代码往往被分散在不同地
方,不便于修改。而且,实际上只有业务对象才知道什么数据需要保存,以及保存时需要
遵守的业务规则。因此把最好应把数据库和业务规则联系起来,由业务对象去使用数据库
对象。但因为界面知道何时使用数据,因此可以用业务对象“代理”数据对象,界面通知
业务对象何时使用数据库。
上面的程序就是用TDataRecordset"代理"了一个Datasource类来实现这种联系。由于
使用了“代理”关系,必然会出现很多用于传递类消息的“代理方法”,看上去是麻烦了。
但实际应用中,这些方法并不常用。例如用一个"FillListWithBookName"的方法把数据库
中的书名填到一个TStringList中,在这个业务中就不需要使用First, Next等方法了。如果
类设计得好,其实是可以减少很多这样的“代理方法”的。但如果真的有需要,则加上也
无妨。
 
to kidneyball:
Martin Fowler的这本书叫什么名字?能下载到吗?
 
叫 Patterns of Enterprise Application Architecture 。这本书还没出版,作者把
稿子放在了 http://martinfowler.com/isa (也就是楼顶的朋友所给的“相关网站”)。
据说出版商只让他放到今年11月份。有兴趣可以去看看
 
等了这么久总算等到一个我比较满意的答复了。最近我比较忙,这个问题就放下了,martinfowler的
文章由于我英语不是很好,看起来很吃力,不过对于这个问题我还是很关注的,前一段时间还和王寒松探
讨过,看了他给的一个程序模块后很受启发,里面的类设计的相当精巧,比起楼顶的代码要精简不少,
但他也是把数据库处理和商业规则放在同一个对象里。
>>成为 Table Module + Table Data Gateway 模式,所有数据库操作放在
>>Table Data Gateway对象中,从而实现商业规则的完全独立。对于这种模式,我现在正在
>>试用,感觉还可以。
对于你说的这个模式,我很有兴趣,不知可否给我一些示例代码以作交流研究,谢谢!
E-mail:zhouqing99@163.net
 
嘿!!!
kidneyball是谁?我必须结识你!
我从98年就开始使用一记录一对象了。现在更是每项目必用!
我是这样使用的:
1.最顶层:只描述对象与对象间可能发生的关系,大部分都是聚合类,不与业务逻辑发生太
大的牵扯。虽然可以看成是所有对象的管理器但本身也是业务对象之一。我总是将一些重要
的对象直接放在顶层下,例如:目前的登录用户、目前的状态等等。
2.第二层放置所有的聚合类。聚合类解决两个问题:数据搜索和数据列表因为同类数据一定
放在同一个聚合类中,所以这个聚合类可以想象成TTable。
3.第三层处理每个对象在内存中的映射,也就是对应表中的一行记录,但不处理数据更新。
只有这个数据被更改后才会被修改,然后由这个对象通知顶层对象。
4.第三层对象的来源目前我只支持来源于XML,当然完全可以来源于TFields,但这个东西我
实在是讨厌。当某个对象被建立、被删除、被置为焦点时都会通知顶层。
5.有一个非常有效的通道从这个顶层对象到达你整个项目的任何一个地方。你猜我通过什么
实现?呵呵。我有个间谍管理器。如果你某个地方需要监控对象的变化你向顶层对象注册一
个间谍就行了。你当然可以设定你的间谍只关心某类对象。当然,你不需要这个间谍的时候,
别忘了注销它。
6.每类对象都需要写一个与它结构类似的更新器。这个更新器解决一个问题:在对一个新对
象建立前或一个旧对象改变前与真正实现数据库刷新之间建立一个临时的区域这个更新器需
要实现:a.建立一个空白的对象更新器(新建);b.从一个已存在的对象建立一个更新器(修
改);c.打算删除某个对象在没有实现数据库一级的删除前将这个对象临时藏在更新器中;
d.建立这个对象所需要的一切约束(业务规则),例如什么项目不可以缺,什么项目的数据必
须在什么范围内等等,通过查询该更新器的Validate属性就能知道可不可以向服务器提交。
e.查询该更新器是否已经被改动,查询Modified属性。如果没有被改动就不必向服务器提交。
f.你可以同时添加、修改或删除多个对象,甚至这个对象所包含的若干子对象;这样你需要
设计一个更新器的聚合类进行管理,原来的数据和已经更新的数据都可以从更新器及其聚合
类中找到。
7.发现什么了吗?到目前为止还没有与界面打交道。可是这时候该与界面打交道了。所有的
界面只需要与更新器联系,外壳界面直接向更新器聚合写数据,如果确认的话向更新器聚合
发布一条submit命令,然后更新器聚合会依照业务规则向数据库写数据。
8.外壳界面到底是什么?窗体(TForm)或者ASP对象或者HTML中的Form。对于更新器聚合来讲
并不关心的。
9.完全摆脱了TDataset了你再写个三层吧!放弃MIDAS!Go! Come on!
 
现在该处理这个机制的高级应用了。
当系统启动的时候有一些基本的对象需要首先建立。有的项目中这个数据集可能是很大的。
这好办在建立主窗体之前建立一个线程搞定它。
实现这一切的难点在哪儿?在Notify体系中。在每个对象被完全建立后才能通知顶层;在
还没有删除打算删除前就必须通知所有的间谍。这个用Delphi实现比较困难,而用VC++就
比较容易。VC++支持多重继承而我们所喜欢的Delphi没有,通过接口实现是不可能的。
这是继承体系:
TObject
|--TCxsObject(单纯数据对象,它缺省的构造方法不是Create,而是CreateFromXML)
| |--TCxsLeaf(带ID属性的数据对象)
| |--TCxsBranch(含有子对象的数据对象)
| |--TGlobal(顶层对象)
|--TCurtain(呵呵,这家伙负责与外界联络,相当于公关部长)
|--TCxsObjects(TCxsObject的聚合)
| |--TCxsLeaves(TCxsLeaf和TCxsBranch的聚合)
|--TCxsInfo(更新器)
|--TCxsInfos(更新器聚合)
|--TCxsSheet(组合对象,类似数据库中的视图行)
|--TCxsSheets(组合对象的聚合)

每个项目做完,发现我的xxxObjs单元最大,没有少过200k的。可是发现这样做非常值。
各位我已经取了个名字:CXS(Client/XML/Server),大家认为行吗?
 
潜在的商业价值:
大家已经发现了,其实你的CXS也没有什么奥妙。OK!的确如此!
在设计数据库的时候,你已经多多少少将一些业务规则放到了数据库中,例如:设置主键,
设置外键、设置触发器等等。很遗憾的是:你现在需要将这些规则在设计CXS类的时候重复
一遍。有没有办法一次搞定呢?
写这些类就象zhongs所说,都是些简单重复的内容。其实如果开发出一个类似PowerDesigner
或者Rose之类的工具,设计数据库的时候直接生成这些类的源代码,完全可行。那样就不需
要Copy-Paste了。而且如果真的要更改数据库的话只需要将这背地类的代码重新生成一遍即
可。我现在就在做这项工作。本论坛的很多大虾是在校学生,做这些比我条件好:一是时间
充足;二是理论比较扎实。所以让我做这件事是不是有些不公平?
 
为什么不能把这些建立在MIDAS基础上?毕竟MIDAS的缓冲机制是非常有效的
 
观点让人兴奋,但有点华而不实。
1、对于系统大时不容易定位错误。
2、代码增加的速度非常快,但复用得不多。
3、面向对象的代码效率差,尤其是多重继承。
 
to 莫知:我的程序写了一部分,还在修正中。因为刚刚看完理论,所以主要目的只是为了
实践一下。但这几天在忙课程设计(每天都被VB气得吐血 :( ),就搁下来了。但仍然
希望和各位朋友们交流一下心得。我的邮箱是zhizhideng@hotmail.com (hotmail messager随时开着),
qq是521955 (验证信息上请说明是delphibbs上的朋友)
 
to barton: 你好,对你所讲到的架构我很感兴趣。但看完之后有一下几个问题(有可能是
理解错误了,请勿见笑):
1)
感觉上整个架构是把TDataSet重写了,在其中加入业务规则。主要的规则放在了第三
层的记录对象和对应的更新器中。第二层(或第一层?)的聚合类负责数据库读写。但这样
的结构仍然是太注重于数据管理的结构(模仿了TDataSet),而且部分数据管理的结构仍然
留在业务对象中。所以必须使用多继承才能加入在这个体系以外的机制 (Martin Fowler
在书中所用的例子是Java和C#,这两种语言都是单继承的)。
而按我的理解,业务对象应该要么完全地与数据库结合(Active Record,包含了业务
规则和数据库处理),要么完全与数据库分离(Domain model+Data Mapper)。值得说明一
下的是,在Domain Model+Data Mapper结构中,Domain Model是完全不知道Data Mapper的
存在的(仅知道一个Finder的Interface,用于向Mapper申请创建有关联的业务对象)。
客户向Mapper申请Create Domain Model对象,之后系统使用一个ObjectList来记录改动过和
新建(指数据库中新建)的业务对象(也就是你的系统中的顶层)。整个事务完成之后(或其他时机,由客户掌握),
由Mapper统一提交更新。架构起这个机制之后,Domain Model对象的所有注意力都集中于业务
规则上。费这么大的力气把数据库的表间关系映射为对象之间的依赖或聚合关系,是基
于一个假设:业务规则已经复杂到数据库的表间关系无法有效地表达。而对象关系则可以使用
继承和各种设计模式来表达出这样的规则。

2)
你的系统中使用的间谍机制是不是Observer模式(我的的设计模式看的是e文的,中文模式
名没记住,呵呵)? 其实我觉得这里为了记录变动,应该不用完全的Observer Pattern。仅需
要在改动时通知Subject(顶层),Subject登记更新的对象和更新类型(删,查,改,这个标志
也可以放在业务对象中),在一个事务完成之后统一通过Data Mapper更新数据库。初步估计这
样的话应该用单继承就可以了。在业务类中的Notify代码和更新标志可以写在整个业务层的统一
基类里。

3)
对于我原来的第二个问题,我想你应该知道答案。使用一记录一对象的方法,怎样处理
批量数据更新的事务?比如工资提级,要对Employee库的所有人进行操作。这样的话,我觉得
可以选择
1)逐个人读入,更新,立即回写 (一记录一对象,一人一事务,可能有效率问题)
2)逐个人读入,更新,所有人完成后统一更新 (一记录一对象,N人一事务,可能有空间问题)
3)绕过业务对象,另外用一个Transaction Script对数据库更新 (需要重复业务规则,
例如提级人员需要另外备档,个别提级备档代码已写在业务类中。在这里则要重复)

请问你在实际应用中是使用哪个方法,或者有更好的办法,有没有遇到上面所说的问题呢?
 
好东西,收藏先!
 
反正Table Module和Table Data Gateway的文章都不长,我干脆把它们译出来吧。
顺便把例子换成DELPHI语法的,我自己也加深一下理解。这些代码都是我看着C#的代码就凭空
写上去的,没经过测试。而且本人英文翻译水平也不高,很多地方是按照我的理解意译的。但
还是希望对大家有所帮助

先放上Table Module的。文中的例子背景在书中的另一章节里,为了保持连贯性。我稍后给出

--------------------------------------------------------------------------------

Table Module

--------------------------------------------------------------------------------

为一个数据表的所有行为提供一个单独的对象

(图 http://martinfowler.com/isa/tableModuleSketch.gif)

面向对象的一个关键特征是捆绑了数据和使用这些数据的方法。传统的面向对象手段
是基于个体对象的,例如Domain Model。因此如果我们有一个“员工”类,这个类的任何
实例都对应一个特定的员工。这个方案运作得很好,因为我们掌握了一个到特定员工的引
用(reference)。对于这个员工,我们可以执行操作,跟踪关系(follow relationships)和
收集数据。

Domain Model面临的一个问题是它与关系数据库的接口。在许多方面,Domain Model方
法把关系数据库看成一些躲在阁楼里无法与人沟通的老婆婆。因此,你常常需要大量的编程
工作来向数据库存取数据,在两种不同的数据表示方式中来回转换。

一个Table Module将数据库中每一个数据表的业务逻辑组织在一起,而且每一个类实例
都包括了各种各样处理数据的过程。它与Domain Model的主要区别是:假如你有许多订单,
Domain Model将为每一份订单创建一个订单对象,然而Table Module将用一个对象来处理所
有的订单。


How it Works


Table Module的优点在于它允许你将数据与行为封装在一起,同时使用关系数据库的强
大功能。表面来看,Table Module和普通对象很相似。关键的区别在于Table Module对它正
在处理的个体毫无概念。因此如果你想取得一个员工的地址,你需要有一个类似
anEmployeeModule.getAddress(long employeeID)的方法。每一次你想对一个特定员工进行
操作,你都必须传递某种确定个体身份的参数。通常,这个参数是数据库中所用的
primary key.

你时常会将Table Module与一个面向数据表的后台数据结构结合起来使用。这些表列数
据(tabular data)通常是一个SQL语句的结果集,它存储在某种模拟SQL数据表格式的记录集合
对象(record set object)中。Table Module为你提供一个显式的方法对这个数据表进行操
作,这一方法基于这个记录集合对象的interface.

Table Module可能是一个对象实例或一些静态方法的集合。使用对象实例的优点是它允
许你用一个已存在的record set来初始化这个Table Module,这个record set可能是一个查
询的结果。这样,我们使用这个对象实例来操作这个record set中的记录。使用对象实例可
以使用继承,因此我们可以写一个包括更多行为的ManagerModule

Table Module可能包括用作factory methods(设计模式)的查询. 另一选择是使用一个
Table Data Gateway。The disadvantage of using an extra Table Data Gateway class
and mechanism in the design(译注:这句未完成,可能作者在写到这里时还未想好,后
来忘了。毕竟,这只是书稿的beta版)。Table Data Gateway的优点是它允许你使用一个
Table Module来处理从不同数据源得来的数据,因为你可以对不同的数据源使用不同的
Table Data Gateway.


When to Use it

Table Module非常依赖于面向数据表的数据。因此,很明显,它在你使用表列数据
(tabular data)时才有意义。而且它把这一数据结构放得非常靠近代码的核心位置,因此
你会希望你能很直接地使用这一数据结构。

我会选择使用这一模式的一个最著名的情况是在使用微软的COM设计时。在COM中,
record set是应用程序的主要数据存储仓库。Record set可以被传递到UI的数据感知控件中
来显示信息。微软的ADO libraries为你提供了一个很好的机制去以record set的方式访问
关系数据库。在这种情况下,Table Module允许你把业务逻辑非常有组织地结合到应用程序
中,而不会失去处理表列数据的各种要素(原句为:without losing the way the various
elements work on the tabular data, 我觉得the way为笔误)

这样你可以访问数据库中的数据并把它传递给一个Table Module。 这些数据被加入
Table Module的后台data set中。你可以用这个Table Module对这些数据进行运算。然后,
你可以把这个data set传递给界面,用数据感知控件对它们进行显示和修改。数据感知控件
并不关心这个record set是直接从关系数据库中来的,还是一个Table Module正在用这种方式来
处理数据。在界面中完成修改后,data set返回到Table Module中进行检验,然后保存到数
据库。

但是Table Module不会为你提供面向对象在管理复杂逻辑中的全部功能。你不能用一个
直接的对象实例来实例化表间关系(relationships),而且多态不能很好地运作。所以对于处
理复杂的业务逻辑,Domain Model是一个更好的选择。基本上,你必须权衡以下两者作出选
择:Domain Model具有处理复杂逻辑的能力;Table Module则能简单地和面向数据表的数据
结构整合起来。

如果Domain Model中的所有对象和数据库中的对应的数据表都很相似,则选择一个使用
Active Record的Domain Model会更好。但在程序的其他部分基于一个普遍的面向数据表的数
据结构时,使用Table Module比混合使用Domain Model和Active Record更好。这就是为什
么你通常在Java环境下看不到Table Module,虽然,随着row sets的使用越来越普遍,这
一情况可能会改变。


c#例子:使用Table Module进行Revenue Recognition(译注:收入识别?收入确认?哪位学经济的高
人解释一下吧)

是时候再看一次那个我在其他业务建模模式中使用的revenue recognition例子了,但
这次是用一个Table Module。重温一下,我们的任务是在业务规则会根据产品类型变动的
情况下对订单进行“收入确认”。在这个虚构的例子里,我们对字处理软件,电子表格软件
和数据库软件使用不同的规则。(例子背景的详细说明请看下一条帖子)

Table Module是基于某种数据方案,通常是一个关系数据模型(虽然在未来我们可能会
经常看到XML--另一种在使用上相似的模型)。在这个例子中我会用图1所描述的关系数据表。
(图 http://martinfowler.com/isa/TableModuleDB.gif)

译注,数据关系为:
Products(ID:Number, Name:string, Type:string) <1--- Contracts(ID:number, whenSigned:Date, Amount:number) <1----Revenue Recognitions(ID:number, amount:Number, date:Date)


处理这些数据的的类与表结构非常类似。每个数据表都有一个Table Module类与其对应
。在.NET架构中有一种data set对象为数据库结构提供了一个在内存中的表示(译注:类似
Delphi的TDataSet),所以创建基于这种data set进行操作的类是有意义的。每一个Table
Module类有一个指向数据表的成员变量,这是一个.NET的系统类,它用一个data set对应一
个数据表(译注:也就是Delphi中的TDataSet啦)。这种可以读数据表的能力对于所有Table Module都
是很普遍的,因此它可以放在超类(superclass)中。

class TableModule...
protected DataTable table;
protected TableModule(DataSet ds, String tableName) {
table = ds.Tables[tableName];
}

================= DELPHI ====================
译注:对应Delphi代码为(仅以TTable为例):
TTableModule = class
...
protected
DataTable: TTable;
constructor create (DatabaseName:string; tableName: string); overload;
...
end;
...
procedure TTableModule.create(DatabaseName:string; tableName:string)
begin
inherited create;
table := TTable.create(Application);
table.DatabaseName := DatabaseName;
table.tableName := tableName;
end;

构造函数作了改动,因为c#中用Dataset.Tables[TableName]可以返回指定table的引用。
在Delphi中也可编程实现这样的机制。
但为了方便,这里改用指定数据库名和表名来建立指定数据表的连接。
=================== END ===================

子类通过传递正确的表名来调用超类的构造函数

class Contract...
public Contract (DataSet ds) : base (ds, "Contracts") {}

================== DELPHI ========================

TContract = class(TTableModule)
...
public
constructor create (DatabaseName:string);
end;
...
procedure TContract.create(DatabaseName:string);
begin
inherited create(DatabaseName,'Contracts');
end;
================== END ==================

这样允许我们仅仅通过传递一个data set给Table Module的构造器就可以创建一个新的
Table Module。

contract = new Contract(dataset);

========== DELPHI ==============
contract := TContract.create(DatabaseName)
这里改成了传递数据库名
============= END ===============

这样就把创建data set的代码从Table Module中分离出来,遵守了使用ADO.NET的指导
规则。

一个有用的功能是使用C#的indexer来根据给定的primary key在数据表中取得一条特定
的记录

class Contract...
public DataRow this [long key] {
get {
String filter = String.Format("ID = {0}", key);
return table.Select(filter)[0];
}
}

=============== DELPHI =================
我暂未发现Delphi中有类似DataRow的类,因此只能自己编程模拟(用一个tfields对象储存
记录数据)
希望有类似经验的朋友谈谈这方面的看法。为了文章的连贯性,不在这里详细讨论
TDataRow,仅说明一下TDataRow的属性和方法。

TDataRow.Greate(Dataset:TDataset,1) 根据指定Dataset的当前活动记录创建DataRow实例
TDataRow.Greate(Dataset:Tdateset,2) 根据指定Dataset结构创建一个空DataRow
TDataRow['字段名'] 或 TDataRow.ValueOfField 返回或设置字段值.
TDataRow.Update() 提交修改,如果是用参数1创建的,则更新。用参数2创建的,则加入。

TContract = class(TTableModule)
property ContractByID[const key : integer]:TDataRow read GetID;default;
end;

function TContract.ContractByID(const key:integer) : TDataRow;
var OldIdx : string;
begin
OldIdx := table.IndexFieldNames;
Table.IndexFieldNames := 'ID';
if Table.FindKey(['ID']) then
Result := TDataRow.create(Table,1)
else
Result := nil;
table.IndexFieldNames := OldIdx;
end;
====================== END =============================

第一部分的功能是计算为一份合同计算“收入确认”,然后更新“收入确认”数据表。
收入的数目是基于产品的类型的。因此这个行为主要使用合同表中的数据。因此我决定把这
个方法加入到合同类中。

class Contract...
Public void CalculateRecognitions (long contractID) {
DataRow contractRow = this[contractID];
Decimal amount = (Decimal)contractRow["amount"];
RevenueRecognition rr = new RevenueRecognition (table.DataSet);
Product prod = new Product(table.DataSet);
long prodID = GetProductId(contractID);
if (prod.GetProductType(prodID) == ProductType.WP) {
rr.Insert(contractID, amount, (DateTime) GetWhenSigned(contractID));
} else if (prod.GetProductType(prodID) == ProductType.SS) {
Decimal[] allocation = allocate(amount,3);
rr.Insert(contractID, allocation[0], (DateTime) GetWhenSigned(contractID));
rr.Insert(contractID, allocation[1], (DateTime) GetWhenSigned(contractID).AddDays(60));
rr.Insert(contractID, allocation[2], (DateTime) GetWhenSigned(contractID).AddDays(90));
} else if (prod.GetProductType(prodID) == ProductType.DB) {
Decimal[] allocation = allocate(amount,3);
rr.Insert(contractID, allocation[0], (DateTime) GetWhenSigned(contractID));
rr.Insert(contractID, allocation[1], (DateTime) GetWhenSigned(contractID).AddDays(30));
rr.Insert(contractID, allocation[2], (DateTime) GetWhenSigned(contractID).AddDays(60));
} else throw new Exception("invalid product id");
}
private Decimal[] allocate(Decimal amount, int by) {
Decimal lowResult = amount / by;
lowResult = Decimal.Round(lowResult,2);
Decimal highResult = lowResult + 0.01m;
Decimal[] results = new Decimal[by];
int remainder = (int) amount % by;
for (int i = 0; i < remainder; i++) results = highResult;
for (int i = remainder; i < by; i++) results = lowResult;
return results;
}

============= DELPHI ==================
type
DecimalArray = array of real;
ProductType = (prdWP, prdSS, prdDB, prdUnknown);

TContract = class...
public
procedure CalculateRecognitions (contractID:integer);
...
private
function allocate(amount:real; by:integer) : DecimalArray;
...
end;
...
procedure TContract.CalculateRecognitions(contractID:integer);
var
contractRow :TDataRow;
amount : real;
rr : TRevenueRecognition; //另一个Table Module
prod : TProduct;
prodID : integer;
begin
contractRow := self[contractID]; //调用之前定义的ContractByID方法
if contractRow = nil then exit;
amount = contractRow['amount'];
rr := TRevenueRecognition.create('数据库名');
prod := TProduct.create('数据库名');
prodID := GetProductId(contractID);
if prod.GetProductType(prodID) = prdWP then
begin
rr.Insert(contractID,amount, TDateTime(GetWhenSigned(contractID)));
end
else if prod.GetProductType(prodID) = prdSS then
begin
allocation := allocate(amount,3);
rr.Insert(contractID, allocation[0], TDateTime(GetWhenSigned(contractID)));
rr.Insert(contractID, allocation[1], TDateTime(GetWhenSigned(contractID))+60);
rr.Insert(contractID, allocation[2], TDateTime(GetWhenSigned(contractID))+90);
end
else if prod.GetProductType(prodID) = prdDB then
begin
allocation := allcate(amount,3);
rr.Insert(contractID, allocation[0], TDateTime(GetWhenSigned(contractID)));
rr.Insert(contractID, allocation[1], TDateTime(GetWhenSigned(contractID))+30);
rr.Insert(contractID, allocation[2], TDateTime(GetWhenSigned(contractID))+60);
end
else raise Exception.Create('invalid product id');
end;

function TContract.allocate(amount:real; by: integer) : DecimalArray;
//这个函数按名字来看是用分配每次收款的金额的,最后返回一个数组,为每次所收的金额。但这个函数看上去有错,后面两个循环会导致数组超界,可能为作者笔误。或我对c#学艺未精理解有误。但这并不影响对整体程序的理解,只要知道这个函数的意图就可以了。
var
lowResult, highResult : real;
remainder : integer;
begin
lowResult := amount / by;
lowResult := Round(lowResult,2);
highResult := lowResult + 0.01;
relength(result,by);
remainder = Floor(amount) div by;
for i := 0 to remainder - 1 do result := highResult;
for i := remainder to by - 1 do result := lowResult;
end;

....................

=================== END =======================


在这里通常我会使用一个Money (译注:作者的另一pattern,专门用于处理与货币相关
事务的对象),但因为程序的通用性的原因,我在这里使用了decimal类型。我在Money模
式中使用了同样的分配方案。

为了这个功能能完整地运行,我们需要一些在其他类里定义的行为。一个产品要能够告
诉我们它是什么类型的,这个功能可以通过一个产品类型的enum类型(译注:不好意思,中
文名不记得了)和一个lookup方法来实现。


public enum ProductType {WP, SS, DB};

class Product...
public ProductType GetProductType (long id) {
String typeCode = (String) this[id]["type"];
return (ProductType) Enum.Parse(typeof(ProductType), typeCode);
}
============= DELPHI ================
//题外话:比较上下两段程序,结论是:如果按程序行数算工钱的话,一定要用Delphi
TYPE
ProductType = (prdWP, prdSS, prdDB,prdUnknown); //已在上例中给出

TProduct = class...
pubic
function GetProductType (id:integer) : ProductType ;
end;

function TProduct.GetProductType(id:integer) : ProductType;
var typeCode : string;
begin
with self[id] do //参考TContract的说明,self['key']返回一个TDataRow对象,它储存了primary key为'key'的product记录信息
begin
typeCode := ValueOfField['type']; //参考上文关于TDataRow的说明
free; //释放TDataRow对象,唉,谁叫Delphi没有垃圾收集器呢。
end;
if typeCode = 'WP' then
result := prdWP
else if typecode = 'SS' then
result := prdSS
else if typecode = 'DB' then
result := prdDB
end;
================== END ==============================

GetProductType封装了数据表中的数据。有人争议应该为表中所有字段都进行这种封
装。这种争议同时也指向我,因为我访问contract里的金额时使用了直接访问的方式。虽然
封装通常是一件好事,但我在这里不这样做,因为这里假设系统的不同部分会直接访问
这个data set,而封装所有数据并不符合这个假设。当这个data set传递给界面的时候,
是没有任何封装可言的。因此访问字段的函数其实仅仅在有更多的事情要做的时候才有意
义。例如这里实现了字符串到productType的转换。

另一个需要说明的行为是在TRevenueRecognition(收入对象)中插入一条新记录

class RevenueRecognition...
public long Insert (long contractID, Decimal amount, DateTime date) {
DataRow newRow = table.NewRow();
long id = GetNextID();
newRow["ID"] = id;
newRow["contractID"] = contractID;
newRow["amount"] = amount;
newRow["date"]= String.Format("{0:s}", date);
table.Rows.Add(newRow);
return id;
}

========= DELPHI ===============
TRevenueRecognition = Class...
public
function Insert(contractID:integer; amount:real; date:TDateTime):integer;
end;

function TRevenueRecognition.Insert(contractID:integer; amount:real; date:TDateTime):integer;
var newRow:TDataRow;
id:integer;
begin
newRow := TDataRow.create(table,2);
try
id = GetNextID();
newRow['ID'] := id;
newRow['contractID'] := contractID;
newRow['amount'] := amount;
newRow['date'] := date;
newRow.Update;
finally
FreeAndNil(newRow);
end;
end;
============== END ========================

再一次说明,这个方法的主旨不在于封装data row,更多是在于用一个对象方法取代可
能会在多个地方重复出现的一段代码。

另一个行为是在一个contract中总计某一天前所有已确认的收入。因为它使用了
revenue recognition表,因此最好把这个方法定义在TRevenueRecognition类中

class RevenueRecognition...
public Decimal RecognizedRevenue (long contractID, DateTime asOf) {
String filter = String.Format("ContractID = {0} AND date <= #{1:d}#", contractID,asOf);
DataRow[] rows = table.Select(filter);
Decimal result = 0m;
foreach (DataRow row in rows) {
result += (Decimal)row["amount"];
}
return result;
}

========== DELPHI =================
TDataRowList为一个DataRow的容器。它的主要方法是
TDataRowList.Create(DataSet :TDataSet) 为Dataset中每一条记录创建TDataRow对象
TDataRowList.Clear 释放所有TDataRow对象
TDataRowList[Index] 或 TDataRowList.Rows[Index] 返回对第Index条DataRow的引用
TDataRowList.count 返回DataRow的数目。

TRevenueRecognition = Class...
public
function RecognizedRevenue(contractID:integer; asOf:TDateTime):real;
end;

function TRevenueRecognition.RecognizedRevenue(contractID:integer; asOf:TDateTime):real;
var filter : string;
rows : TDataRowList;
i : integer;
begin
filter := 'ContractID = ' + inttostr(contractID) + ' AND date <= ' + datetimetostr(asOf); //不知这样的写法是否有效。但不难修正。
table.filter := filter;
table.filtered := true;
rows := TDataRowList.create(table);
result := 0;
for i := 0 to row.count - 1 do
result := result + rows["amount"];
end;

如果不使用DataRowList,可以 (但直接操作table在实际运用时候会不会与系统其他部分有冲突我尚不知道,希望各位朋友发表一下看法)
function TRevenueRecognition.RecognizedRevenue(contractID:integer; asOf:TDateTime):real;
var filter : string;
begin
filter := 'ContractID = ' + inttostr(contractID) + ' AND date <= ' + datetimetostr(asOf);
table.filter := filter;
table.filtered := true;
table.first;
while not table.eof do
begin
result := result + table['amount'];
table.next;
end;
end;
============= END ====================

这段程序得益于ADO.NET中一个很好的功能,它允许你定义一个where子句从而在数据表
中选择出一个子集来进行操作。其实,你可以更进一步,使用统计函数。(Delphi中不通
用)

class RevenueRecognition...
public Decimal RecognizedRevenue2 (long contractID, DateTime asOf) {
String filter = String.Format("ContractID = {0} AND date <= #{1:d}#", contractID,asOf);
String computeExpression = "sum(amount)";
Object sum = table.Compute(computeExpression, filter);
return (sum is System.DBNull) ? 0 : (Decimal) sum;
}


--------------------------------------------------------------------------------

 
例子背景:
Revenue Recognition问题
Revenue recognition是一个在商业系统很普遍的问题,它是关于你什么时候能够真正把你在预订中获得的钱划到帐上。如果我卖给你一杯咖啡,这非常简单。我把咖啡给你,拿到钱。我在十亿分之一秒内就可以把这些钱入帐了。但对于许多其他事务,事情就变得复杂了。比如你给我一笔一年的预聘费,我不能把它立即放到账本上,因为这个服务是在一年的过程中的实施的。一个方法可能是,在这年的每一个月内,我仅仅把12分1的收入入帐,因为你有可能在1个月后发现我由于忙于写作而荒废了编程,于是取消合同。

revenue recognition的规则多种多样而且容易发生变动。有些是由规章设定的,有些由行业规范设定,有些由公司政策设定。跟踪收入因此成为一个相当复杂的问题。

现在我并不想钻研这么复杂的情况。相反,我们想像一个公司出售三种产品(Product):字处理软件,数据库,和电子表格。规则是当你签了一份字处理软件的合同(Contract),你可以把所有收入立刻划入帐上。如果签了一份电子表格合同,你可以当日把1/3入帐,1/3在30天后入帐,1/3在六十天后入帐。而如果是数据库,则1/3在当日入帐,1/3在60天后入帐,最后1/3在90天后入帐。制订这样规则完全是由于我心血来潮,没有什么其他原因。但有人告诉我实际的规则其实也跟这个一样有道理(译注:即是没道理啦)。
 
--------------------------------------------------------------------------------

Table Data Gateway

--------------------------------------------------------------------------------

An object that acts as a Gateway to a database table
一个作为数据库表Gateway的对象
(图) http://martinfowler.com/isa/dbgateTable.gif

分离访问数据库的代码与程序的其他代码是一件好事。一个简单的Table Data Gateway掌
握了所有用于访问单个数据表的SQL语句:select, insert, update, 和delete。其他程序
调用它的子程序来更改数据库,以及使用它的find程序来实施查询。每个find程序都回传
合适的数据结构。

How it Works

一个Table Data Gateway有一个简单的接口,通常由一些从数据库中取数据的find方法和
update,insert,delete方法组成。每一个方法都把输入的参数映射到SQL语句中并向一个数
据库连接提交这个SQL。Table Data Gateway通常是stateless(?)的(译注:原句为The
Table Data Gateway is usually stateless),因为它扮演的角色是存取数据。

Table Data Gateway中最麻烦的事情是怎样从一个查询中返回信息。即使是一个简单的使
用id查找的查询都需要返回多个数据项目。在你能够一次返回多个值的环境中,你可以用这种
技术来返回一条记录。但许多语言仅仅允许你使用一个返回值,而且许多查询会返回多条记录。

一个替代的办法是返回一些简单的数据结构,例如一个map。map有一定作用,但为了使数据
从数据库中进入map中,数据必须从record set中复制出来。我发现使用map来传递数据是一
个坏方法,它使编译期的检查失效,而且没有提供一个显式的接口 - 导致了当编程人员对
map拼写错误而形成的bug。一个更好的选择是使用Data Transfer Object(译注:另一种
模式。一个仅仅用于传递数据的类,不包含其他复杂的处理),这需要创建另一个对象 - 但
它可能在别处已被用到了。(anther object to be create - but one that may well be
used elsewhere.)

为了节省以上提到的所有这些代价,你可以返回一个从SQL查询中得出的Record Set。但这
在概念上是很模糊的,因为理想中我们希望内存中的对象不需要知道关于SQL接口的细节。
而且如果你无法简单地在你代码中建立起record set机制,这种方法还使以数据库来代替文件变
得困难。但是在许多普遍使用Record Set的环境中,例如.NET,这是一个非常有效的手段。
这样,一个基于数据表的Table Data Gateway与Table Module结合的非常好。如果你所有
的更新都通过Table Data Gateway提交, 那返回的数据可以基于视图(view)而不是实际的数
据表。这一做法减少了你的代码与数据库的耦合。

如果你正在使用一个Domain Model,你可以使用Table Data Gateway返回合适的业务对象。
这样做的问题是在gateway与业务对象之间存在双向依赖。由于这两者关系紧密所以这并不
是一件很可怕的事情,但这总是一件我不希望发生的事。

在你使用Table Data Gateway的大部分情况下,你会为数据库中每个数据表设置一个
Table Data Gateway。但对于非常简单的情况,你可以用一个Table Data Gateway处理全
部数据表的所有方法

When to Use it

与Row Data Gateway一样,选择使用Table Data Gateway时首先要确定是否使用Gateway手
段,然后确定使用哪种Gateway.

我发现Table Data Gateway可能是可用的最简单的数据接口模式了,因为它很好地实现了到
数据表或记录类型的映射。它还提供了一个自然的结合点来为封装精确的数据源访问逻辑。
我很少将它与Domain Model一起使用,因为我发现Data Mapper把Domain Model与数据库分
离得更好,而且用起来并不是太过于复杂。

Table Data Gateway与Table Module结合时工作得特别好,这时Table Data Gateway生产出
一个record set数据结构给Table Module进行处理。事实上,我不能为Table Module想出其
他数据库映射方案了。

就象Row Data Gateway, Table Data Gateway对Transaction Scripts(译注:另一模式,
就是最常用的一事务一子程序模式)非常适合。在这两者间(Row Data Gateway vs. Table Data Gateway ?)
做出选择的标准在于怎样处理多条数据记录。许多人喜欢使用Data Transfer Object,但
这似乎要做许多不值得的工作,除非同一个Data Transfer Object在其他地方也被用到。当
结果集的表达方式能很方便地被Transaction Script使用的时候,我倾向于使用Table Data Gateway。

有趣地,把Table Data Gateway与Data Mapper结合起来使用常常是有意义的,
Data Mapper可以通过Table Data Gateway与数据库对话。虽然当所有东西都被硬编码的
时候,这样做意义不大。但如果你希望为Table Data Gateway使用metadata(译注:元数
据?)但又喜欢把实际的映射硬编码到domain object中的话,把Table Data Gateway与
Data Mapper结合起来的做法就非常有效了。

使用Table Data Gateway来封装数据库访问的一个好处是同样的接口能够用于两种情形:
使用SQL来操作数据库的情形,或者使用储存过程的情形。实际上储存过程本身常常被组织
为一个Table Data Gateway,这种情况下实际的表结构被封装在insert和update储存过程
后面。find过程可以返回视图,这些视图有助于隐藏实际的表结构。

例子:Person Gateway (c#)

Table Data Gateway 是在windows世界中一种很常见的数据库访问形式,因此举一个C#的
例子是很有意义的。但在这样做的同时,我必须强调这个Table Data Gateway的标准形式
并不仅仅适合.NET环境,因为它并没有从ADO.NET的data set中得到多少好处。相反地,它
使用了Data Reader,一个游标类型的数据库记录接口。在你要处理大量的信息因而不想一
次性把所有东西都放进内存的时候,data reader是一个很好的选择。

在这个例子中我使用了一个连接数据库中一个人员表的person gateway类。这个person
gateway包括了查找代码,返回一个ADO.NET的data reader来访问返回的数据。

class PersonGateway...
public IDataReader FindAll() {
String sql = "select * from person";
return new OleDbCommand(sql, DB.Connection).ExecuteReader();
}
public IDataReader FindWithLastName(String lastName) {
String sql = "SELECT * FROM person WHERE lastname = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter("lastname", lastName));
return comm.ExecuteReader();
}
public IDataReader FindWhere(String whereClause) {
String sql = String.Format("select * from person where {0}", whereClause);
return new OleDbCommand(sql, DB.Connection).ExecuteReader();
}

========== DELPHI ============
TPersonGateway = class...
public
function FindAll() : TDataSet;
function FindWithLastName(lastName : String) : TDataSet;
function FindWhere(whereClause : String) : TDataSet;
end;

function TPersonGateway.FindAll() : TDataSet;
begin
result := TQuery.create(Application);
try
result.DatabaseName := Query.DatabaseName;
//Query是gateway用于储存于数据库连接的TQuery对象。
result.SQL.Text := 'select * from person';
result.Open;
except
FreeAndNil(result)
end;
end;

function TPersonGateway.FindWithLastName(lastName:string) : TDataSet;
begin
result := TQuery.create(Application);
try
result.DatabaseName := Query.DatabaseName;
result.SQL.Text := 'select * from person where lastname = :lastName';
result.Params[0].asstring := lastName;
result.Open;
except
FreeAndNil(result)
end;
end;

function TPersonGateway.FindWhere(whereClause : string) : TDataSet;
begin
result := TQuery.create(Application);
try
result.DatabaseName := Query.DatabaseName;
result.SQL.Text := 'select * from person where ' + whereClause;
result.Open;
except
FreeAndNil(result)
end;
end;

=========== END ================

你几乎总是希望用一个reader提交一批记录。在一个罕有的情形下,你可能会希望用一个
方法提取某一条记录的数据。

class PersonGateway...
public Object[] FindRow (long key) {
String sql = "SELECT * FROM person WHERE id = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter("key",key));
IDataReader reader = comm.ExecuteReader();
reader.Read();
Object [] result = new Object[reader.FieldCount];
reader.GetValues(result);
reader.Close();
return result;
}


=========== DELPHI ====================
TPersonGateway = class...
public
function FindrRow(key : integer) : TFields;
end;

function TPersonGateway.FindRow() : TFields;
begin
Query.SQL.Text := 'select * from person WHERE id = :id';
Query.params[0].asinteger := key;
Query.Open;
result := GetRowValues(Query); //GetRowValues是一个成员方法,把当前DataSet的活动记录内容复制到一个TFields对象中。
Query.Close;
end;
============= END =====================

update和insert方法从参数中取得需要的数据并调用合适的SQL语句。

class PersonGateway...
public void Update (long key, String lastname, String firstname, long numberOfDependents){
String sql = "UPDATE person SET lastname = ?, firstname = ?, numberOfDependents = ? WHERE id = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter ("last", lastname));
comm.Parameters.Add(new OleDbParameter ("first", firstname));
comm.Parameters.Add(new OleDbParameter ("numDep", numberOfDependents));
comm.Parameters.Add(new OleDbParameter ("key", key));
comm.ExecuteNonQuery();
}

========= DELPHI ===========
TPersonGateway = class...
public
procedure Update(key:integer; lastname,firstname:string; numberOfDependents:integer);
end;

procedure TPersonGateway.Update(key:integer; lastname,firstname:string; numberOfDependents:integer);
begin
Query.SQL.Text := 'UPDATE person SET lastname = :lastname, firstname = :firstname, numberOfDependents = :NOD where id = :id';
Query.Params[0].asstring := lastname;
Query.Params[1].asstring := firstname;
Query.Params[2].asinteger := numberOfDependents;
Query.Params[3].asinteger := key;
Query.Execute;
end;
===========END==============

class PersonGateway...
public long Insert(String lastName, String firstName, long numberOfDependents) {
String sql = "INSERT INTO person VALUES (?,?,?,?)";
long key = GetNextID();
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter ("key", key));
comm.Parameters.Add(new OleDbParameter ("last", lastName));
comm.Parameters.Add(new OleDbParameter ("first", firstName));
comm.Parameters.Add(new OleDbParameter ("numDep", numberOfDependents));
comm.ExecuteNonQuery();
return key;
}


========= DELPHI ===========
TPersonGateway = class...
public
function Insert(lastname,firstname:string; numberOfDependents:integer):integer;
end;

function TPersonGateway.Insert(lastname,firstname:string; numberOfDependents:integer) : integer;
begin
Query.SQL.Text := 'INSERT INTO person VALUES:)key, :lastname, :firstname, :NOD)';
result := GetNextID();
Query.Params[0].asinteger := result;
Query.Params[1].asstring := lastname;
Query.Params[2].asstring := firstname;
Query.Params[3].asinteger := numberOfDependents;
Query.Execute;
end;
===========END==============

一个delete方法仅仅需要一个key。

class PersonGateway...
public void Delete (long key) {
String sql = "DELETE FROM person WHERE id = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter ("key", key));
comm.ExecuteNonQuery();
}

========= DELPHI ===========
TPersonGateway = class...
public
procedure Delete(key:integer);
end;

procedure TPersonGateway.Delete(key:integer);
begin
Query.SQL.Text := 'DELETE FROM person WHERE id = :id';
Query.Params[0].asinteger := key;
Query.Execute;
end;
===========END==============

例子:使用ADO.NET的Data Sets (C#)
(译注,由于在delphi中使用ADO.NET的方法我并不熟悉,因此以下代码不作转换)

一般的Table Data Gateway可以在任何平台下运作,因为它仅仅是SQL语句的一层包装。当
你使用.NET的时候你会经常使用data set,但Table Data Gateway仍然有用,虽然它变为
了一种不同的形式。

一个data set需要数据适配器(data adapters)来读取数据到data set中并对数据进行更新。
因此我发现为这个data set和存取数据的adapters定义一个容器非常有用。这样,一个
gateway使用这个容器来保存data sets和adapters。大部分行为都是有普遍意义的,可以
放在超类中。

图1: 面向data set的gateway以及支持它的容器(holder)类图
http://martinfowler.com/isa/dateSetGateway.gif
容器储存了一个data set和一个以表名来索引的adapter集合。

class DataSetHolder...
public DataSet Data = new DataSet();
private Hashtable DataAdapters = new Hashtable();

gateway 储存这个容器,并把data set暴露给用户。

class DataGateway...
public DataSetHolder Holder;
public DataSet Data {
get {return Holder.Data;}
}

gateway可以对一个已存在的容器进行操作,或者创建一个。

class DataGateway...
protected DataSetGateway() {
Holder = new DataSetHolder();
}
protected DataSetGateway(DataSetHolder holder) {
this.Holder = holder;
}

在这里,find方法可以以不同的方式来工作。因为一个data set是一个面向数据表的容器,而且一个data set可以包含来自几个数据表的数据。因此把数据读入data set是更好的做法。

class DataGateway...
public void LoadAll() {
String commandString = String.Format("select * from {0}", TableName);
Holder.FillData(commandString, TableName);
}
public void LoadWhere(String whereClause) {
String commandString =
String.Format("select * from {0} where {1}", TableName,whereClause);
Holder.FillData(commandString, TableName);
}
abstract public String TableName {get;}

class PersonGateway...
public override String TableName {
get {return "Person";}
}

class DataSetHolder...
public void FillData(String query, String tableName) {
if (DataAdapters.Contains(tableName)) throw new MutlipleLoadException();
OleDbDataAdapter da = new OleDbDataAdapter(query, DB.Connection);
OleDbCommandBuilder builder = new OleDbCommandBuilder(da);
da.Fill(Data, tableName);
DataAdapters.Add(tableName, da);
}

更新数据的时候,你直接在客户代码中对data set进行操作。


person.LoadAll();
person[key]["lastname"] = "Odell";
person.Holder.Update();

gateway可以提供一个索引器来简化取得特定记录的操作。

class DataGateway...
public DataRow this[long key] {
get {
String filter = String.Format("id = {0}", key);
return Table.Select(filter)[0];
}
}
public override DataTable Table {
get { return Data.Tables[TableName];}
}

update方法触发容器(holder)中的update行为。

class DataSetHolder...
public void Update() {
foreach (String table in DataAdapters.Keys)
((OleDbDataAdapter)DataAdapters
).Update(Data, table);
}
public DataTable this[String tableName] {
get {return Data.Tables[tableName];}
}

插入可以用差不多同样的办法完成:获得一个data set, 向数据表插入一条新的记录,并向每一字段填入数据。但是使用一个update方法在一个调用中实现插入也是很有用的。

class DataGateway...
public long Insert(String lastName, String firstname, int numberOfDependents) {
long key = new PersonGatewayDS().GetNextID();
DataRow newRow = Table.NewRow();
newRow["id"] = key;
newRow["lastName"] = lastName;
newRow["firstName"] = firstname;
newRow["numberOfDependents"] = numberOfDependents;
Table.Rows.Add(newRow);
return key;
}


--------------------------------------------------------------------------------


 
大家可以参考INSTANTOBJECT或PERSISTENTLAYER
我觉得INSTANTOBJECT做的还可以,就是没有原码?
如哪位需要PERSISTENTLAYER 可以供享!!!
 
很久没有看书了,想不到martin fowler的Table Module和我一直使用的TDataObject类有点
类似。TDataObject类继承至TDataSet,使用了装饰模式,使得可以象操作TDataSet那样去
操作TDataObject。同时,可在TDataObject的子类中加入商业规则。类似于:

TBook = class(TDataObject)
public
function FindBook(BookName: string): boolean;
end;

Book := TBook.Create(Table1);
Book.FindBook('aaa');
Book.Delete;
Book.Commit;
Book.DataSet := Query1; //TDataSet的子类都可以
Book.Append;
Book.Post;
 

Similar threads

S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
2K
SUNSTONE的Delphi笔记
S
I
回复
0
查看
577
import
I
顶部