DA11 – 深入业务规则(Business Rules)

 
DA11 – 深入业务规则 (Business Rules)
综述
业务规则 (BRs) 是一个包含业务逻辑封装的概念 , 从前台的终端用户接口及后台的终端数据库分离处理 . Data Abstract 通过使用业务帮助类 (Business Helper Classes) 实现了这个概念 .
Delphi 的属性 / 方法 / 事件范例是一个好原型但是对于实际开发却不总是适用 . 问题是事件处理与窗体或数据模块绑定 . DA06 文档查看更多关系这个话题的详细信息 .
通过如下组件的事件处理 , 业务规则的逻辑可以在客户端及服务器初始化 :
  • TBusinessProcessor服务端的数据集规则
  • TDataTable客户端的数据集规则
  • TDataTable.Field客户端的字段规则
我们需要一个更够将这些事件处理移到业务层的过程 , 本文将集中与此 .
本文提供一个指南展示如何向已经存在的项目中增加这三个类型的规则 . 这里将基于自带的存取 Northwind 数据库的 Customers 表的 CalculatedFields 范例 .
在本文的底部将提供下载代码的连接 . 文件连接包含 'before' 'after' 项目 , 允许你用最初版本与最终的解决方案做比对 .
强类型单元
虽然使用提供的向导去创建强类型单元不是绝对必要的 , 但是我们推荐用这种方式为你生成业务规则架构 .
  1. 拷贝Samples/CalculatedFields目录下的所有文件到一个叫做Strongly Typed的目录.
  2. 打开CalcFields.bpg ( CalcFields.groupproj)并在项目管理器中将其命名为BusinessRulesExample (通过Save Project Group As).
  3. 确保服务端项目 (StronglyTypedServer.Exe)是项目管理器的默认项目.
  4. 打开 CalcFieldsService_Impl 右击 Schema:
DA11 – 深入业务规则(Business Rules)_第1张图片
点击上图指向的菜单项 . 你需要在向导中输入两个单元名字 . 这里使用默认的 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的声明,以及如何明确的以ICustomersICustomersDelta类型引用.
客户端业务规则
客户端规则的主要意图就是将 DataTable 的事件处理代码从窗口或数据模块中移除 , 同时封装为业务逻辑以便于重用 . 关注 tbl_Customers 的事件处理 ( CalcFields_ClientData ):
DA11 – 深入业务规则(Business Rules)_第2张图片
我们需要转移 OnCalcFields 事件处理并作为一个业务规则 . 如上所述 , 我们将在单独的单元中创建一个 TCustomersDataTableRules 的子类 . 首先 , 我们在项目管理器中通过拖动的方式将 SchemaClient_Intf 加入到客户端项目 :
DA11 – 深入业务规则(Business Rules)_第3张图片
下一步 , 保证 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). 实际上只有一个 :
DA11 – 深入业务规则(Business Rules)_第4张图片
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 ;
注意 :
  1. SysUtils (Format方法)已经加入到类的uses表达式中, uDADataTable 从实现的uses表达式中移动过来.
  2. 不要忘记向接口声明中增加override指示,否则代码无法运行.
  3. 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 事件处理做同样处理 :
DA11 – 深入业务规则(Business Rules)_第5张图片
由于当前没有事件处理 , 我们需要加入一个 .
提示 : 使用 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 指定 ).
虽然这个范例表现的非常繁琐 , 它已经包括了重点并阐述如何隔离业务逻辑 .
字段级别的业务规则
在字段级别 , 只有两个事件要处理 :
DA11 – 深入业务规则(Business Rules)_第6张图片
如前面一样我们通过 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':
DA11 – 深入业务规则(Business Rules)_第7张图片
运行客户端验证运行情况 .
这里就是全部要做的 !
总结
本文包含了客户端和服务端的数据集级别和客户端的字段级别的业务规则的实现 .
展示了如何设置 TDataTable, BusinessProcessor 和字段的 BusinessRulesID 属性值以及强类型向导如何减少代码量 .

你可能感兴趣的:(Data,Abstract文档翻译)