Strongly Typed DataTables and Business Helper Classes
This article explains how to encapsulate business rules in reusable and dynamic classes and how to use strongly-typed fields without losing the flexibility of DataTables. It also shows how similar concepts are applicable to server side development and Business Processors.
DataTables, RAD and Design Issues using Event Handlers
DataTables are conceptually very similar to datasets and extend them by adding dynamic method binding functionality, regular expression support, object oriented deltas and many other features. You will use DataTables mostly client-side, to display and edit data via standard data-aware controls, the same way you would when using an in-memory dataset like standard TClientDataset
or Developer Express's TdxMemData
.
Despite the similarities, DataTables don't support persistent fields. The reason behind this choice has a lot to do with the quality of the code of applications using a purely RAD (event-driven) approach, and the difficulty of isolating business oriented code into reusable units.
When you add an event handler for events such as BeforePost
or AfterInsert
, Delphi adds a method to the TDataset's Owner
. The Owner can be a form or, more commonly, a data module. Now, imagine having 10 or more TDatasets that require separate BeforePost
and AfterInsert
handlers. Your data module will quickly become a messy collection of methods that are not usable and will not provide elegant encapsulation.
Systems built by using only this technique usually become a maintenance-nightmare after a few months of development and RAD can quickly become the acronym of Rapid Application Disaster.
Data Abstract provides an elegant solution to address the issue by allowing you to treat DataTables as if they were pure objects such as those you'd create in non-data-aware applications.
The Data Abstract solution
Data Abstract solves the problems listed above (and others) through the use of strongly-typed datatables, strongly-typed business processors and business helper classes. In a way, this is similar to what ADO.NET allows you to do with strongly-typed datasets, but we have extended the concept a lot as you will see at the end of the article.
Tip: The solution combines variations of the Builder and Strategy design patterns.
The Builder pattern allows a client object to construct an object by only specifying its type and content. The client is shielded from the details of the object's construction.
The Strategy pattern consists of decoupling an algorithm from its host and encapsulating it into a separate class. This basically allows you to switch the algorithm that you are using at any time.
You can read more about these two patterns and others that are implemented in our products in "Design Patterns", Addison-Wesley (Gamma, Helm, Johnson, Vlissides) and "Patterns of Enterprise Application Architecture", Addison-Wesley (Fowler).
Strongly typed data tables are returned by type casting a normal datatable to specific business interfaces so that you can access their fields as properties, or call the additional methods you might need to add. We will see how to generate those interfaces later.
For now, just look at the following screenshot to get an idea of the result (note the CodeInsight list). You need to understand that the ICustomers
definition and code was already generated for you:
Although this is a huge step forward compared to using FieldByName
or the Fields
collection, you might think that the same can easily be achieved by using persistent fields. Well, not quite. You can see this by examining the definition of the TStronglyTypedClientMainForm
class:
Thanks to Data Abstract's strongly typed interfaces, your data modules and forms won't be polluted by dozens of TField
components anymore. The definitions of the structure of your tables is completely isolated from the owner module and is contained in specific business units.
This is a big step forward for the maintenance and the design of your system as a whole.
But can you add specific behavior to your classes (e.g. have a TCustomer.CheckBalance
method) and keep that isolated from the owner's code? Of course! Take a look at the following screenshot:
Notice that the data table has been typecast to IAdvancedCustomer
rather than ICustomers
. That method is isolated from the StronglyTypedClientMainForm
, as you would expect.
All this is made possible through the use of business helper classes. We will explain what they are and how they work later in the article. Before we do that, we need to see how you can get to the base of strongly typed classes and interfaces (e.g. ICustomers
) by using our IDE wizards.
IDE wizard for Strongly typed units
Open the project group of the Strongly Typed example, select the server and open the StronglyTyped Service
service.
The Data Abstract schema contained in this module only contains a data table called Customers. You can see its definition below:
The use of Data Abstract schemas makes the generation of strongly typed units a simple and automated process. Right click on the main form's schema and click on the selected menu item below:
The wizard will now prompt you for the names of two units:
- The client-side business unit: this unit contains interfaces such as ICustomers and IOrders together with base classes that implement them (see
TCustomersDataTableRules
below). These classes contain all the code required to map the properties to the datatable fields. You can then subclass them to implement your specific business rules and methods. An example of this can be found in theuBizCustomersClient.pas
unit. - The server-side business unit: this unit references the previous one and extends its types to support server side deltas. You will find the definitions of interfaces such as
ICustomersDelta
and the relative implementer classes (seeTBizCustomerServerRules
below). You can subclass these as you do with the client classes and implement your custom business rules. An example of this is contained in theuBizCustomersServer.pas
unit.
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.
As you can see, there is an interface (ICustomers), a class that implements it (TCustomersDataTableRules) and a call to the RegisterDataTableRules function. We'll explain the purpose of this class and the function call later.
Here is a snap shot of the server-side business unit:
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.
As you can see, this unit extends the client side types and gives us a strongly typed view of the deltas that the clients generate. As you might expect, this allows you to take advantage of the coding strategies on the server-side of the examples above when handling deltas.
Data Abstract's Business Helper classes
TCustomersDataTableRules
and TCustomersBusinessProcessorRules
are business helper classes. Their purpose is to implement property getters and setters and provide a base class to inherit from. You will then create one or more descendants and implement your business code in separate units.
This is exactly what we did in the uBizCustomersClient.pas
and uBizCustomerServer.pas
units by creating two descendants implementing real business logic. This is a snapshot of 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.
As you might have guessed, the TDADataTableRules class (from which TBizCustomersClientRules
ultimately descends) is responsible for providing the datatable's implementation of the event handlers (i.e. BeforePost, AfterInsert, etc):
{ 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
inherits from TDABusinessProcessorRules instead and provides an implementation for the event handlers of a TDABusinessProcessor.
This is a snippet from the uBizCustomersServer.pas
unit:
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.
There's only one element we haven't discussed yet: how do you associate a TDADataTable and TDABusinessProcessor to a business helper class? This is accomplished by setting the BusinessRulesID property to match the value used in the RegisterXXXRules call in the initialization section of the units above.
When you do this, the data table and the business processor will create an instance of the class associated with that ID (see the 2nd parameter of RegisterDataTableRules and RegisterBusinessProcessorRules) and let those handle the support for the business interface you are trying to use in your code.
Tip: as you may have noticed, TBizCustomersClientRules
is the class that provides the implementation for IAdvancedCustomer
. You can add as many extra interfaces as your design suggests and the data table will still be able to be typecast to that interface, by using the 'Supports' VCL function or by simply calling QueryInterface
. This allows you to check which interfaces the data table and its helper class support.
Conclusion
Data Abstract strongly-typed data tables and business processors greatly improve the readability and integrity of your code by adding type safety to your data tables. Through the use of business helper classes and interface type-casting, you can say good-bye to messy data modules and start designing your systems using pure and clean object oriented techniques in a RAD way.
The architecture of Data Abstract allows you to clearly separate client and server-side business rules and reutilize shared blocks whenever needed (e.g. the ValidateCustomers function). Object orientation is finally a RAD experience for multi-tier data-access!