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 the uBizCustomersClient.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 (see TBizCustomerServerRules 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 the uBizCustomersServer.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!