在delphi中开发业务对象

 在delphi中开发业务对象

 

介绍
Delphi是一个非常强大的开发商业应用程序的工具。通过使用数据控件和其它VCL库,我们可以迅速地做出强壮而高效的小型应用程序。然而,开发复杂的应用程序有其特有的难题。小型部门级的应用程序相比企业级应用程序能从RAD开发方式中获得更多好处。在业务规则,数据,用户界面之间存在着复杂交互的大型C/S应用程序需要对设计有更多的关注,而不能仅仅满足于RAD.然而不管怎样, Delphi都是建立企业级应用程序非常好的工具。

开发企业级应用程序独特的挑战在于,在这个PC时代(相对于主机时代,译者注),大部分公司的数据处理是分散的。使用RAD和其它用户友好的工具开发的小型的廉价的应用程序逐渐地取代了大型主机/终端结构的程序,那些程序代码长,改动困难,而且开发维护费用昂贵。由于这种分散结构,现今有很多程序是部门级和工作组级别的。得益于程序的小型化和开发工具的进化,有很多信息部门的人员不是IT科班出身。虽然,在很大程度上,这是一个好的演变,但是目前我们面对的是大量的冗余数据和业务规则,相同的数据被各种不同的应用程序以不同的方式存储。更加麻烦的是,在各种不同的系统中往往存在相同的考量。解决这些问题的方法要重复多次。为了保持业务规则的同步,所有相关应用程序都必须同步更新,通常这是很困难的。

  现今的商业环境改变非常迅速,这些问题困扰着信息系统的开发者。现今的市场需要可以随时修改的高度灵活的信息系统。对于RAD开发方式,开发时的确比较迅速,但是,当需求和规则改变后,对软件的修改却比较缓慢。RAD容易造成一种只懂点皮毛就可以开始做的心理,这对开发来说,不是一个好习惯。由于“时间紧迫”,开发者往往把业务规则放在用户交互界面中。这些相同的规则由于在不同的地方要重复实现,他们喜欢把代码在不同的窗体和不同的事件中到处拷贝。在不同的应用程序和同一个程序不同的部分都可以看到这种本应该相同的规则,作为结果,业务规则存在着潜在的不一致,并且很脆弱。

企业发展的难题不仅仅在于开发良好的强壮的应用程序,我们必须在战略上来计划软件的开发者和终端用户存取企业数据和业务规则。为了能够给企业开发强壮可靠的软件系统,良好的工具和良好的设计缺一不可。问题的范围和解决的方案是个很大的话题,不可能在这个演讲中解决。我不会怀疑有人有很好的解决方案,也不会质疑该方案是否符合所有组织的需求。在此,我想呈现一个方案的例子――集中化业务规则,并且将概括叙述我们如何实现这个方案。
考察当前C/S计算方式的局限
RAD 胖客户方案
Delphi是一个绝佳的RAD/原型工具,开发者可以使用它迅速地开发可靠的应用程序。不同于典型的RAD项目,开发面向对象的MIS系统,需要相当多的计划和设计。RAD开发方式有它自己的适应范围:小型mis程序,原型开发,工具软件。在这三种情况下,RAD方式很好。但是,在开发企业级的mis系统时,这种方式会暴露出一些弊端。
在RAD开发方式中,把业务规则绑定到用户界面中是一个很自然的思路。举个例子:在一个控件的onexit事件中,检查它的值是否大于其它某几个控件的值的总和,如果满足则通过,否则不给保存,并抛出一个异常。但是,我们假设用户没有操作这个窗体而是用了另外的窗体,则这个检查的规则将不会执行。你永远不能确保所有的业务规则能够被执行。如果把业务规则绑定在用户界面中,当业务规则变得复杂起来时,它的维护就会变得很困难。你不得不仔细查找每一个用户可能交互的窗体,仔细查看每一个可能的控件事件,以确保每个可能的操作都不会符合新的业务规则。
另外一个考虑是代码重用,这可以通过建立一个中心的业务规则存储地方来解决。如果你把业务规则放在用户交互的窗体中,则很难被重用,除非该窗体也被用在其它应用程序中。一般开发用户拥抱RAD的主要理由是RAD可以节省大量“计划”的时间。在这个“节省下来的时间”里,他们把代码四处拷贝。其实你可以设计一个类来控制业务规则。
软件产业专家估计很多公司花费最初开发费用4-10倍的金钱用于维护软件(维护费用大大高于开发费用,译者)。当把业务规则写在GUI交互界面中时,每改动一次,维护人员就要找遍所有的源代码,然后更改全部涉及的地方,一处都不能漏掉。作为替代,如果把业务规则写在一处地方,则可大大减少维护费用。容易查找,因为只找一处,容易修改,因为只改一处。
把业务规则写在客户交互界面中还有一个弊端,当业务规则改变时。客户端软件的所有部分都要更新。在一个大企业中,有时更新一个软件的总体费用甚至超过重新写一个新的程序。这种胖客户方案在灵活性,伸缩性,可量测性方面都会有一些问题。
胖服务器方案
有很多客户/服务器的应用软件使用胖服务器样式。在胖服务器系统中,所有的业务规则都假定包含在后端数据库服务器的存储过程和触发器中。我认为这种方式同样有一些问题,为使业务规则“传播出去”,同样的规则会在客户端用客户端语言代码重写一遍,因而有人又只好把规则放客户端用户界面,而省掉服务器端。
作为一般的原则,在后端服务器通过存储过程和触发器实施业务规则要好过胖客户端方案。对于所有的客户端,在存取数据更新数据时强迫执行这些业务规则的确要好过仅仅把业务规则放在客户界面中。集中这些业务规则的代码同样带来一定的重用性。
普遍的,对很多商业应用程序要求的复杂数据处理,服务器端的语言不适合用来做这个事情的,服务器端的语言主要用于数据的存取而不是逻辑处理。有SQL92,T-SQL或者PL/SQL语言经验的程序员,可能有能力在后端写复杂地处理逻辑。但是在后端写存储过程很难修改和维护。对于类似ORACLE的j/sql一类的语言,它们是面向对象的,有可能在不久的将来我们可以做到在服务器端很容易写商业逻辑。
服务器的本性不是交互式的。在70年代,终端把一大把数据扔到大型或者小型主机,然后等待主机在终端上显示,告诉用户有什么不妥的地方。而今,用户希望电脑提示他(她)应该输入什么样的数据,告诉他输入的数据错在什么地方,他希望在一个订单的明细输入过程中,电脑不断地告诉他目前的商品数量和金额总和是多少,而不是全部输完了,天啊,发现错了,全部重来。时代不同了!
基于服务器端的代码很难排错。缺乏集成调试器,没有观察窗和其它现代调试工具,这一切使得服务器端代码难写难维护。服务器端本身是设计用于存取数据的,现在却把维护业务规则的责任也丢给了它,它被迫使用很多CPU周期去做本不应该它做的事情,这可能会带来一些性能问题。当它忙于处理业务规则时,查询和更新数据的速度就会受到影响。
业务对象是一个方案
现在我讲到这次演讲中最有价值的部分。我相信,业务对象是一个理想的解决方案。它不影响RAD快速开发程序的自由,同时提供了服务器端代码的安全和可靠性。业务对象提供了一条既不同于胖客户又不同于胖服务器的另外一种方案。一个适当设计的业务对象增加了代码重用性,并且允许你将业务规则放在一个单一的地方。通过业务对象对业务规则的集中存储,我们可以生产容易修改和维护应用程序。
并非所有的开发者都依靠数据库服务器。很多应用程序的开发者使用文件型数据库,比如foxbase,parabox,access表,如果你没有一个后端的数据库服务器,只有这些表文件,难道你又得把业务规则的代码粘到用户界面中去?不,业务对象提供一个地方存放业务规则,并且同时也提供了一个一致的可伸缩的应用程序体系结构,不管你是用基于SQL的数据库服务器还是基于文件共享的表数据库。
业务对象模拟真实的业务活动,业务活动中的业务规则用业务对象的方法来体现。业务对象有一个无形的好处,首先它能在用户和开发者之间增加沟通。其次,业务对象提供一条途径将复杂的问题分解成较小的可以解决的部分。正常地,复杂的业务问题由一些小点的次复杂的问题组成,次复杂的问题又由一些更简单的问题组成。
当我们实行良好的业务对象设计时,大的代码重用是可能的。使用继承和专门化软件对象(雇员对象就是一个人员对象的扩展)。在对象之间协作(一个薪水册对象不用去记录员工的信息,它只是包含和使用一个员工对象就够了)和在应用程序中共享对象,我们可以减少费用,减少新的应用程序开发周期。

定义业务对象
业务对象模拟真实世界的一个实体,它结合了数据和行为。
The central activity in working with objects is not so much a matter of programming, as it is representation...(此句不知如何翻译,原文放在此处)。在面向对象的开发中,每个重要的真实世界的对象或者概念都被一个软件对象所代,该真实对象的所有的数据和行为都被软件对象所代表。(David Taylor, Business Engineering  With Object Technology, New York: John Wiley and Sons, 1995)
  如果这个对象模型正确地表示了问题域,通过给某个对象赋予模型中的某个角色,我们就可以复制该对象的行为。(不太通顺,不知如何译)

关键业务对象概念
继承
从基类型产生一个新的派生类型,派生类型继承了基类型的数据结构和行为,但同时又作了一些增强和改动。Delphi 支持单继承,这就是说,你可以而且只可以有一个基类,但是Delphi支持一个类实现多个接口(interface).

封装
封装概念就是隐藏了实现细节。换句话说,你告诉我你能做什么,不需要告诉我你怎样做到的。良好设计的对象分离了它能做的和它是怎样做的部分。举例来说,如果你有一台轿车,它有一个方法是“开始”,它自己可能要检查油泵是否打开,档位是否合适,火花塞是否有电,等等,然后跑动起来,但是,你不用管这些具体细节。只要知道它能“开始”就行了。
多态
设计具有类似操作的对象可能具有相同的公开接口,但是他们具有不同的行为细节,这些细节依赖它们所在的类。还是以前面说的汽车为例,如果将它派生类化,生成一个电动汽车,电动汽车还是具有“开始”这个方法(或叫行为),在内部来讲,电动汽车的“开始”方法有不同于非电动车的行为,例如它没有油泵,当然不会去检查油泵。多态是这样一种机制,它可以覆盖基类的执行方法从而做它自己的事情。我们可以使用多态的方式用相同的透镜去观察不同的对象。
类别化
决定什么是一个对象和什么不是一个对象。这在面向对象设计中可能是一个最困难的任务,因为没有什么快速而且可靠的方法来达此目的。实际上,不存在理想的完美的途径。迟一点我将示范如何在一个给定的问题域中找出合适的对象。
协作/对象职责
类必须是自包含的,也就是说,它们必须处理它们自己被赋有责任的功能。一个对象可以和其它的对象合作来实现它的职责。对象可以包含其它对象,也可以创建和使用其它对象。为了完成一个任务,应用程序不必一定要创建两个对象,有时候只需要创建一个复合的对象就可以达到目的。

建立业务对象的原则
这里有一些建立业务对象的原则需要用心记住。
不要基于表现层建模
要基于实际的业务来建模,而不是用户界面的具体实现。基于用户接口的对象模型非常难于在其它应用程序中复用。
开发业务对象要能独立于图形用户接口(GUI)
为确保你的业务对象独立于表现层,一个方法是考虑让你的业务对象能被非图形用户界面使用(虽然不一定有这种实际的需求)。开发业务对象的个人或者团队首先不必考虑用户界面。然而一旦业务对象有了公开的方法和属性,你可以使用依赖这些业务对象的方法和属性来建立图形用户界面。
让业务对象实施业务规则
如果业务规则不能被业务对象实施,或者不能按设计意图实施,你必须在对象上注明它将在哪里实施,以及为什么要在那里实施。一个例子是一个后台存储过程从数据库中统计了很多信息,来判断你是否能处理某件事情,由于是针对数据库统计后的简单判断,就近处理有速度上的优势,因此,放置改规则在存储过程中可能是个好的设计。当另外一个程序员在看业务对象的代码时,他就能够知道这个规则不在本对象中实施,它在另外某个地方实施,以及它在另外地方实施的好处。
不要向GUI发消息
直接向GUI发消息将使你的业务对象在应用程序中缺乏可移植性。比方说,一旦你在业务对象的代码中写下了Form1.Label1.Caption:= `保存当前工作' ,那么你就失去了任何可重用的希望,因为其它地方可能根本没有Form1.如果你需要告诉用户一些事情,或者需要用一个对话框交互,可以专门为此目的设计一个对象。这也同样允许用户定制应用程序通知他们的方式(举例来说,在输入数据的时候,很多人都不希望出现一个消息框,把消息放在状态条可能更好)。而且,当你准备设计分布式业务对象的时候,你根本没办法直接向用户发消息,只能用回叫函数。
隐藏内部实现,不让GUI知道
业务对象发布的接口描述了业务对象可以做什么。使用业务对象的客户程序不需要,大部分情况下也不应该知道业务对象的数据是如何存储的,查询功能是通过什么方式完成的。当内部的规则以及数据访问可以改变,此时,大部分的公布的接口应该不受影响。有可能在业务对象的祖先类中定义了某个抽象方法,在后代类中却有大相径庭的实现。比如,某个业务对象隐藏了关于SQL database和 foxbase的信息,在查询的具体实现中,一个使用SQL 文本方式,一个用本地文件API,但是该业务对象的客户-图形用户接口,完全不知道。因为图形用户接口是通过业务对象公布的接口来工作的,而不是基于业务对象的具体实现。
技术上的方法(非业务的)不能放在公用接口中
不要在业务对象的公开接口中放置opentables, updatecache之类的技术方法。业务对象是基于真实世界建模的,它应该隐藏技术上的方法和具体实现。除非真实世界的对象的确有一个opentables的行为,否则在业务对象的公开方法中不要包含它们。如果业务对象隐藏了它的实现细节,一个 GetCustomer(查找一个顾客)调用可能会打开一个表, 或者一个TransferAccount方法(转账)可能删除和一条记录,更新一个汇总表并且插入一条记录到另外一个表. 只是这些对图形用户界面来说,是看不见的。
靠近了-在Delphi中开发业务对象
Enough theoretical说到:现在,让我们来点实际的,怎样在Delphi中开发应用程序时使用业务对象?好的,我们开始设计吧。我先来介绍一个简单的业务问题域,后面当我们讨论建立业务对象时,它作为我们的基础。
下面我建立一个真实的面向对象的系统—银行,用这个例子来演示一下业务对象的能量。我来演示如何封装业务逻辑,并且建立一系列的类。你将看到,这些都是很典型的业务应用程序可能要做的。
我将从如何提取代表真实商业问题域的类。就面向对象开发来说,提取类别,并且把商业问题域分解到类和对象中是一件最关键的,也是一件最困难的事情。由于我们提取的类决定了应用程序的灵活性和可扩展性,恰当地划分问题域到几个不同的类中是很关键的。你所开发的应用程序的行为就由这里划分的类之间的交互来决定。当类已经定义好并且发布接口后,再来分割类的功能进行重构是很困难的。

下面是提取类的一般性原则的指引
它是不是物理实体?(账单,车辆,地理位置等等)
软件用户是否提到这个概念的名字?(电话呼叫?接见?)
是否该对象会包含其他实体?(驾驶证里面记载了一个人,但是驾驶证本身并不是人,因此驾驶证对象可能包含一个人的对象)
他是否仅仅只是一个软件开发过程中使用的对象,而不是应用程序本身的功能?(通过一个对象去查询另外一个对象在软件中常见,但不一定是一个真实世界的功能,视具体问题而定)

探索Delphi的力量
Delphi主要是一个基于关系数据库/数据集的工具。Delphi的主要能力是,首先是一个快速应用程序开发工具-RAD,同时具备良好的访问关系数据库的能力。经过良好构造的Delphi程序是严密的,快速的,灵活的,并且只需要写少量代码。

传统Delphi开发中的不足
在企业级应用软件开发过程中,Delphi的优势可能会变成一个绊脚石。由于组件丰富,可以迅速搭积木组成应用程序,导致很多人不重视前期设计。很多本来只能用于原型的代码结果直接就放到了产品代码中,把原型程序随随便便就变成了上线的程序。这样一来问题便出现了。首先,是表示层和数据访问层耦合太过紧密,快速原型开发方法导致代码可重用性非常低,而且业务逻辑被生硬地写在了表示层中。
数据感知控件加速了应用程序开发,但同时也将表示层绑在了数据集控件上。即使通过数据模块来绑定数据集仍然造成对象间的相互依赖过强。由于这个原因,表示层和数据访问层不大可能单独发布。于是,开发者倾向于干脆就在表示层中写代码,也就不打算任何重用了。另外开发中的可视化设计方式也容易引导我们在用户交互界面中写业务规则。使用可视化开发的应用程序也容易导向基于数据而不是基于方法。

Delphi中的“纯”业务对象
基于业务对象的开发首先应该是基于方法的,而不是基于数据的。当我们解耦业务对象后,它们仍然能在系统的其它地方使用。一个纯的业务对象是完全独立并且…(不会翻译,原文是:A pure business object is a fully independent and communicative entity that can be inherited),用这种方式写代码在Delphi中无疑是可能的,当然这种方式也不会充分享受Delphi的便利。这儿有一个例子,我们来看看它是什么样子的:

type Tperson = class(TObject)
  private
     fName : String;
     fBirthDate : TdateTime;
     fFather : TPerson;
     fMother : TPerson;
  protected
  public
     procedure setMother(mother : TPerson);
     function getMother : TPerson;
     procedure setFather(father : TPerson);
     function getFather : TPerson;
  published
     property Mother : Tperson read getMother
        write setMother;
     property Father : Tperson read getFather
        write setFather;
     property Name : String read fName
        write fName;
     property BirthDate : TdateTime
        read fBirthDate write fBirthDate;
     end;

注意到上面这个对象不是基于数据集的,它由真实世界建模而来,是一个纯的对象实现。这是一个人的对象,你可以询问他(她)而找到跟他相关的人,比如他的父母亲等。但是在这里没有数据集中的那些上一下,下一个,最后一个这些概念。这个人的属性照字面意义写在那儿(姓名,生日等)。由于你还得将这个人的信息存放在关系数据库中(关系数据库绝对是目前的主流),因此你还得处理对象/关系的映射(O/R Mapping).对于java和C++程序员来说,有一些比较成熟的O/R Mapping工具,但是,对Delphi来说,比较少,也不太成熟。

在Delphi中以这种写对象的方式来开发程序,面向对象是很纯了,但是又不能充分发挥Delphi最初设计给她的优势了。这个优势就是整合了高效关系数据库访问机制,和通过VCL提供了高级的数据绑定。当然你也可以为你自己的业务对象提供自己的绑定机制,通过将自己的业务对象做成抽象类TdataSet的派生类就可以达到目的。这听起来不是一个坏主意,能够免掉很多重复的手工绑定代码。用这种方式绑定单值的控件并不困难,但如果打算绑定多值控件如Grid,那真是一个挑战。
这种方式的设计又带来另外一个麻烦,当业务对象的某个值改变后,没有对象来通知你。因为你没有把界面控件注册为一个监听器,当业务对象改变后,用户界面不知道要重新从业务对象中取值并将其呈现出来。
我并不是说上面的问题没有办法解决(我自己就完成了这个任务)。但是这样一来要做很多额外的工作,软件开发周期也将被拉长。Delphi是一个可视开发工具,做这些事情对大部分人来说,似乎前景并不吸引人。
难道在Delphi中可视开发环境天生就不适合使用业务对象?当然不是,业务对象的好处能在Delphi的基本上得到实现,并且就在Delphi中,无须假借他求,就在可视化设计中就能使用业务对象。

以数据模块为中心的方案
我们需要问一下自己:“为什么要尽量写成基于业务对象的系统?”主要理由有我们需要建立一个分布式的系统,集中化业务规则,增加代码重用。Delphi给了我们高度的生产率,强大的数据库访问能力。充分利用它,我们也能达成上面的三个目标。
Delphi提供了数据模块的机制,允许我们把数据访问和业务规则放置在用户界面之外集中的地方。那么数据模块(TDataModule)是业务对象吗?既是,也不是。问题在于你的界面所围绕的数据模块里面都含有dataset控件,这个dataset已经包含了实体。经常有想设计业务对象的程序员在问:“我怎样才能不连接到一个数据集(dataset)?”概念上,他们意识到一个含有Dataset的datamodule可以作为一个业务对象。在这儿我不会讨论所有可能的问题(在这儿我提议使用datamodeul/dataet的联合体要作为我们的业务对象)。大部分时候他们(指datamodeul/dataet)都是一对一的关系,外表看起来我们的业务对象像是表格,也不存在传统对象具有的复合数据类型。
在delphi中开发业务对象,使用DataModule是一个很好的方案。为什么这样说?因为它充分利用了Delphi工具的力量,享受了OO(面向对象)的好处,却又避免对象和关系数据库映射的痛苦。Delphi开发企业信息系统的优势是可视化整合环境,强大的关系数据库访问能力。使用这个方案让你既不失去任何Delphi本身的优点,又能够使用业务对象的好处。
本次会议的主要议题主要在于使用Delphi已经提供的工具来开发非传统的业务对象。我将演示如何分离UI(用户界面)和数据访问及业务规则,用这种方式,你可以不改动业务对象切换多个不同的UI.我们将演示怎样得到最大化的代码重用和最小化重复编码业务规则。
构造基于业务对象的应用程序
问题域的提出
在这次会议中,我将使用银行和账号作为我们的业务对象来作为一个简单的例子。我挑选这个例子的理由是它能够体现出很多开发基于业务对象的系统的原则。我们将使用Delphi构造这个程序,它能够建立各种不同的能取款和存款的金融账户。
设计Tbank对象
做任何好的设计工作的第一步是要确定你期望达到什么目的。谁是系统的操作者,什么样的规则将实施。这个背景很简单,并且我将通过限制我们的讨论范围在两个主要实体,银行和账户来进一步简化它。我们来建模两种不同的活期账户,它们的基本信息是差不多的,但是存款和取款的规则不一样。
银行系统业务对象
银行,银行是账户的管理者。银行有责任建立新账户,管理账户的访问,比如查一查某个人的账号是多少。
支票账户(Checking Account):这是一种取钱不受约束的账户类型,它不限定每次取钱的上限,也不限定某段时间的交易次数。它还能挂钩一个储蓄账户来透支,这种账户没有最小余额规定。
储蓄账户(Savings Account):这种类型有最小余额限制,并且每月取款不能超过两次。
让我们首先鉴别这两种账户的共性。在多个对象之间发现共性的过程叫泛化。两个账户都必须知道当前的余额,并且都有存取款的功能。在取款方面,它们同样有一些规则要处理。我们观察这些对象,把它们的共性行为放到一个更高层的类别中,称为超类。我们就为这两种账户建立一个超类,就叫账户,把它们共同的方法和属性放进“账户”中。

用例
在面向对象设计的世界里,用例(use cases)非常重要。为了系统正常工作,有一些场景我们必须要解决。很多程序员在这个时候往往会说“给代码我看看”(我有时也会这样!),但是,为了测试我们是否达到了目的,有一个好的测试计划针对企业级系统开发来说是非常重要的。用例就是一个测试场景。
建立新账户,我们必须能够建立新账户。
找出一个账户。我们需要通过一个人的姓名或者身份证号码等信息来找到一个账户,并且提取该账户的相关信息。
存款 我们要能够在账户上存款
取款 不管哪一种类型的交易(签支票,ATM,购物刷卡),基本的作用是一样的。
转账 在支票账户余额不够,打算透支取钱时,将出现从储蓄账户暂借给支票账户的情形

对象设计
我们首先要识别出对象的数据成员和方法,在你定义数据成员时,可能会发现,一些其它的对象必须包含在你的类图(class diagram)中.数据成员和方法描绘出这个对象是怎样的,而如何做到的却是业务对象的私有和保护成员和方法要干的事情。如果你能成功地隔离业务对象的公用接口和实现,你就可以在不影响接口的情况下修改你的实现。在分布式对象环境下,要特别注意不能轻易修改接口,因为你很难确定有哪些应用程序在使用你的业务对象。

用用例来检验对象设计
我们的对象模型必须能够解决那些在用例图中出现的问题,基于对象的系统是被设计成对话式的,这就是说,在对象之间对话讨论的是哪个对象可以完成甚么样的业务功能。我们来讨论一个通过支票账户取款的过程,在这个过程中,需要从其他的透支账户转帐。
方法CanIWithDraw()和getOverdraftAccount()不在我们最初的设计模型中。在一个序列图的调用过程中,为了完成这个任务,我们发现还需要其他的方法。另一个感兴趣的是我们已经说过,对于储蓄账户,我们取款时有频度限制和数目限制。因此在转帐前,储蓄账户同样有CanIWithDraw()方法,来判断此次交易是否被允许。看看下面的序列图,我们发现需要改变原来的对象图(应该是类图,译者)。后面有新的改进过的类图(原始来源没看到)。

设计数据访问
在我们的设计中,必须仔细考虑数据访问部分。映射对象到关系数据库主要有三个不同的策略。我们首先要确保对象图是符合真实世界的,举例来说,这个应用程序的界面中不应该有个‘xxx.exeSQL’的方法,而应该代替为一个‘getcustomer’的方法。这个’getcustomer’的方法可能要加载三个query的方法,并且传回一个variant类型的数组。调用业务对象的程序部分不需要知道是怎样取得这个客户实体的,取得这个客户实体是业务对象的职责。
映射关系结构到对象
我们现今的MIS开发差不多都要用关系数据库来实现持久化。不幸地,不管是基于SQL的数据库服务器还是文件共享型的Access,dbf等都不支持继承。试着考虑一个例子,存储厂商对象信息的表结构和存储管理人员对象的表结构肯定不一样。一位著名的计算机权威曾经说过,“把一个对象存在关系表格中,就像把汽车开进车库,然后把零件都拆开放在货架上,下次你开车出门时再拼装好”。面向对象的数据库还没有渗透主流市场。你必须决定一个策略来解决这个“阻抗不匹配”的问题。下面有三个基本的映射对象结构到关系结构的策略:
属性全部上移
只要是同一个祖先的对象,都存放在一个表中,只要是任何一个派生类有的属性,都在这个总表中存在。这种方式在查询时你得写代码区分不同的派生类型。在我们的银行例子中,如下一个查询:“把账户余额超过10元的都列出来”,电脑就查找一个表,不管是甚么账户,只要满足条件的都显示出来。这种方式有一个明显的不利之处-浪费了存储空间,没办法优化。另外就是结构不清晰,试想一下,一个拿佣金的销售员在表格中的计件薪水标准那一栏完全没意义。这种映射方式在类继承数层次不深的情况下比较简便。
属性全部下移
此种方式就是不设总表,所有叶子级的后代对象都单独做一个表,每一个这样的表中都包含了该对象的所有的属性。这种方式最容易理解和写代码,很多人也凭直觉这样做。在我演讲的例子中也提供了一个简化的例子。支票账户具有所有账户的属性,而现金支票账户具有所有账户和支票账户的属性。这种方式也有不利之处,如果查询“列出所有支票账户和它的子代类型的账户”,就很不方便了。不过,整体来说,这样映射还是最容易的方法。
建立多表一对一
这种方式就是尽量映射对象的层次关系,派生类对象在祖先表和本身的表中都有一条记录。每个对象都具有的共同属性(字段)放到祖先表中,每个层次的子对象都在祖先表中占一条记录。然后子对象有另外存放一条记录在自己的表中,这个表只有自己独有的属性(字段)。这种方式具有上面两种方式的优缺点,可以减少存储空间,可以比较方便的查询某类及其派生类,缺点是也同样不属于规范化的关系。你在查询和修改插入时都得对一个以上的表进行操作。
虽然麻烦,最合乎逻辑的还是第三种方式。在下面的例子中,字段BizObjID在它所有的表中都是一个唯一的主键,它将成为一对一关系的链接纽带。你可以用下面的方式取得这个现金支票账户的所有属性。更新表有点麻烦,因为得同步更新一个以上的表。
select Account.*, CheckingAccount.*, MMChecking.* from Account,
CheckingAccount, MMChecking where
     Account.type = 'Checking' and
     Account.ID = CheckingAccount.ID and
     MMChecking.ID = CheckingAccount.ID
(注意以上联接方式不是新标准,虽然能通过,译者注)

实现业务对象
特殊化TdataModule对象
Tdatamodule组件建立业务对象的一个理想的组件。你能够可视化地设计数据访问途径,并且还可在运行时再绑定数据感知控件。由于Tdatamodule运行时是不可视的,它强迫训练程序员把数据访问和业务规则排除在用户界面之外(我见过很多程序用了datamodule,但是还是将业务逻辑写在了dbgrid的onexit事件中,译者)。Tdatamodule还有一个另外的好处是,它已经有方法实现了IDataBroker 接口,当你开发分布式业务对象时,这个很重要。它同样允许我们使用可视化方式开发,而且可以使用事件驱动机制。不只如此,TdataModule 还可以被继承,对我们开发业务对象来说,真是太方便了。
Datamodule提供了一个正式的机制,包装那些数据访问控件,让那些不那么熟悉类之间错综复杂的关系的面向对象的设计和编码方式的程序员,也可以写出自己的“业务对象”。Datamodule也可以编译成DLL,或者做成COM/OLE servers,从而非Delphi的程序也能用Datamodule.

建立一个抽象的基类业务对象
一个基类业务对象可以让你扩展Delphi的可视化开发能力,你可以将你的可视化控件绑定到你的业务对象上,而不是通常直接绑定到数据集。(在本文的例子中,实际上是绑定在业务对象中的数据集上)。基类业务对象让你集中一些在派生类共同的行为并且以一致的方式来处理(应该是多态的)。

Unit base;
interface

uses
   Windows, Messages, SysUtils, Classes, Graphics,
   Controls, Forms, Dialogs, Db, DBTables;
{
This is the base class for all business objects. This contains behavior that you want to be common in the system as a whole.In this example we will be just putting some simple locking mechanisms. Other things that we may want to put in this object could be validations, common behavior for locking etc. We will place our common table object on this so we have a point of reference for the other objects to work off.
}
type
   TBizObj = class(TDataModule)
        q_data: TQuery;
        procedure q_dataAfterPost(DataSet: TDataSet);
        procedure q_dataAfterCancel(DataSet: TDataSet);
        procedure q_dataBeforeEdit(DataSet: TDataSet);
        procedure q_dataBeforeDelete(DataSet: TDataSet);
        procedure q_dataBeforePost(DataSet: TDataSet);
        private
        public
   end;
var
   BizObj: TBizObj;

Implementation

{$R*.DFM}
procedure TBizObj.q_dataAfterPost(DataSet: TDataSet);
begin
   // release our optimistic lock
end;
procedure TBizObj.q_dataAfterCancel(DataSet: TDataSet);
begin
   // release out optimistic locks
end;
procedure TBizObj.q_dataBeforeEdit(DataSet: TDataSet);
begin
   // put code here to place an optimistic lock on this
   // row in the table
end;
procedure TBizObj.q_dataBeforeDelete(DataSet: TDataSet);
begin
   // check the optimistic lock for contentions
end;
procedure TBizObj.q_dataBeforePost(DataSet: TDataSet);
begin
   // check the optimistic lock for contentions,
end;
end.

定义一个公用接口
一旦你派生类化了一个Tdatamodule,你必须在这个派生类中定义公共接口。这实际上是一个很简单的练习,因为我们在定义这个类的时候已经做了分析。公共接口包括了可以被别的对象操作的方法和属性。在这个数据库应用程序的例子中,数据成员可能主要是Tfield的后代类,通常是通过定义Dataset类加入到业务对象中。
type
        TAccount = class(T_BizObj)
          private
                { Private declarations }
          public
           procedure withdraw(amount : float);
           procedure deposit(amount : float);
           procedure transferTo(amount : float,
                toAccount : TAccount);
           function canIWithDraw(amount: float) : boolean;
      end;

实现数据访问
现在,你要为你的TAccount类设计公共数据访问方法,我们需要实现基本数据访问。还记得这个TAccount类是抽象的吗?它不能被实例化,只有它的后代类才可能。我们打算用第三种数据库映射方式来存储我们的Account,因此我们的表结构看起来像下面这个样子:
Account
  Nbr: int
  Amount : double
  AccountHolderName : Char(40)
  DateOpened : DateTime
  DateClosed : DateTime
  Descr : Char(80)

CheckingAccount
  Nbr
  OverdraftAccountNbr

SavingsAccount
  Nbr
  MinBalance

实现数据访问就像以往写的代码一样,唯一不同的就是我们现在在“业务对象”中写访问方法。你可能想要一个方法去得到你感兴趣的账户。但是,在一个Account对象中出现多个账户是不合法的(真实世界不会这样)。正确的类交互方式应该是请求Tbank对象按条件查询账户,返回一些Account对象。下面是我们的账户对象,它包含了一些数据访问方法。

我们打算利用SQL 数据库的查询,我们需要一个参数化的查询来从服务器得到我们需要的账户资料。

我们同样需要在账户对象上一个方法来打开账户,我们的Account对象的公共接口方法看起来如下面所示:
type
   TAccountBO = class(TBizObj)
   private
     { Private declarations }
   public
      procedure getAccount(AcctNbr : integer);
      procedure withdraw(amount : double);
      procedure deposit(amount : double);
      procedure transferTo(amount : double; account : TAccountBO);
      function canIWithDraw(amount : double): boolean;
   end;

在这个getAccount( )方法的实现中,我们需要在Account对象中打开适当的账户数据
procedure TAccountBO.getAccount(AcctNbr : integer);
begin
   if (q_data.Active)
      begin
         // check to see if we can close this account
      end;

   q_data.close;
   q_data.parambyname('Nbr').value = acctNbr;
   q_data.open;
   // check to see if there is a record, if not raise an error
end;

建立我们的派生类TCheckingAccount, TsavingsAccount
对于每个特殊的账户类型,我们需要它们创建合适的具体的派生类,它们实现自己特殊的行为,规则和数据访问.为了避免陷入太多的细节,我仅仅重点讲一下派生类部分.

建立自己的数据访问,依赖你自己的数据访问策略,你可能要加入相关的数据访问控件或者改变Taccount的默认方式.在你的业务对象中还要放入数据验证方法.因为你建立的是一个新的类型,因此完全可以放置特殊的验证(可能用事件驱动方式,当某件事情发生是,在事件相应中执行),也可能有自己外在的控制行为,比如一些基类不能做在派生类中可以做的事情.

为这个业务对象建立用户接口,有可能这个派生类跟它的基类对用户接口有不同的考虑.

建立 TBank 类
Tbank是管理所有账户的类别.由于时间和空间关系,我不打算真正实现一个完整的Tbank类的构造(如果这样的话,我们有很多的细节问题要处理).在这个简单的例子中,Tbank对象可以提供TAccount对象的列表,并且可以打开和维护账户.由于空间关系,Tbank的完整代码我没有列出来.

实现私有的和保护接口
当我们定义类的属性和公共方法之后,我们需要实现它的功能和业务规则. 不要把数据成员变量放在类的公共接口中是一个好的习惯.Delphi的”属性”可以提供透明的数据存储,你仍然可以设置和访问这个数据成员.另一个好的习惯是将属性的读写方法设置为virtual(虚方法),如果派生类需要,可以覆盖它.

把业务规则放进事件里面
一个问题我们必须意识到,当我们在Datamodule中把一个控件放进去的时候,这个控件的指针就是公共的(本来是published,是另外稍有点不同public,译者),其它引用了这个单元的方法都可以访问这个控件,有可能会避开我们潜心设计的访问方法,为了防备这一点,我们要尽可能地把业务规则放进数据集控件的事件中.
   比如有一个地址类,其中有邮政编码一栏,假设业务规则规定这一栏必须不能为空,并且只能用正确的格式书写.这个地址对象本身如果仍被其它的对象引用的话,它也不能被删除.另外当一个新的地址增加时,应该提供一个默认值.我们最好将这些规则放到地址类的事件中.

使用约束
Delphi给我们一个新的地方来存放业务规则-约束.约束是当数据被提交到数据集的时候的一个检验. 检查约束包含了从数据字典或者远程数据库服务器导入的check constraint 的SQL语句.在Delphi企业级开发中,需要使用远程数据模块,而约束在远程数据模块中扮演一个很重要的角色.后面还会讲到.
约束被写在SQL中,当违反时能给自定义的信息.这里有一个Ttable对象的约束屏幕(译者在来源文章中没看到图片).
约束确保这个邮政编码不为空,如果为空,则不能post到数据集中.

数据验证
使用Tfield 对象来做数据验证.Tfield对象提供了一个理想的地方来验证数据和执行业务规则,因为所有的字段值的改变都必须通过Tfield对象来进行.当字段的值被改变时, OnValidate事件将被触发.如果你在OnValidate事件中写下”abort”的代码,那么改变将被放弃,当值一旦改变时,这个事件就被激发了,因此是一个提供验证绝好的地方.不过,还是会有些问题.举例来说,用户在OnValidate中输入一个非法值,OnValidate事件被调用了.你已经知道这个值是非法的了,然后通知用户放弃并退出编辑,但是实际上限制了用户离开这个字段.当另外一些字段的值在某个错误的范围时,当前这个字段的值是正确的,但是用户只能傻傻地定在当前字段,想去改变错误的字段都没法离开.你不能改变另外的字段,因为另外的字段依赖于当前的字段的值.
你知道这个值是错误的,你也通知了用户但是没法终止.用户没法回头更正这个问题,然后错误的值被存到了数据库中.
你知道这个值是错误的,你也通知了用户.在你post之前,你设置了一个全局变量来终止OnValidate事件.然后你再调用OnValidate事件.如果它发现验证通不过,给用户一个信息然后放弃post.这个方法提示用户一个正确的处理方法,让用户可以继续运行程序,在再次调用post前再验证.已经调用过验证码了,再次验证会带来一些性能上的影响.同样,当用户再次post时,被提示的还是第一次的信息,它们更正了这个问题又可能碰到另外类似的问题.
你知道这个值是错误的,你也通知了用户.然后你在这个类中设置了一个私有变量来表示你在post之前还要回头验证这个变量.这个方案是最好的,但是在所有的业务对象中都做到这一点要多做不少的工作.

建立一个表现层

基于控件的GUI
一个可能的途径去设计用户界面是设计你自己的对业务对象有数据感知能力的界面控件.如果你打算在应用程序的多个部分都重复使用自己各个不同的界面组件,但是又不想相同的界面多次出现.或者你只想改变自己的程序的外观感觉不想改动太多地方, 这是个好主意.下面有个小的话题是关于建立基于组件的用户界面的.

为控件建立默认的GUI界面
这个例子显示了一个简单的地址编辑器演示了这个好处,那就是绑定一个业务对象到一个默认的图形用户接口上以便用户能操作和控制业务对象.业务对象的感知控件TaddressEdit是TcustomPanel的派生类,它绑定了一个业务对象Addressable. 开发一个控件能够绑定业务对象可以提高生产力,并且可以通过改变这个控件来改变应用程序的外观.如果这个控件是放在package里面,你要改变外观甚至都不用重新编译,把那个package换一下就行了.下面有TaddressEdit的代码.

你可能注意到仅仅增加了一个公共方法,这个方法让你能够绑定它到一个业务对象.它将这个抽象的Taddressable业务类作为参数.这样这个控件就能用于多个不同的业务类了.

type
TAddressEdit = class(TCustomPanel)
private
      ds : TDataSource;
      dbAddr1, dbAddr2, dbCity, dbState, dbZip, dbPhone, dbApt :
TDBEdit;
      lAddr1, lAddr2, lCity, lState, lZip, lPhone, lApt : TLabel;
   protected
      constructor create(AOwner: TComponent); override;
      destructor destroy;
      procedure loaded; override;
   public
      procedure BindAddressable(BO: TAddressable);
   published
end;

为了把这个控件挂上业务对象,你只需要像下面这样做:
procedure TForm1.Button1Click(Sender: TObject);
begin
   AddressEdit1.BindAddressable(Addressable);
end;

基于Form(窗体)的 GUI
一个更通常设计Delphi程序的界面的途径是将它们做成Froms.这种方式允许你使用Delphi自带的所有的可视化控件工具,然后和迅速地建立用户界面.再说一次,我主要用这种方式来建立我的应用程序,因为这种方式能充分发挥它的优势—可视化设计.就像你平常建立应用程序一样就可以了.为确保你没有把GUI硬扯上业务规则,小心在GUI事件中的编码.这将迫使你中心化你的业务逻辑,并且在业务对象中为业务逻辑提供一个公共接口.

GUI--Midas物理三层结构
把业务对象移到三层结构的中间层去.业务对象移到第三层包括具体的数据访问业务规则都是在一个应用服务器上进行的.仅仅是最近,把功能分解到多层的开发方式才能被普通的程序员理解和实行.微软在windows操作系统中加入了DCOM, Netscape 也在新版本的Navigator 中加入了分布式的ORB.不久事实上会出现高级通讯协议,来代替目前开发中使用混乱的通讯协议和库文件.

物理三层的优势
通过多台机器分开数据处理能提高好的伸缩性.
简化客户端的配置,不需要高性能的客户端.
容易分发应用程序
比逻辑上的三层更少问题.
为你的业务对象提供容错保险.

物理三层中的GUI
把中间层的业务对象绑定到客户端与逻辑上的三层类似,但是有一些问题还是不同于逻辑上三层的胖客户,包括相应能力,用户反馈,灵活性,下面具体论述:

用户验证的立即反馈,为了使中间层的响应速度接近胖客户的业务对象,从客户来的数据必须有它的主要放置地方,就是中间层业务对象.任何存在客户端的数据都只能使中间层业务对象的一个缓存.这样规则可以很快触发,用户能迅速得到反馈.一个典型的不能正常工作的情形是CGI,你在Form中填入了数,然后提交,如果有什么问题,CGI程序提示你,然后你再重来.如果你去银行申请贷款,这样也许能够接受,如果你整天和这样的程序工作,会很难受.通常Gui界面是由业务逻辑控制的,为什么发票的保存按钮是灰色的?原来是因为总数不等于…你只好发呆了.通常Gui的控制是标准的(如果数据集不处于编辑状态,你不能按保存按钮),但是恰好这些变化是由业务对象的状态所控制的.这也是为什么业务逻辑一定要编码放在一处地方,而且不能违背(不管是在业务对象中还是在数据库中的存储过程和触发器),并且对每一个用户的屏幕来说,都是友好的合理的,这就是理由.

选择和挑取
通常一个picklist的内容是由一个对象的状态或数据决定.如果你有一个person对象,你想找出他(她)的配偶,你只能挑选那些注明了已婚的人,他的配偶和他是不同性别.这是一处界面与业务规则有关的例子,不过,人们通常会忽略这也是业务规则,但它的确是.问题在于哪里是业务逻辑,我们能否以这种方式合成我们的系统,而业务逻辑不需要重复编码.

让业务对象回叫用户界面.常常很有必要让业务对象来问用户界面一个问题.这是因为业务对象不能预计到用户界面的每一种情形.怎样让业务对象回叫用户界面的确是一个问题.在这儿需要回叫函数让业务对象提示用户一些目前的状态,用户违背了什么样的规则,以及其它一些重要的信息.

这儿没有对上面问题的最好回答.你的应用程序必须有一个体系结构,必须能够解决这些问题.构建这样的应用程序结构是比较花时间和金钱的.但是目前显然没有在Delphi环境下商业成品化的软件能解决以上所有的问题.因此做你自己的应用程序框架设计也许是必要的.

CGI
由于你始终是通过业务对象的公共接口来对它进行访问的, 很有可能你打算给它提供一个html的前端接口.这个章节有一个这样的例子(在哪里?我没看到)

潜在的缺陷
计划丢掉一个—原型的重要性
对象和对象之间的交互是个很棘手的东西.如果你第一次或者做了几次没有成功,不要气馁.做一个用来验证的原型是很重要的,有时候我们纸上谈兵一点问题都没有,到了实际未必能正常运行.计划为你的业务对象写一个原型吧,然后扔掉它.你的第一原型将会给你提出很多要解决的问题.
业务对象设计
基本上,在RAD开发中,设计就是写代码.严格地面向对象开发要有分析和设计阶段.没有经过良好设计的对象继承层次将会十分难用,搞乱人的思维并最终被丢掉了事.在开始编码之前请花点时间来设计你的类吧.

寻求帮助—业务对象开发是一门艺术,不是一门科学
面向对象编程同过程式编程不是同一种技能,它要求不同类型的方法和设计思路.对过程式程序熟悉的程序员不是那么容易转换过来的,更加不可能突然学会.寻求帮助吧,这些帮助可能来源于大师们的方法论述,来源一些专业杂志,来源于一些专家的讨论会.

为重用而建,但没有人重用
我发现刚开始带领我们的团队进入面向对象领域时,我们有很多的玩笑是关于怎样写可重用对象的(他们没想怎样重用别人的东西).人们必须给那些开发可重用对象的作者付给报酬.那些专业组件写作者的作品可以被别人派生后作为自己的业务对象,其质量远远好过不专业的人随手涂鸦的东西,这些人应该得到尊重.组件开发者需要人们购买他们的东西,并且要实际使用组件.

业务对象开发是为长期而准备的
开发和使用业务对象在初期并不会节省你任何时间,但它们会产生长久的效果.直到你积累了相当多的业务对象后,你才会从业务对象中获得好处.

结论
对于我们各种不同的任务,Borland公司一贯地提供我们需要的工具.它让我们有可能更容易写分布式的业务对象.如果你想开发具有灵活性和伸缩性的企业级系统,在开发过程中,所有的物理的,逻辑的多层体系都应该考虑一下作为你的应用程序体系的选择.

建议读物
Booch, Grady. Object-Oriented Analysis and Design with Applications, 2nd ed. (Addison-Wesley, 1994).
Booch, Grady. Object Solutions:Managing the Object-Oriented Project, (Addison-Wesley, 1995)
Taylor, David A. Business Engineering with Object Technology, (John Wiley & Sons,1995)
Fowler, Martin. Analysis Patterns: Reusable Object Models, (Addison-Wesley, 1996)
Gamma, Erich, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software,
(Addison-Wesley,1995)


译后记:
这篇文章是在大富翁上一个问题的解答中()找到的,部分代码缺乏,所有的图片都没有,本身原始来源应该是(from Carl Hewitt’s paper “Developing and Deploying Business Objects in Delphi”, http:// www. oop. com/ white papers).这个网站现在上不了.这个老外的母语好像也不是英文,句子太长,我的英文也烂,翻译得磕磕碰碰的,最明显的是在关于OnValidate事件的描述那部分.
   这篇文章给我们还是带来了一些启示,虽然有时候我很想看他的业务对象的例子,这个时候他又说不想陷入细节,又说时间和空间不允许等等.尽管如此,我还是能得到收获.
   TdataModule只是Borland给开发者的一个提示,其实你完全可以用正常的Form来作为你的DataModule,关键是要有把界面和业务逻辑在某种程度上分开的考虑.简单地说,就是要多用Tdatamodule,一个程序可以用很多个,即使一个窗体对应一个也不是不可以.能够写在Datamodule中的方法不要写在界面窗体中.能够写在DataSet中的事件,不要写在dbedit,dbgrid中,如果你哪天要换更酷点的控件不是要重写事件?在Dataset中就不会.还有可以把一些过程封装在Datamodule中,不要在Form1中写一大段的话,就是为了取某个数,不停地说DM1.DATASET1.xxx,DM1.DATASET2.xxx… 其实可以做一个函数作为DataModule的对象方法,给Form1调用得到返回值就好了.这样做违背了面向对象一个原则:不要和陌生人说话,DM1对Form1来说,是被使用的对象,是熟人没问题,DataSet1,DataSet2对Form1来说,就是陌生人.有时返回值也可以用dataset的指针(Delphi中比较少用显式指针,但是对象引用都是隐式指针)来传递,不过要注意正确地create和Free.
   另外Delphi中的业务对象基本还是体现在DataSet中,表模式驱动呗.业务逻辑也主要放在DataSet的事件中.想用纯的面向对象,还是用java吧.
   我个人觉得,用表模式配合Delphi的BDE数据访问,特别是用Tquey和TUpdateSQL,更新数据十分方便,不像vf和pb,vb,哪些数据更新了,如何更新的,还要用循环语句自己去判断,在这里Query全部给你做好了,你只要在onupdaterecord中写事件就好了,管你是不是join的表,管你要不要同步更新库存和会计凭证,全部解决.可惜很多人都不知道,包括那国内经常出书的人.扯远了!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值