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 :=
''
;
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
属性值以及强类型向导如何减少代码量
.