DA11 – 深入业务规则(Business Rules)
来源:互联网 发布:serv u ftp软件 编辑:程序博客网 时间:2024/05/20 13:04
DA11 – 深入业务规则(Business Rules)
综述
业务规则(BRs)是一个包含业务逻辑封装的概念,从前台的终端用户接口及后台的终端数据库分离处理. Data Abstract通过使用业务帮助类(Business Helper Classes)实现了这个概念.
Delphi的属性/方法/事件范例是一个好原型但是对于实际开发却不总是适用.问题是事件处理与窗体或数据模块绑定.看DA06 文档查看更多关系这个话题的详细信息.
通过如下组件的事件处理,业务规则的逻辑可以在客户端及服务器初始化:
- TBusinessProcessor –服务端的数据集规则
- TDataTable – 客户端的数据集规则
- TDataTable.Field – 客户端的字段规则
我们需要一个更够将这些事件处理移到业务层的过程,本文将集中与此.
本文提供一个指南展示如何向已经存在的项目中增加这三个类型的规则.这里将基于自带的存取Northwind数据库的Customers表的CalculatedFields 范例.
在本文的底部将提供下载代码的连接.文件连接包含'before' 和 'after'项目,允许你用最初版本与最终的解决方案做比对.
强类型单元
虽然使用提供的向导去创建强类型单元不是绝对必要的,但是我们推荐用这种方式为你生成业务规则架构.
- 拷贝Samples/CalculatedFields目录下的所有文件到一个叫做Strongly Typed的目录.
- 打开CalcFields.bpg (或 CalcFields.groupproj)并在项目管理器中将其命名为BusinessRulesExample (通过Save Project Group As).
- 确保服务端项目 (StronglyTypedServer.Exe)是项目管理器的默认项目.
- 打开 CalcFieldsService_Impl 右击 Schema:
点击上图指向的菜单项.你需要在向导中输入两个单元名字.这里使用默认的SchemaClient_Intf.pas和 SchemaServer_Intf.pas. 提示: 这时这两个单元文件已经加入到服务端项目.
那么,这两个单元文件提供了什么功能呢?首先,我们查看SchemaClient_Intf.pas.这个单元提供了所有Customers数据集字段有效的Getter和Setter方法. 这种代码太多,为了便于理解我们抽取与ClintCalculated自动相关的代码(空的构造或构消方法也被省略):
unit SchemaClient_Intf;
interface
uses
Classes, DB, SysUtils, uROClasses, uDADataTable, FmtBCD, uROXMLIntf;
const
{ Data table rules ids
Feel free to change them to something more human readable
but make sure they are unique in the context of your application }
RID_Customers = '{17318140-A122-4D1F-B41C-CD93E0E64AA7}';
{ Data table names }
nme_Customers = 'Customers';
{ Customers fields }
fld_CustomersClientCalculated = 'ClientCalculated';
{ Customers field indexes }
idx_CustomersClientCalculated = 2;
type
{ ICustomers }
ICustomers = interface(IDAStronglyTypedDataTable)
['{8FB0F228-C1A1-40A7-9895-3D476F56B03A}']
function GetClientCalculatedValue: String;
procedure SetClientCalculatedValue(const aValue: String);
function GetClientCalculatedIsNull: Boolean;
procedure SetClientCalculatedIsNull(const aValue: Boolean);
{ Properties }
property ClientCalculated: String read GetClientCalculatedValue
write SetClientCalculatedValue;
property ClientCalculatedIsNull: Boolean read GetClientCalculatedIsNull
write SetClientCalculatedIsNull;
end;
{ TCustomersDataTableRules }
TCustomersDataTableRules = class(TDADataTableRules, ICustomers)
protected
function GetClientCalculatedValue: String; virtual;
procedure SetClientCalculatedValue(const aValue: String); virtual;
function GetClientCalculatedIsNull: Boolean; virtual;
procedure SetClientCalculatedIsNull(const aValue: Boolean); virtual;
{ Properties }
property ClientCalculated: String read GetClientCalculatedValue
write SetClientCalculatedValue;
property ClientCalculatedIsNull: Boolean read GetClientCalculatedIsNull
write SetClientCalculatedIsNull;
end;
implementation
uses Variants;
{ TCustomersDataTableRules }
function TCustomersDataTableRules.GetClientCalculatedValue: String;
begin
result := DataTable.Fields[idx_CustomersClientCalculated].AsString;
end;
procedure TCustomersDataTableRules.SetClientCalculatedValue(const aValue: String);
begin
DataTable.Fields[idx_CustomersClientCalculated].AsString := aValue;
end;
function TCustomersDataTableRules.GetClientCalculatedIsNull: boolean;
begin
result := DataTable.Fields[idx_CustomersClientCalculated].IsNull;
end;
procedure TCustomersDataTableRules.SetClientCalculatedIsNull(
const aValue: Boolean);
begin
if aValue then
DataTable.Fields[idx_CustomersClientCalculated].AsVariant := Null;
end;
initialization
RegisterDataTableRules(RID_Customers, TCustomersDataTableRules);
end.
从上面可以看到,这里只包含ClientCalculated字段相关的代码,这个单元中还有所有字段的相似的内容.
注意在Initialization节中调用RegisterDataTableRules,使这个ICustomers接口的实现可在运行时使用. RID_Customers 的值(如 '{17318140-A122-4D1F-B41C-CD93E0E64AA7}')可以用于向DataTable的BusinessRulesID属性赋值. 可能这不是首选的解决方案.在单独的单元中创建TCustomersDataTableRules子类可以使我们写的代码与这些自动生成的单元分离.
如果这样, RegisterDataTableRules 将需要关联到TCustomersDataTableRules的子类. 你将在本文的下一节看到如何这样做(以及如何使用这个类提供远程数据集事件处理,例如将其事件处理与窗体/数据模块分离).
现在看一下SchemaServer_Intf单元. 为了清楚我们再次将ClientCalculated相关的代码抽取出来:
unit SchemaServer_Intf;
interface
uses
Classes, DB, SysUtils, uROClasses, uDADataTable, uDABusinessProcessor, FmtBCD,
uROXMLIntf, SchemaClient_Intf;
const
{ Delta rules ids
Feel free to change them to something more human readable
but make sure they are unique in the context of your application }
RID_CustomersDelta = '{37A04F4D-3C5A-4F30-B916-C430A873DD1A}';
type
{ ICustomersDelta }
ICustomersDelta = interface(ICustomers)
['{37A04F4D-3C5A-4F30-B916-C430A873DD1A}']
{ Property getters and setters }
function GetOldClientCalculatedValue : String;
{ Properties }
property OldClientCalculated : String read GetOldClientCalculatedValue;
end;
{ TCustomersBusinessProcessorRules }
TCustomersBusinessProcessorRules = class(TDABusinessProcessorRules,
ICustomers, ICustomersDelta)
protected
{ Property getters and setters }
function GetClientCalculatedValue: String; virtual;
function GetClientCalculatedIsNull: Boolean; virtual;
function GetOldClientCalculatedValue: String; virtual;
function GetOldClientCalculatedIsNull: Boolean; virtual;
procedure SetClientCalculatedValue(const aValue: String); virtual;
procedure SetClientCalculatedIsNull(const aValue: Boolean); virtual;
{ Properties }
property ClientCalculated: String read GetClientCalculatedValue
write SetClientCalculatedValue;
property ClientCalculatedIsNull: Boolean read GetClientCalculatedIsNull
write SetClientCalculatedIsNull;
property OldClientCalculated: String read GetOldClientCalculatedValue;
property OldClientCalculatedIsNull: Boolean read GetOldClientCalculatedIsNull;
end;
implementation
uses
Variants, uROBinaryHelpers, uDAInterfaces;
{ TCustomersBusinessProcessorRules }
function TCustomersBusinessProcessorRules.GetClientCalculatedValue: String;
begin
result :=
BusinessProcessor.CurrentChange.NewValueByName[fld_CustomersClientCalculated];
end;
function TCustomersBusinessProcessorRules.GetClientCalculatedIsNull: Boolean;
begin
result := VarIsNull(
BusinessProcessor.CurrentChange.NewValueByName[fld_CustomersClientCalculated]);
end;
function TCustomersBusinessProcessorRules.GetOldClientCalculatedValue: String;
begin
result :=
BusinessProcessor.CurrentChange.OldValueByName[fld_CustomersClientCalculated];
end;
function TCustomersBusinessProcessorRules.GetOldClientCalculatedIsNull: Boolean;
begin
result := VarIsNull( BusinessProcessor.CurrentChange.OldValueByName[fld_CustomersClientCalculated]);
end;
procedure TCustomersBusinessProcessorRules.SetClientCalculatedValue( const aValue: String);
begin
BusinessProcessor.CurrentChange.NewValueByName[fld_CustomersClientCalculated]:= aValue;
end;
procedure TCustomersBusinessProcessorRules.SetClientCalculatedIsNull( const aValue: Boolean);
begin
if aValue then
BusinessProcessor.CurrentChange.NewValueByName[fld_CustomersClientCalculated] := Null;
end;
initialization
RegisterBusinessProcessorRules(RID_CustomersDelta,
TCustomersBusinessProcessorRules);
end.
这里有几个问题值得关注:
- ICustomers 接口被扩展用于传输到服务端的Delta包中的子项.你将发现在本文后面这些很有用.
- 接口的实现基于BusinessProcessor而不是 DataTable.
- 如果你刚接触这些接口,查看TCustomersBusinessProcessorRules的声明,以及如何明确的以ICustomers和ICustomersDelta类型引用.
客户端业务规则
客户端规则的主要意图就是将DataTable的事件处理代码从窗口或数据模块中移除,同时封装为业务逻辑以便于重用.关注 tbl_Customers 的事件处理 (在 CalcFields_ClientData中):
我们需要转移OnCalcFields事件处理并作为一个业务规则.如上所述,我们将在单独的单元中创建一个TCustomersDataTableRules 的子类.首先,我们在项目管理器中通过拖动的方式将SchemaClient_Intf 加入到客户端项目:
下一步,保证CalcFields_Client是项目管理器中的当前项目并加入一个叫做BizCustomersDataTable的新单元. 输入如下内容:
unit BizCustomersDataTable;
interface
uses SchemaClient_Intf;
type
IBizCustomers = interface(ICustomers)
['{F9D78080-2B61-44D2-9148-C8D53329A08F}']
end;
TBizCustomersDataTableRules = class(TCustomersDataTableRules, IBizCustomers)
end;
implementation
uses uDADataTable;
initialization
RegisterDataTableRules('CustomerClientRules',TBizCustomersDataTableRules);
end.
注意: IBizCustomers 接口不是我们当前任务实际需要说明使用的业务规则(这样TBizCustomersDataTableRules应该改为实现ICustomers). 然而, 提供这样的一个接口以便于稍后我们可以在其中添加自定义方式是一个好习惯.
提示:一些开发者不是非常理解RegisterDataTableRules,及其传递的参数和运行原理. 这个过程定义在uDADataTable:
RegisterDataTableRules(const anID: string;
const aDataTableRulesClass: TDADataTableRulesClass);
注册过程这样就向全局规则列表中加入一个规则.
在运行时,存储在BusinessRulesID属性中的值用于查找需要的类.本例中,我们向注册过程传递一个易于理解的字符串('CustomerClientRules') ,这个值将会指定给BusinessRulesID 属性.
在前面生成的代码中,传递的字符串变量(不易理解的GUID)正是BusinessRulesID 属性所需要的值.
同时,我们只需要为tbl_Customers增加事件处理(in CalcFields_ClientData).实际上只有一个:
procedure TCalcFields_ClientDataForm.tbl_CustomersCalcFields(
DataTable: TDADataTable);
begin
DataTable.FieldByName('ClientCalculated').AsString := 'Got #' +
DataTable.FieldByName('ServerCalculated').AsString;
end;
现在我们可以在TBizCustomersDataTableRules (通过TCustomersDataTableRules)中增加一个OnCalcFields方法而忽略这个混乱的方法,并同样可以实现上面的逻辑.
提示:这样重新分解以前(例如移除一个事件处理),很值得去测评一下原来的代码以便让你知道其效率,否则就没有好的参照去比对你对运行的改进.
从tbl_Customers中移除事件处理,否则它拥有优先权将屏蔽业务规则.
强类型可用于替换这些代码的方式非常优美:
uses SysUtils, uDADataTable, SchemaClient_Intf;
type
IBizCustomers = interface(ICustomers)
['{F9D78080-2B61-44D2-9148-C8D53329A08F}']
end;
TBizCustomersDataTableRules = class(TCustomersDataTableRules,
IBizCustomers)
protected
procedure OnCalcFields(Sender: TDADataTable); override;
end;
implementation
procedure TBizCustomersDataTableRules.OnCalcFields(Sender: TDADataTable);
begin
ClientCalculated := Format('Got #%d',[ServerCalculated]);
end;
注意:
- SysUtils (Format方法)已经加入到类的uses表达式中, uDADataTable 从实现的uses表达式中移动过来.
- 不要忘记向接口声明中增加override指示,否则代码无法运行.
- ServerCalculated 字段, 不像ClientCalculated,是一个整形字段.
最后,到CalcFields_ClientData将tbl_Customers.BusinessRulesID属性设置为 'CustomerClientRules' (中告知我们在RegisterDataTableRules过程中提供).现在编译并运行服务端和客户端测试代码.
OnCalcFields事件处理已经从数据模块脱离以后就不知道它的存在.
其他的数据处理可以轻松添加,例如BeforePost和 AfterInsert:
TBizCustomersDataTableRules = class(TCustomersDataTableRules,
IBizCustomers)
protected
procedure AfterInsert(Sender : TDADataTable); override;
procedure BeforePost(Sender : TDADataTable); override;
procedure OnCalcFields(Sender: TDADataTable); override;
end;
本例中这两个事件处理可能像如下实现:
implementation
procedure TBizCustomersDataTableRules.AfterInsert(Sender: TDADataTable);
begin
inherited;
CustomerID := IntToStr(DataTable.RecordCount);
CompanyName := '<company name>';
end;
procedure TBizCustomersDataTableRules.BeforePost(Sender: TDADataTable);
begin
inherited;
ValidateCustomer(Self);
end;
BeforePost代码特别有趣.注意如何将Self作为参数传递给ValidateCustomer过程.这种情况下Self是什么类型的?看一下ValidateCustomer的实现:
procedure ValidateCustomer(const aCustomers : ICustomers);
var errors : string;
begin
errors := '';
with aCustomers do begin
if (Trim(CustomerID)='') then
errors := errors+'CustomerID cannot be empty'+#13;
if (Trim(CompanyName)='') then
errors := errors+'CompanyName is required'+#13;
if (errors<>'')
then raise EDABizValidationException.Create(errors);
end;
end;
这阐明了使用接口的最主要原因:充当多种继承身份. BusinessProcessor的子类也实现了ICustomers同时我们也可以在服务端调用ValidateCustomer.
服务端业务规则
我们已经看到如何在客户端规则中将DataTable的事件处理从窗体或数据模块中移除.服务端规则可以对BusinessProcessor事件处理做同样处理:
由于当前没有事件处理,我们需要加入一个.
提示:使用IDE工具可以很轻松的创建事件处理.在CalcFieldsService_Impl中生成这些代码再拷贝到你的新业务单元. 这省得你自己去生成各种事件的签名了.
为了简单阐述这个过程,我们将增加一个OnBeforeProcessChange事件处理将CompanyName字段转换为大写.首先我们将bpCustomers的这个事件处理拷贝一下(在CalcFieldsService_Impl):
procedure TNewService.bpCustomersBeforeProcessChange(
Sender: TDABusinessProcessor; aChangeType: TDAChangeType;
aChange: TDADeltaChange; var ProcessChange: Boolean);
var s: string;
begin
s := aChange.NewValueByName['CompanyName'];
aChange.NewValueByName['CompanyName'] := Uppercase(s);
end;
要将之转换为业务规则,我们向服务项目中增加一个新单元并保存为BizCustomersServer.pas,代码如下:
unit BizCustomersServer;
interface
uses Classes, SysUtils, uDADataTable, uDABusinessProcessor,
SchemaServer_Intf, BizCustomersDataTable, uDADelta, uDAInterfaces;
type
TBizCustomerServerRules = class(TCustomersBusinessProcessorRules)
protected
end;
implementation
initialization
RegisterBusinessProcessorRules('CustomersServerRules',
TBizCustomerServerRules);
end.
现在增加一个与前面事件用样签名的BeforeProcessChange方法,当然不要忘记override标志:
type
TBizCustomerServerRules = class(TCustomersBusinessProcessorRules)
protected
procedure BeforeProcessChange(Sender : TDABusinessProcessor;
aChangeType : TDAChangeType; aChange : TDADeltaChange;
var ProcessChange : boolean); override;
end;
implementation
procedure TBizCustomerServerRules.BeforeProcessChange(
Sender: TDABusinessProcessor; aChangeType: TDAChangeType;
aChange: TDADeltaChange; var ProcessChange: boolean);
begin
inherited;
CompanyName := Uppercase(CompanyName);
end;
注意实现中简单的语法,我们将做一点说明.你在SchemaServer_Intf.pas中将会看到CompanyName的实现:
function TCustomersBusinessProcessorRules.GetCompanyName: String;
begin
result := BusinessProcessor.CurrentChange.NewValueByName[fld_CustomersCompanyName];
end;
procedure TCustomersBusinessProcessorRules.SetCompanyName(
const aValue: String);
begin
BusinessProcessor.CurrentChange.NewValueByName[fld_CustomersCompanyName] := aValue;
end;
强类型属性关联到Delta的子项.然而事件处理函数处理Delta中的所有子项(如OnBeforeProcessDelta传递一个Delta参数)并需要在遍历Delta实体时直接调用NewValueByName语法.
最后,我们从bpCustomers中移除时间处理并将BusinessRuleID属性设置为 CustomersServerRules (在BizCustomersServer中RegisterBusinessProcessorRules指定).
虽然这个范例表现的非常繁琐,它已经包括了重点并阐述如何隔离业务逻辑.
字段级别的业务规则
在字段级别,只有两个事件要处理:
如前面一样我们通过File | New | Unit在客户端加入一个新的单元,这时我们将其命名为FieldRules.pas.
注意: 在前面我们生成的单元中你将要向BizCustomersDataTable 中增加代码实际上就是要处理这种情况.为了看的清晰这里我们使用不同的单元.
增加如下架构代码:
unit uFieldRules;
interface
uses Classes, SysUtils, uDADataTable, uDAInterfaces;
type
TCompanyFieldRules = class(TDAFieldRules)
end;
implementation
initialization
RegisterFieldRules('Company_Rules', TCompanyFieldRules);
end.
注意: Company_Rules 是将要向赋予适当的字段的BusinessRulesID属性的值.
我们将在fClientDataModule中对CompanyName字段加入一些简单的事件并在我们要移动它们之前使它们运行.
在CalcFields_ClientData中右击tbl_Customers打开字段集合编辑器并选择CompanyName条目.
使用对象查看器,创建OnChange和OnValidate事件处理加入如下代码:
procedure TCalcFields_ClientDataForm.tbl_CustomersCompanyNameChange (
Sender: TDACustomField);
var
i : integer;
nam : string;
begin
nam := Sender.AsString;
for i := 1 to Length(nam) do
if not (nam[i] in ['a'..'z', 'A'..'Z', '0'..'9'])
then raise Exception.Create('Invalid character');
end;
procedure TCalcFields_ClientDataForm.tbl_CustomersCompanyNameValidate (
Sender: TDACustomField);
begin
if Length(Sender.AsString) < 5 then
raise Exception.Create('CompanyName must exceed 5 characters');
end;
生成并运行服务端和客户端应用程序去验证客户端事件处理行为.
注意: OnChange 在你离开这个字段时触发而OnValidate在你离开这个行时触发.
现在我们将在FieldRules.pas中增加等价的事件处理.首先,接口声明如下:
TCompanyFieldRules = class(TDAFieldRules)
protected
procedure OnValidate(Sender: TDACustomField); override;
procedure OnChange(Sender: TDACustomField); override;
end;
实现代码可以从fClientDataModule拷贝过来并将过程的签名作如下修改:
procedure TCompanyFieldRules.OnChange(Sender: TDACustomField);
var
i : integer;
nam : string;
begin
nam := Sender.AsString;
for i := 1 to Length(nam) do
if not (nam[i] in ['a'..'z', 'A'..'Z', '0'..'9'])
then raise Exception.Create('Invalid character');
end;
procedure TCompanyFieldRules.OnValidate(Sender: TDACustomField);
begin
if Length(Sender.AsString) < 5 then
raise Exception.Create('CompanyName must exceed 5 characters');
end;
最后,我们将CompanyName字段的BusinessRulesID属性值设置为'Company_Rules':
运行客户端验证运行情况.
这里就是全部要做的!
总结
本文包含了客户端和服务端的数据集级别和客户端的字段级别的业务规则的实现.
展示了如何设置TDataTable, BusinessProcessor 和字段的BusinessRulesID 属性值以及强类型向导如何减少代码量.
- DA11 – 深入业务规则(Business Rules)
- 业务规则管理(Business Rules Management,简称BRM)
- K2 blackpearl 中的业务规则(Rules)
- K2 blackpearl 中的业务规则(Rules)
- 最近关于XBRL 业务规则处理器Business Rules Processors的讨论很热闹,也罗列了一些实现Formula的工具
- 应用Jboss rules规则引擎,以中文定义业务规则
- K2.Net 2003中的5种业务规则(Rules)
- K2.Net 2003中的5种业务规则(Rules)
- Business Rules(From Wiki)
- rules 规则
- 转载]深入了解WPS中的业务对象Business Object
- Danial Selman谈Business Rules
- ILOG Rules for .NET -为Microsoft.NET量身定制的业务规则管理技术
- Table 的 rules规则
- CSS @规则(at-rules)
- kohana验证规则rules
- 调试规则,debugging rules
- Yii rules常用规则
- 云计算泄露Google的秘密
- 使用ffmpeg截取视频(包括截图)
- Hi.csdn.net - CSDN空间全新改版上线,抢先试用!
- Orical配置的过程步骤
- PE文件格式与API HOOK
- DA11 – 深入业务规则(Business Rules)
- SQL中IN,NOT IN,EXISTS,NOT EXISTS的用法和差别
- 三门问题
- jsp(WAP)获取手机号码
- 面向对象设计的69条经验原则
- 数据持久层组件的初次接触
- Silverlight 2.0中文学习资源集萃
- JDK1.5 环境变量配置
- 数学黑洞