DA06 – 强类型DataTable和 Business Helper 类

来源:互联网 发布:ar扫描软件 编辑:程序博客网 时间:2024/05/17 21:46
 
DA06 – 强类型DataTable Business Helper
本文说明了如何将业务规则封装为可重用动态的类,如何在无损DataTable灵活性的前提下使用强类型字段.也展示了用于服务端开发和业务处理器相类似的概念.
注意: 本文引用的范例可以在文档底部的连接中下载.
DataTable, RAD 和用于事件处理的设计问题
DataTable在概念上非常类似于DataSet,并扩展了动态方法绑定功能,规则的表达式支持,面向对象的Delta和其他特性.通常在客户端使用DataTables,同使用BorlandTClientDatasetDeveloper ExpressTdxMemData内存数据集一样通过数据敏感控件显示和编辑数据.
尽管很相似,DataTable不支持定义固定的字段.因为这样的话需要大量使用RAD(事件驱动)性质的应用程序代码,将很难使业务对象隔离为可重用单元.
当你增加一个如BeforePostAfterInsert事件处理后,Delphi自动在含Dataset的单元(可能是窗体或Data Module)中添加一个函数.现在假设有10个或更多的TDataset组件需要使用BeforePostAfterInsert事件处理.你的Data Module将很快变成了杂乱的函数集合,将无法重用也不支持优美的封装.
使用这种技术创建的系统在开发几个月后将成为维护的噩梦,RAD很快变为Rapid Application Disaster(快速应用灾难)的缩写.
Data Abstract对这种问题提供了优美的解决方案,允许你将DataTable作为创建非数据感知应用时的纯粹对象.
Data Abstract 解决方案
Data Abstract解决上面提到的问题(和其他问题)使用了强类型数据集, 强类型的业务处理器和业务帮助(business helper). 某种程度上允许你像ADO.NET的强类型数据集一样使用,但是在下面你还将看到一些扩展概念.
提示:解决方案结合了制造商模式(Builder pattern)和策略设计模式(Strategy pattern)的变异.
制造商模式(Builder pattern)允许客户端对象通过特定类型和参数去创建其它对象.将客户端与对象构造细节隔离开.
策略设计模式(Strategy pattern )组合主机的底耦合算法并封装在单独的类中.允许你在任何时间选择使用这些算法.
关于这两个模式及我们产品中实现的其它模式,你可以查看《设计模式》(Design Patterns) (作者Addison-Wesley,Gamma, Helm, Johnson, Vlissides) 和《企业应用架构模式》(Patterns of Enterprise Application Architecture)作者(Addison-Wesley,Fowler).
强类型数据表是由正常的DataTable转化为类型化的特殊业务接口,以便于可以通过属性去存取字段,或调用你可能加入的附加方法. 接下来我们将看看如何生成这个接口.
现在我们看看下图对结果产生一个基本的认识 (注意代码提示列表).你需要理解ICustomers定义和为你生成的代码:
虽然同以前使用FieldByNameFields集合相比有很多进步,你可能认为使用固定的字段也很容易. 也不全对,你可以检查TStronglyTypedClientMainForm 类的定义:
由于Data Abstract的强类型接口,你的Data Module和窗体不再需要声明很多TField组件了. 表结构的定义与其拥有者模块完全隔离,并包含在一个特定的业务单元中.
这对于维护和系统设计来说是一大完全的进步.
当你你能向你的类增加特定的行为(TCustomer.CheckBalance方法)又能保证与其拥有者保持隔离吗?当然!看如下截图:
注意DataTable已经类型转换为IAdvancedCustomer 而不是 ICustomers.如你希望的这个方法与StronglyTypedClientMainForm是隔离的.
这些都是通过使用业务帮助类(business helper classes)实行的.我们将在本文说明它们是什么如何巧妙的运用.在说明这些之前,我们需要看一下如何使用IDE向导生成强类型类和接口( ICustomers).
强类型单元的IDE向导
打开强类型范例的项目组,选择服务端并打开StronglyTyped Service服务.
此模块的Data Abstract schema只包含一个叫做CustomersDataTable.如下图:
使用Data Abstract schemas使生成强类型单元的过程简单自动.右击主窗体的Schema控件点击如下项:
向导提示你输入两个单元的名字:
  1. 客户端业务单元:这个单元中包含一些接口如ICustomersIOrders以及实现它们的基础类(见下面的TCustomersDataTableRules).这些类包含了DataTable字段向属性影射的代码. 你可以继承它们执行你自己的特殊业务规则和方法.这个范例可在uBizCustomersClient.pas单元中发现.
  2. 服务端业务单元: 这个单元引用上一个并扩展其类型支持服务端Delta. 你将发现一些接口的定义(ICustomersDelta)和相关的实现类 (见下面的TBizCustomerServerRules). 你可以像在客户端一样继承这些类并实现你的业务逻辑规则.这个例子包含在 uBizCustomersServer.pas 单元.
unit SampleSchemaClient_Intf;
interface
[..]
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 = '{A97B58B8-3C56-413D-BA55-360BCD6ACBEA}';
 RID_Orders = '{63B9B897-D9BC-430C-9D81-C0466A5CD6AD}';
 { Data table names }
 nme_Customers = 'Customers';
 nme_Orders = 'Orders';
 { Customers fields }
  fld_CustomersCustomerID = 'CustomerID';
 fld_CustomersCompanyName = 'CompanyName';
 [..]
 { Customers field indexes }
 idx_CustomersCustomerID = 0;
 idx_CustomersCompanyName = 1;
 [..]
type
 { ICustomers }
 ICustomers = interface
 ['{555F0253-7185-47B4-86D9-0CCCF239EBBE}']
    // Property getters and setters
    function GetCustomerIDValue: String;
    procedure SetCustomerIDValue(const aValue: String);
    function GetCustomerIDIsNull: Boolean;
    procedure SetCustomerIDIsNull(const aValue: Boolean);
    function GetCompanyNameValue: String;
    procedure SetCompanyNameValue(const aValue: String);
    function GetCompanyNameIsNull: Boolean;
    procedure SetCompanyNameIsNull(const aValue: Boolean);
    [..]
    // Properties
    property CustomerID: String read GetCustomerIDValue
                                write SetCustomerIDValue;
    property CustomerIDIsNull: Boolean read GetCustomerIDIsNull
                                       write SetCustomerIDIsNull;
    property CompanyName: String read GetCompanyNameValue
                                 write SetCompanyNameValue;
    property CompanyNameIsNull: Boolean read GetCompanyNameIsNull
                                        write SetCompanyNameIsNull;
    [..]
 end;
 
 { TCustomersDataTableRules }
 TCustomersDataTableRules = class(TDADataTableRules, ICustomers)
 private
 protected
    // Property getters and setters
    function GetCustomerIDValue: String; virtual;
    procedure SetCustomerIDValue(const aValue: String); virtual;
    function GetCustomerIDIsNull: Boolean; virtual;
    procedure SetCustomerIDIsNull(const aValue: Boolean); virtual;
    function GetCompanyNameValue: String; virtual;
    procedure SetCompanyNameValue(const aValue: String); virtual;
    function GetCompanyNameIsNull: Boolean; virtual;
    procedure SetCompanyNameIsNull(const aValue: Boolean); virtual;
    [..]
    // Properties
    property CustomerID: String read GetCustomerIDValue
                                write SetCustomerIDValue;
    property CustomerIDIsNull: Boolean read GetCustomerIDIsNull
                                       write SetCustomerIDIsNull;
    property CompanyName: String read GetCompanyNameValue
                                 write SetCompanyNameValue;
    property CompanyNameIsNull: Boolean read GetCompanyNameIsNull
                                        write SetCompanyNameIsNull;
    [..]
 public
    constructor Create(aDataTable : TDADataTable); override;
    destructor Destroy; override;
 end;
 
 { IOrders }
 IOrders = interface(IDAStronglyTypedDataTable)
 ['{50A479DE-60BA-4066-AAEB-B840FC045BBB}']
 [..]
implementation
uses Variants;
{ TCustomersDataTableRules }
constructor TCustomersDataTableRules.Create(aDataTable : TDADataTable);
begin
 inherited;
end;
destructor TCustomersDataTableRules.Destroy;
begin
 inherited;
end;
function TCustomersDataTableRules.GetCustomerIDValue : string;
begin
 result := DataTable.Fields[idx_CustomersCustomerID].Asstring;
end;
procedure TCustomersDataTableRules.SetCustomerIDValue(const Value : string);
begin
 DataTable.Fields[idx_CustomersCustomerID].Asstring := Value;
end;
[..]
initialization
 RegisterDataTableRules(RID_Customers, TCustomersDataTableRules);
 RegisterDataTableRules(RID_Orders, TOrdersDataTableRules);
end.
可见有一个接口 (ICustomers)就有一个实现它的类 (TCustomersDataTableRules) 可一个叫做RegisterDataTableRules的方法的调用.我们将说明类和方法调用的用意.
这里是服务端业务单元部分代码:
unit SampleSchemaServer_Intf;
interface
uses
 Classes, DB, SysUtils, uROClasses, uDADataTable,
 uDABusinessProcessor, SampleSchemaClient_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 = '{A21D52B7-F0E3-4815-B0D3-FE378A08A79A}';
 RID_OrdersDelta = '{AD299B05-275D-495E-8036-3B383CDA5248}';
type
 { ICustomersDelta }
 ICustomersDelta = interface(ICustomers)
 ['{79206377-2D23-4B53-84A3-AA445FF02FA8}']
    // Property getters and setters
    function GetOldCustomerIDValue : string;
    function GetOldCompanyNameValue : string;
    [..]
    // Properties
    property OldCustomerID : string read GetOldCustomerIDValue;
    property OldCompanyName : string read GetOldCompanyNameValue;
    [..]
 end;
 { TCustomersBusinessProcessorRules }
 TCustomersBusinessProcessorRules = class(TDABusinessProcessorRules,
    ICustomers, ICustomersDelta)
 private
 protected
    // Property getters and setters
    function GetCustomerIDValue : string; virtual;
    function GetCustomerIDIsNull: Boolean; virtual;
    function GetOldCustomerIDValue : string; virtual;
    function GetOldCustomerIDIsNull: Boolean; virtual;
    procedure SetCustomerIDValue(const Value : string); virtual;
    procedure SetCustomerIDIsNull(const aValue: Boolean); virtual;
    function GetCompanyNameValue : string; virtual;
    function GetCompanyNameIsNull: Boolean; virtual;
    [..]
    // Properties
    property CustomerID : String read GetCustomerIDValue
                                 write SetCustomerIDValue;
    property CustomerIDIsNull : Boolean read GetCustomerIDIsNull
                                        write SetCustomerIDIsNull;
    property OldCustomerID : String read GetOldCustomerIDValue;
    property OldCustomerIDIsNull : Boolean read GetOldCustomerIDIsNull;
    property CompanyName : String read GetCompanyNameValue
                                  write SetCompanyNameValue;
    property CompanyNameIsNull : Boolean read GetCompanyNameIsNull
                                         write SetCompanyNameIsNull;
    property OldCompanyName : String read GetOldCompanyNameValue;
    property OldCompanyNameIsNull : Boolean read GetOldCompanyNameIsNull;
    [..]
 public
    constructor Create(aBusinessProcessor : TDABusinessProcessor);
    destructor Destroy; override;
 end;
  [..]
implementation
uses Variants;
{ TCustomersBusinessProcessorRules }
constructor TCustomersBusinessProcessorRules.Create(
 aBusinessProcessor : TDABusinessProcessor);
begin
 inherited;
end;
destructor TCustomersBusinessProcessorRules.Destroy;
begin
  inherited;
end;
function TCustomersBusinessProcessorRules.GetCustomerIDValue: String;
begin
 result := BusinessProcessor.CurrentChange.NewValueByName[                                                       fld_CustomersCustomerID];
end;
function TCustomersBusinessProcessorRules.GetCustomerIDIsNull: Boolean;
begin
 result :=    VarIsNull(BusinessProcessor.CurrentChange.NewValueByName[                                                      fld_CustomersCustomerID]);
end;
function TCustomersBusinessProcessorRules.GetOldCustomerIDValue: String;
begin
 result := BusinessProcessor.CurrentChange.OldValueByName[                                                       fld_CustomersCustomerID];
end;
function TCustomersBusinessProcessorRules.GetOldCustomerIDIsNull: Boolean;
begin
 result :=    VarIsNull(BusinessProcessor.CurrentChange.OldValueByName[                                                      fld_CustomersCustomerID]);
end;
[...]
initialization
 RegisterBusinessProcessorRules(RID_CustomersDelta,
                                 TCustomersBusinessProcessorRules);
 RegisterBusinessProcessorRules(RID_OrdersDelta,
                                 TOrdersBusinessProcessorRules);
end.
可见,这个单元扩展了客户端类型并提供了一个增强的类型查看客户端生成的Delta.你可能希望在上例的服务端利用代码策略处理Delta.
Data Abstract的业务帮助类( Business Helper classes)
TCustomersDataTableRules TCustomersBusinessProcessorRules是业务帮助类. 它们的目的是实现属性的GetterSetter并提供一个继承基础类.你可以在单独的单元重创建多个子类实现你的业务代码.
我们正是在uBizCustomersClient.pasuBizCustomerServer.pas单元中通过创建两个子类实现了真正的业务逻辑.这是uBizCustomersClient.pas中的代码:
unit uBizCustomersClient;
interface
uses
 Classes, SysUtils,
 uDADataTable, SampleSchemaClient_Intf,
 uDABusinessProcessor, SampleSchemaServer_Intf,
 StronglyTypedLibrary_Intf;
 { IAdvancedCustomer }
 IAdvancedCustomer = interface(ICustomers)
 ['{BDB203DC-954B-4D78-A446-B1E2232BEF71}']
    function GetOrders : IOrders;
    function CheckBalance : currency;
    procedure DisableAccount;
    property Orders : IOrders read GetOrders;
 end;
 { TBizCustomersClientRules }
 TBizCustomersClientRules = class(TCustomersDataTableRules, IAdvancedCustomer)
 private
 protected
    // Business events
    procedure AfterInsert(Sender : TDADataTable); override;
    procedure BeforeDelete(Sender : TDADataTable); override;
    procedure BeforePost(Sender : TDADataTable); override;
    // IAdvancedCustomer
    function CheckBalance : currency;
    procedure DisableAccount;
    function GetOrders : IOrders;
 end;
 { TBizOrdersClientRules }
 TBizOrdersClientRules = class(TOrdersDataTableRules)
 protected
    procedure OnNewRecord(Sender: TDADataTable); override;
    procedure BeforePost(Sender : TDADataTable); override;
 end;
 { TBizCustomerIDRules }
 TBizCustomerIDRules = class(TDAFieldRules)
 private
 protected
    procedure OnValidate(Sender: TDACustomField); override;
    procedure OnChange(Sender: TDACustomField); override;
 end;
{ General validation routine shared by client and server }
procedure ValidateCustomer(const aCustomers : ICustomers);
implementation
uses uDARemoteDataAdapter;
const
 def_CompanyName = 'New Company';
 def_ContactName = '<Unknown>';
{ General validation routine shared by client and server }
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;
procedure ValidateOrder(const aOrder : IOrders);
var errors : string;
begin
 errors := '';
 with aOrder do begin
    if (Trim(CustomerID)='') then
      errors := errors+'An order must have a CustomerID'+#13;
    if (EmployeeID<=0) then // 0 also covers NULL in the conversion of AsInteger
      errors := errors+'Invalid or unspecified EmployeeID'+#13;
    if (errors<>'') then
      raise EDABizValidationException.Create(errors);
 end;
end;
[..]
initialization
 RegisterDataTableRules('ClientRules.Customers', TBizCustomersClientRules);
 RegisterDataTableRules('ClientRules.Orders', TBizOrdersClientRules);
 RegisterFieldRules('CustomerID', TBizCustomerIDRules);
end.
你可能猜到, TDADataTableRules (最终继承TBizCustomersClientRules)负责实现DataTable的时间处理( BeforePost, AfterInsert):
{ TDADataTableRules }
TDADataTableRules = class(TDABusinessRules, IDAStronglyTypedDataTable,                                                             IDARangeController)
private
 fDataTable: TDADataTable;
 fDetails : TStringList;
 function GetDetails(Index: integer): TDADataTable;
 function GetDetailsCount: integer;
protected
 // Misc
 function GetDataTable: TDADataTable;
 procedure Attach(aDataTable : TDADataTable); virtual;
 procedure Detach(aDataTable : TDADataTable); virtual;
 [..]
 // Business events
 procedure BeforeOpen(Sender: TDADataTable); virtual;
 procedure AfterOpen(Sender: TDADataTable); virtual;
 procedure BeforeClose(Sender: TDADataTable); virtual;
 procedure AfterClose(Sender: TDADataTable); virtual;
 procedure BeforeInsert(Sender: TDADataTable); virtual;
 [..]
public
 constructor Create(aDataTable : TDADataTable); virtual;
 destructor Destroy; override;
end;
TCustomersBusinessProcessorRules继承自TDABusinessProcessorRules并提供TDABusinessProcessor的事件处理.
这是uBizCustomersServer.pas单元的部分代码:
unit uBizCustomersServer;
{
 This unit contains the business rules handlers for the server application.
 It enforces additional rules that might change over time. This is a good
 example to show the advantages of a multi-tier architecture: systems can be
 updated via a server re-deploy without the need to update any client.
 It's important to notice how some business rules are shared among clients and
 servers. In particular, Customer validation is done by calling the
 ValidateCustomers function (from uBizCustomersClient.pas).
 This is not a requirement but a highly desirable practice, expecially when
 your system is accessed by clients that were not developed by you (e.g. Java
 clients accessing your server through SOAP).
 For additional notes, such as how to extend the business functionality by
 adding custom interfaces, refer to the comments in the
 uBizCustomersClient.pas unit
}
interface
uses
 Classes, SysUtils,
 uDADataTable,uDADelta,
 uBizCustomersClient, uDAInterfaces,
 uDABusinessProcessor, SampleSchemaServer_Intf;
type
 { TBizCustomerServerRules }
 TBizCustomerServerRules = class(TCustomersBusinessProcessorRules)
 protected
    // Business events
    procedure BeforeProcessChange(Sender: TDABusinessProcessor;
                                  aChangeType: TDAChangeType;
                                  aChange: TDADeltaChange;
                                  var ProcessChange: boolean); override;
 end;
implementation
{ TBizCustomerServerRules }
procedure TBizCustomerServerRules.BeforeProcessChange(
 Sender: TDABusinessProcessor; aChangeType: TDAChangeType;
 aChange: TDADeltaChange; var ProcessChange: boolean);
begin
 inherited;
 if (aChangeType<>ctDelete) then ValidateCustomer(Self);
 // It's a sort of a strong rule but it's just to make a point that server side
 // business rules might enforce stronger rules than clients.
 if (aChangeType=ctInsert) and not SameText(ContactName, 'Alex') then
    raise Exception.Create('Cannot process an update without Alex as ContactName');
 ContactTitle := TimeToStr(Now);
end;
initialization
 RegisterBusinessProcessorRules('ServerRules.Customers',TBizCustomerServerRules);
end.
只有一个要素我们还没有讨论:如何向TDADataTable TDABusinessProcessor管理业务逻辑帮助类?这是在上面单元的Initialization小节中调用RegisterXXXRules设置匹配的 BusinessRulesID 属性实现的.
这样设置以后,DataTable和业务处理器将创建与这个ID(RegisterDataTableRules RegisterBusinessProcessorRules的第二个参数)匹配的类的实例,使之操作你要在代码中使用的业务接口支持.
提示:你可能主要到, TBizCustomersClientRules实现了IAdvancedCustomer接口.同样你也可以增加其它接口并可以使用支持VCL功能或QueryInterfaceDataTable类型转换为这些接口.这允许你检测DataTable及其帮助类支持哪个接口.
结论
Data Abstract强类型数据表和业务处理器通过增加类型安全的DataTable大大的改进了代码的可读性和完整性. 通过使用业务帮助类和接口类型转换,你可以与凌乱的Data ModuleGood-bye,并以RAD的方式使用纯粹清晰的面向对象技术.
Data Abstract架构允许你清晰的将客户端和服务端业务规则分离,并在需要的时候再利用共享块( ValidateCustomers 函数).为多层数据接口以RAD的方式提供面向对象支持!
 
 
原创粉丝点击