
Encapsulating Business Rules In A Delphi Component Aids Reuse.
With a steady increase in demand for client/server programming, there has been quite a bit of emphasis placed on breaking up an application into three distinct layers. The resulting layers typically include the user interface layer, the data storage and access layer, and the business rules layer. Each layer isolates itself from the others, which means that none of the layers needs to be concerned with how the other layers perform their respective functions.
The user interface layer and the data access layer are fairly easy to separate. Borland's Delphi does a good job of this by using forms for the user interface layer and the dataset and data source components for the data access layer. The user interface, isolated in the form file, does not need to know how or where the data is accessed. This arrangement makes creating database applications very simple. However, when business rules need to be incorporated into the application, they are typically placed i n methods of the form class.
The problem with placing business rules in form methods is that the form is part of the user interface layer, and database tables are rarely dedicated to a single application, let alone a single form. This does not promote reusability or consistent appli cation of business rule logic.
To demonstrate the potential problems, consider the following: You are developing an application that needs to determine the number of vacation days an employee has earned for the current year. You could write the code to calculate this number in one of the form's methods, such as a button click event handler. However, if you needed to perform this same calculation in another program or even on another form, you would have to duplicate the code.
In practice, you can avoid this scenario by wrapping the calculation code inside a function, which then resides in a separate Delphi unit that can be used by any application that needs to perform that function. This process certainly increases your abili ty to reuse the calculation code, but it requires extra parameters defining the values needed for the calculation. These parameter lists can get quite long for complex business rules. Furthermore, this particular function is only applicable to employees, which suggests that an object-oriented solution would be more appropriate.
A better approach is to construct an employee class that encapsulates the data associated with an employee and knows how to perform the necessary business functions, such as calculating the number of vacation days earned. This function would be defined a s a method of the employee class. In this way, any other business rules associated with an employee could be defined in the class.
The only problem with this approach is in how it fits in with the way Delphi works - that is, how does an employee object interact with the Delphi database architecture? For example, a typical Delphi database application uses data-aware controls connecte d to a data source that is then connected to a dataset such as a table in an RDBMS. In this arrangement, the data-aware controls directly edit the record referenced by the dataset.
The problem is getting the data into and out of the employee object. Any time a change is made in any one of the data-aware controls, the employee object must be updated. This requires handling events, which unfortunately requires writing code in the for m file - the original problem you are trying to avoid! Even if you can keep the employee object in synch with the controls, there is still the problem of posting the record to the database. You would have to interrupt the posting process so that the data could be moved from the employee object to the underlying field objects. Again, this would require more code in the form file.
As an alternative, you could adopt the employee object approach while avoiding the data-aware controls. Although this solution eliminates some of the synchronization code needed, it is no better than the previous approach because the advanced features of the data-aware controls (such as auto-editing) are sacrificed. Besides, who wants to give up Delphi's DBGrid?
Delphi 2.0 introduces the concept of DataModules as a way of separating business rules from the user interface layer. DataModules and business components are very similar. However, DataModules have three weaknesses. First, when using a DataModule, you ar e locked into using one type of dataset, TTable or TQuery, because the business rules are implemented in event handlers of the dataset. Second, because it is common to update the user interface when a record changes, developers find it necessary to write user interface code within the DataModule. And finally, DataModules are only available in Delphi 2.0. (Of course, if you're using Delphi 2.0, this last point is moot.) However, business components do not suffer from any of these weaknesses.
Field objects are powerful components. Their obscurity results mainly from the fact that they are not represented visually - that is, they do not appear on the component palette and therefore are not dropped onto a form. Instead, field objects are create d implicitly whenever a dataset is opened or explicitly by the user using the Fields Editor.
How field objects are accessed depends on how they were created. If the field objects are created implicitly by the dataset, then they can be accessed only at runtime using either the Fields array property or the FieldByName method as shown in the follow ing code fragment.
TblEmployee.Fields[ 4 ].AsDateTime := Now;
TblEmployee.FieldByName( 'HireDate' ).AsDateTime :=
Now;
This notation is not very concise, but it does provide access to a column's data. Field objects have several methods for converting a column's data into different formats, such as AsString and AsInteger. In the above example, the AsDateTime method is use d to manipulate the underlying value as a TDateTime.
Creating field objects at design time has several advantages. Most important, field objects have properties that can be modified at design time. For example, each field object has a DisplayLabel property that is used to specify the column heading that ap pears in a DBGrid. The LastName, FirstName, and HireDate column headings in Figure 1 have been modified using this property.
In addition to DisplayLabel, the DisplayWidth and Index properties also affect how the columns appear in a grid. DisplayWidth is used to control the size of a column, and Index determines the column ordering. In Figure 1, notice that although a field obj ect has been created for EmpNo, it does not appear in the grid. That's because the Visible property for TblEmployeeEmpNo has been set to False. This setting removes the column from the grid, but it does not prevent individual controls from accessing the data.
The properties of field objects provide much more functionality than simply altering a grid's display. For example, the Currency property of TblEmployeeSalary is set to True. This causes Delphi to automatically format the floating point number stored in the Salary column using the currency settings specified in the Windows Control Panel.
Field objects are also useful at runtime. Field objects provide a clean and concise way of referencing a column's data. Setting the HireDate now looks like this:
TblEmployeeHireDate.Value := Now;
Compare this result to the results from using the Fields array and FieldByName method shown earlier. Notice, too, that because we are dealing directly with a TDateTimeField, and not just a basic TField object, we can use the Value property to access the data in its native data type; there is no need to convert the data.
One of the most important uses of field objects is in the creation of calculated fields. Calculated fields look like any other column to the user, except that they cannot be modified. The data displayed is calculated at runtime using values from other co lumns. For example, the MonthlySal field is calculated by dividing the Salary by 12.
Using calculated fields requires two steps. First, the field must be defined using the Fields Editor. Second, an event handler must be written for the dataset's OnCalcFields event. (This event fires every time a record in a dataset needs to be displayed. ) In the event handler, set the calculated field's Value to the result of the desired calculation. Figure 1 also shows the OnCalcFields event handler for the TblEmployee dataset. Because the OnCalcFields event occurs only at runtime, the MonthlySal colum n in the grid is not populated.
Simply having a business object use field objects is not such a difficult task. As you have seen, field objects can be accessed through a dataset. What makes the TRzBusinessComponent class different is that it can create field objects explicitly without using the Fields Editor.
Every business component inherits a MainDataset property from the TRzBusinessComponent class. This property is linked to the desired dataset, either at design time or at runtime. When the business component is connected to a dataset at design time, the b usiness component explicitly creates the desired field objects with the same mechanism used by the Fields Editor. This event results in field objects appearing in the form's class declaration. More important, the business component itself obtains a refer ence to each of the field objects, which it can then use to support business rules and functions.
Although the TRzBusinessComponent can be used in isolation, it is much more effective when used with a TRzBusinessTable or TRzBusinessQuery. These two classes are simple descendants of the standard TTable and TQuery components; their role in the architec ture comes from their ability to communicate with a business component. When the MainDataset property of a business component is connected to one of these datasets, the business component can intercept the events of the dataset.
Validation is probably the most important utilization of business rules. Typically, validation is performed during the BeforePost event. The new table and query components define a OnStartPost event, visible only to the TRzBusinessComponent class. When t he dataset record is about to be posted, the business component intercepts the BeforePost event and generates the OnStartPost event for the business component, which in turns calls its virtual Validate method. Therefore, the Validate method is guaranteed to be called before a record is posted. In addition, the developer is still able to write a custom BeforePost event handler.
As an example, let's create an employee business component. This component will define methods to determine how long an employee has been employed by the company and the number of vacation days this employee has earned for the current year. In addition, the Validate method will be overridden to prevent an employee's salary from exceeding a user-defined salary cap.
Listing 1 shows the RzDBEmp unit that contains the TRzDBEmployee class definition. The class declaration is responsible for declaring the field objects that will be used in the component. The field objects are stored as private el ements of the class. Each element supports a property that provides access to the field object.
The first five properties declared in this class represent field objects that are mapped to five of the six possible columns in the Employee table. The PhoneExt column is not used. This approach shows that a business component can operate on a subset of columns in a table. Of course, the Fields Editor could be invoked and a PhoneExt field object could be created. In this case, the PhoneExt field object could be used by the form, but the employee component would know nothing about it.
The MonthlySal property is for a calculated field object. All of these properties are declared as read-only. This declaration does not prevent the user from changing the underlying table data; it simply prevents the user from altering or deleting the fie ld object.
When declaring the field objects, the data type used for each object must match the corresponding column type. For example, FSalary must be declared as a TFloatField and FHireDate must be declared as a TDateTimeField.
In addition to declaring the field object properties, the TRzDBEmployee class also declares a number of business methods and a published property. The SalaryCap property is used to specify the maximum annual salary that can be earned by any one employee, and the Validate method will prevent any salary from exceeding this limit.
The most important method of any business component is the CreateFields method, which is responsible for creating the desired field objects. This method also specifies the initial settings for the objects. Creating each object is relatively easy using th e CreateMainField method inherited from the TRzBusinessComponent. The only tricky part is that the return value must be converted to the appropriate type before being assigned to the private variable, because CreateMainField returns a TField.
Once the individual field objects are created, you can modify any of the object's properties. For example, the DisplayLabel property is commonly altered to create more appropriate labels by adding spaces and removing abbreviations. In addition, the Curre ncy property for the FSalary field object is set to True so that values always appear formatted.
To provide validation, the Validate method is overridden. In this method, the salary value is compared with the salary cap, and if the cap is exceeded, a validation error is added to the Error Handler. The TRzErrorHandler component, the third piece of th e business component architecture, provides a clean and consistent way of recording and displaying error and warning messages. In addition, the TRzBusinessComponent can be connected to an error handler. If it is, the error handler will be used to display any error messages automatically, and if an error is recorded, the business component will raise an EValidationError exception to abort the post.
Figure 3 shows the same form after the BizEmployee.MainDataset property is set to TblEmployee. When the assignment is made, all of the field objects defined in the TRzDBEmployee class are automatically created. The effects of the a ssignment are apparent in the Fields Editor and the grid. For example, the EmpNo column is no longer displayed, and all of the headers have been changed.
But the most dramatic change is the addition of the Monthly Salary column, which corresponds to the MonthlySal calculated field created by the business component. Notice, however, that the Monthly Salary column is populated even though the application is still in design mode! This occurs because the TRzDBEmployee class overrides the CalculateFields method from the base class.
Figure 4 illustrates all three pieces of the architecture at work when a user tries to enter a new employee with a salary that exceeds the salary cap of $60,000.00. (The salary cap is specified using the SalaryCap property of TRzDB Employee.) In this figure, the user has just pressed the Post button on the navigator, and the Validation Errors dialog box is displayed. Notice that the dialog can display multiple messages. In this example, the TRzDBEmployee component issues a warning if a new employee is entered without a first name.
Encapsulating business rules in Delphi components makes it easy to reuse business rule logic in a consistent manner. It also enables Delphi developers to continue using data-aware controls. By using these techniques, you can architect an application that cleanly separates the user interface layer, the data storage and access layer, and the business rules layer. Finally, you can apply the concepts described in this article to the other types of logic that you must incorporate in your Delphi applications.
Editor's note: Tom Spitzer will return as our Desktop DBMS columnist next month. See his feature article "A Database Perspective on GIS (Part 1)" in this issue.




unit RzDBEmp;
interface
uses
Classes, DB, DBTables, SysUtils, RzBizCmp, RzErrDlg;
type
EExceedSalaryCap = class( Exception );
TRzDBEmployee = class( TRzBusinessComponent )
private
{ Field Objects }
FEmpNo : TIntegerField;
FLastName : TStringField;
FFirstName : TStringField;
FHireDate : TDateTimeField;
FSalary : TFloatField;
{ Calculated Field Object }
FFullName : TStringField;
FMonthlySal : TFloatField;
{ Business Property }
FSalaryCap : Single;
protected
procedure CreateFields; override;
procedure CalculateFields; override;
procedure Validate; override;
procedure FinishInsert; override;
public
{ Methods Implementing Business Rules }
function GetFullName : string;
function YearsOfService : Single;
function TotalVacationDays : Integer;
{ Specify properties to enforce read-only attribute }
property EmpNo : TIntegerField
read FEmpNo;
property LastName : TStringField
read FLastName;
property FirstName : TStringField
read FFirstName;
property FullName : TStringField
read FFullName;
property HireDate : TDateTimeField
read FHireDate;
property Salary : TFloatField
read FSalary;
property MonthlySal: TFloatField
read FMonthlySal;
published
property SalaryCap : Single
read FSalaryCap
write FSalaryCap;
end;
procedure Register;
implementation
uses
Dialogs;
{===========================}
{== TRzDBEmployee Methods ==}
{===========================}
{= TRzDBEmployee.CreateFields
=}
{= This method specifies the Field Objects that are to be used by
the =}
{= Employee Business Component.
=}
procedure TRzDBEmployee.CreateFields;
begin
FEmpNo := CreateMainField( 'EmpNo' ) as TIntegerField;
FEmpNo.Visible := False;
FLastName := CreateMainField( 'LastName' ) as TStringField;
FLastName.DisplayLabel := 'Last Name';
FLastName.Visible := False;
FFirstName := CreateMainField( 'FirstName' ) as TStringField;
FFirstName.DisplayLabel := 'First Name';
FFirstName.Visible := False;
FFullName := CreateMainCalcField( 'FullName', TStringField, 30 ) as
TStringField;
FFullName.DisplayLabel := 'Full Name';
FHireDate := CreateMainField( 'HireDate' ) as TDateTimeField;
FHireDate.DisplayLabel := 'Hire Date';
FHireDate.EditMask := '!99/99/00;1;_';
FSalary := CreateMainField( 'Salary' ) as TFloatField;
FSalary.Currency := True;
FMonthlySal := CreateMainCalcField( 'MonthlySal', TFloatField, 0 )
as TFloatField;
FMonthlySal.DisplayLabel := 'Monthly Salary';
FMonthlySal.Currency := True;
end; {= TRzDBEmployeeObject.CreateFields =}
function TRzDBEmployee.GetFullName : string;
begin
Result := FFirstName.Value + ' ' + FLastName.Value;
end;
function TRzDBEmployee.YearsOfService : Single;
begin
Result := Trunc( Now - FHireDate.Value ) / 365;
end;
{= TRzDBEmployee.TotalVacationDays
=}
{= This method calculates the number of vacation days available for
the =}
{= current year. The number is based on how long the employee has
been =}
{= with the company. The number is scaled according to the
employee's =}
{= years of service. The scale is as follows:
=}
{=
=}
{= Less than 6 months = 0 days
=}
{= Less than 5 years = 10 days (2 Weeks)
=}
{= 5 to 9 years = 15 days (3 Weeks)
=}
{= 10 to 14 years = 20 days (4 Weeks)
=}
{= 15 to 19 years = 25 days (5 Weeks)
=}
{= ...
=}
function TRzDBEmployee.TotalVacationDays : Integer;
var
YOS : Real;
begin
YOS := YearsOfService;
if YOS < 0.5 then
Result := 0
else
Result := ( Trunc( YOS ) div 5 + 2 ) * 5;
end;
{= TRzDBEmployee.Validate
=}
{= This method gets called whenever the Salary column in the table
is =}
{= updated. If the Salary exceeds the salary cap, an exception is
raised =}
{= to abort the current process. For example, if the salary cap is
set to =}
{= 70,000 and the user enters 85,000 and then tries to Post the
change, the =}
{= exception will cause the Post to fail.
=}
procedure TRzDBEmployee.Validate;
begin
if ( FSalaryCap <> 0 ) and ( FSalary.Value > FSalaryCap ) then
ErrorHandler.AddError( Format( 'Salary cannot exceed %m.', [
FSalaryCap ] ),
etValidation );
if FirstName.Value = '' then
ErrorHandler.AddError( 'First Name is not specified.', etWarning
);
end;
procedure TRzDBEmployee.CalculateFields;
begin
FMonthlySal.Value := FSalary.Value / 12;
FFullName.Value := GetFullName;
end;
procedure TRzDBEmployee.FinishInsert;
begin
FHireDate.Value := Date;
end;
{========================}
{== Register Procedure ==}
{========================}
procedure Register;
begin
RegisterComponents( 'Raize', [ TRzDBEmployee ] );
end;
end.
Listing 1--The RzDBEmp unit implements the TRzDBEmployee class. This class
represents an abstract class that is used as the base class for
building data-aware business object components. (Source code Copyright c 1995, 1996 by Raize Software Solutions, Inc.)