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'项目,允许你用最初版本与最终的解决方案做比对.
强类型单元
虽然使用提供的向导去创建强类型单元不是绝对必要的,但是我们推荐用这种方式为你生成业务规则架构.
  1. 拷贝Samples/CalculatedFields目录下的所有文件到一个叫做Strongly Typed的目录.
  2. 打开CalcFields.bpg ( CalcFields.groupproj)并在项目管理器中将其命名为BusinessRulesExample (通过Save Project Group As).
  3. 确保服务端项目 (StronglyTypedServer.Exe)是项目管理器的默认项目.
  4. 打开 CalcFieldsService_Impl 右击 Schema:
点击上图指向的菜单项.你需要在向导中输入两个单元名字.这里使用默认的SchemaClient_Intf.pas SchemaServer_Intf.pas. 提示: 这时这两个单元文件已经加入到服务端项目.
那么,这两个单元文件提供了什么功能呢?首先,我们查看SchemaClient_Intf.pas.这个单元提供了所有Customers数据集字段有效的GetterSetter方法. 这种代码太多,为了便于理解我们抽取与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}')可以用于向DataTableBusinessRulesID属性赋值. 可能这不是首选的解决方案.在单独的单元中创建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):
我们需要转移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;
注意:
  1. SysUtils (Format方法)已经加入到类的uses表达式中, uDADataTable 从实现的uses表达式中移动过来.
  2. 不要忘记向接口声明中增加override指示,否则代码无法运行.
  3. ServerCalculated 字段, 不像ClientCalculated,是一个整形字段.
最后,CalcFields_ClientDatatbl_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 (BizCustomersServerRegisterBusinessProcessorRules指定).
虽然这个范例表现的非常繁琐,它已经包括了重点并阐述如何隔离业务逻辑.
字段级别的业务规则
在字段级别,只有两个事件要处理:
如前面一样我们通过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条目.
使用对象查看器,创建OnChangeOnValidate事件处理加入如下代码:
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 属性值以及强类型向导如何减少代码量.
原创粉丝点击