PROMOTIONAL ARTICLE

This article is taken from "Delphi COM Programming" - very successful book written by Eric Harmon, that was released in January, 2000 (Macmillian). By the end of May 2000, the polish translation by Paweł Głowacki (“Programowanie COM w Delphi”) will be available. The material covered in paper deals with interfaces as a pure Object Pascal language feature.
The publication of this material is permitted by copyright owner.

Programming with Interfaces

So far in this chapter, we really haven’t looked at any real-life examples of using interfaces in your Delphi code. Let’s stop now and look at an example that puts interfaces to good use in a familiar programming concept: sorting.

SortDemo Example: Algorithms with Interfaces

Many programs make use of sorting to some extent. Simpler programs might use a well-placed bubble sort algorithm here or there, whereas more complex programs may require a workhorse quicksort or mergesort algorithm. Usually when you implement a sort routine in your application, it is tightly bound to the data you want to sort. How many times have you written code such as the following?

procedure SortEmployees;
// - sort an array of employees by name
var
  I, J: Integer;
begin
  for I := 1 to MAX_EMPLOYEES - 1 do
    for J := I + 1 to MAX_EMPLOYEES do
      if EmployeeArray[J].Name < EmployeeArray[I].Name then begin
        Temp := EmployeeArray[I];
        EmployeeArray[I] := EmployeeArray[J];
        EmployeeArray[J] := Temp;
      end;
end;

The only thing this procedure is ever going to sort is an array of employee records. You probably justify implementing the procedure this way because it’s just a quick bubble-sort procedure, and you can bang out the code so fast that you don’t even have to think about it.

Wouldn’t it be nicer to have a stock routine in your bag of tricks that could be used not only for sorting, but also for ordering elements in a sorted list, and anywhere else a comparison function is needed? Using interfaces, you can accomplish this easily.

Let’s define an interface called ICompare. ICompare’s job will be to compare an object (ObjectA) to another similar object (ObjectB), and return -1 if ObjectA comes before ObjectB, 0 if the two objects are equal, or 1 if ObjectA comes after ObjectB. To make this example more useful, I’m going to add to this interface the capability to support multiple ordering methods for an object. Here is the interface declaration:

type
  ICompare = interface
    [‘{DDFE0840-E8FB-11D2-9085-0040F6741DE2}’]
    function CompareWith(ACompareTo: ICompare; AOrderBy: Integer): Integer;
  end;

This interface’s sole purpose in life is to provide an object with the means of ordering itself in some way. This illustrates another important point about interfaces: When creating an interface, make it as specific as possible. You don’t want to create the Swiss army knife of interfaces. An interface that is multipurpose will not be useful in very many situations. More than likely, it will be specific only to a single application you’re writing. Instead of one general-purpose interface, create several smaller, more focused interfaces that you can reuse in many different situations.

The usage for this interface will be like this:
if MyObject1.CompareWith(MyObject2, SortIndex) < 0 then
  // MyObject1 comes before MyObject2

The parameter AOrderBy is intended to let this interface’s implementor define many ordering methods. For example, a TStudent class in a college enrollment program might need to order students by name or social security number. The TStudent class can define the number 1 to mean order by name, and the number 2 to mean order by social security number.

Listing 1.7 shows the source code for unit IntfUnit, which declares the ICompare interface and implements a simple, yet reusable, bubble sort.

Listing 1.7  SortDemo Source: intfunit.pas

unit IntfUnit;
interface
type
  ICompare = interface
    [‘{DDFE0840-E8FB-11D2-9085-0040F6741DE2}’]
    function CompareWith(ACompare: ICompare; ASortBy: Integer): Integer;
  end;
procedure SortArray(var A: Array of IUnknown; ASortBy: Integer);

implementation
procedure SortArray(var A: Array of IUnknown; ASortBy: Integer);
var
  I, J: Integer;
  Temp: IUnknown;
begin
  for I := Low(A) to High(A) -1 do begin
    for J := I + 1 to High(A) do begin
      if (A[J] as ICompare).CompareWith(A[I] as ICompare, ASortBy) < 0 then
      begin
        Temp := A[I];
        A[I] := A[J];
        A[J] := Temp;
      end;
    end;
  end;
end;  
end.

Note that SortArray takes an array of IUnknown. If I passed in an array of TInterfacedObjects, for example, then the following line of code would cause the objects to be destroyed prematurely:

if (A[J] as ICompare).CompareWith(A[I], AOrderBy) < 0 then begin

The as operator would cause Delphi to call _AddRef and _Release on the ICompare interface, which would destroy all the objects I’m sorting right in the middle of the sort. This is a classic example of when mixing reference models can get you into trouble.

The SortDemo example defines two different classes that implement the ICompare interface: TEmployee and TInventoryItem. These classes are defined in the units EmpUnit and InvUnit, respectively. Listing 1.8 shows the source code for the EmpUnit unit.

Listing 1.8  SortDemo Source: empunit.pas
unit EmpUnit;
interface
uses
  SysUtils, IntfUnit;
type
  TEmployeeOrder = (eoName, eoSalary);
  IEmployee = interface
    [‘{FFCD24F0-4FE8-11D3-B84D-0040F67455FE}’]
    function GetName: string;
    function GetSalary: Double;
  end;

TEmployee = class(TInterfacedObject, IEmployee, ICompare)
  private
    FName: string;
    FSalary: double;
  public     constructor Create(AName: string; ASalary: Double);
    function CompareWith(ACompare: ICompare; ASortBy: Integer): Integer;
    function GetName: string;
    function GetSalary: Double;
  end;

implementation
{ TEmployee }

function TEmployee.CompareWith(ACompare: ICompare; ASortBy: Integer): Integer;
var
  Emp: IEmployee;
begin
  Result := 0;
  Emp := ACompare as IEmployee;
  case ASortBy of
    Ord(eoName):
      Result := CompareStr(FName, Emp.GetName);
    Ord(eoSalary):
      if FSalary < Emp.GetSalary then
        Result := -1
      else if FSalary > Emp.GetSalary then
        Result := 1
      else
        Result := 0;
  end;
end;

constructor TEmployee.Create(AName: string; ASalary: Double);
begin
  inherited Create;
  FName := AName;
  FSalary := ASalary;
end;

function TEmployee.GetName: string;
begin
  Result := FName;
end;

function TEmployee.GetSalary: Double;
begin
  Result := FSalary;
end;

end.

TEmployee is a simple class used to track employee names and salaries. Employees can be sorted by either name or salary. I have implemented the enumerated type TEmployeeOrder to specify valid ordering methods for the TEmployee class.

Listing 1.9 shows the source code for the InvUnit unit.

Listing 1.9  SortDemo Source: invunit.pas
unit InvUnit;
interface
uses
  SysUtils, IntfUnit;
type
  TInventoryItemOrder = (iioPartNumber, iioDescription, iioUnitPrice, iioInStock);
  IInventoryItem = interface
    [‘{FFCD24F1-4FE8-11D3-B84D-0040F67455FE}’]
    function GetPartNumber: string;
    function GetDescription: string;
    function GetInStock: Integer;
    function GetUnitPrice: Double;
  end;
  TInventoryItem = class(TInterfacedObject, IInventoryItem, ICompare)
  private
    FPartNumber: string;
    FDescription: string;
    FInStock: Integer;
    FUnitPrice: Double;
  public
    constructor Create(APartNumber: string; ADescription: string; AInStock: Integer; AUnitPrice: Double);
    function CompareWith(ACompare: ICompare; ASortBy: Integer): Integer;
    function GetPartNumber: string;
    function GetDescription: string;
    function GetInStock: Integer;
    function GetUnitPrice: Double;
  end;

implementation
{ TInventoryItem }
function TInventoryItem.CompareWith(ACompare: ICompare; ASortBy: Integer): Integer;
var
  Inv: IInventoryItem;
begin
  Result := 0;
  Inv := ACompare as IInventoryItem;
  case ASortBy of
    Ord(iioPartNumber):
      Result := CompareStr(FPartNumber, Inv.GetPartNumber);
    Ord(iioDescription):
      Result := CompareStr(FDescription, Inv.GetDescription);
    Ord(iioInStock):
      Result := FInStock - Inv.GetInStock;
    Ord(iioUnitPrice):
      if FUnitPrice < Inv.GetUnitPrice then
        Result := -1
      else if FUnitPrice> Inv.GetUnitPrice then
        Result := 1
      else
        Result := 0;
  end;
end;

constructor TInventoryItem.Create(APartNumber, ADescription: string; AInStock: Integer; AUnitPrice: Double);
begin
  inherited Create;
  FPartNumber := APartNumber;
  FDescription := ADescription;
  FInStock := AInStock;
  FUnitPrice := AUnitPrice;
end;

function TInventoryItem.GetDescription: string;
begin
  Result := FDescription;
end;

function TInventoryItem.GetInStock: Integer;
begin
  Result := FInStock;
end;

function TInventoryItem.GetPartNumber: string;
begin
  Result := FPartNumber;
end;

function TInventoryItem.GetUnitPrice: Double;
begin
  Result := FUnitPrice;
end;

end.

TInventoryItem is similar in nature to TEmployee, but it is used to track inventory items, and can be sorted by part number, description, unit price, or quantity in stock (iioPartNumber, iioDescription, iioUnitPrice, or iioInStock).

The reason for creating these two disparate classes is to show you that you can easily use the same ICompare interface to sort completely independent sets of data.

Listing 1.10 shows the code for SortDemo’s main form.

Listing 1.10  SortDemo Source: mainform.pas

unit MainForm;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,   Dialogs, StdCtrls, ComCtrls, IntfUnit, EmpUnit, InvUnit;

const
  MAX_EMPLOYEES = 5;
  MAX_INVENTORYITEMS = 3;

type
  TfrmMain = class(TForm)
    PageControl1: TPageControl;
    tabBubbleSort: TTabSheet;
    tabListView: TTabSheet;
    lbEmployees: TListBox;
    Label1: TLabel;
    btnByName: TButton;
    btnBySalary: TButton;
    lvInventory: TListView;
    Label2: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure btnByNameClick(Sender: TObject);
    procedure btnBySalaryClick(Sender: TObject);
  procedure lvInventoryColumnClick(Sender: TObject; Column: TListColumn);
    procedure lvInventoryCompare(Sender: TObject; Item1, Item2: TListItem; Data: Integer; var Compare: Integer);
  private
    { Private declarations }
    FEmpArray: Array[1 .. MAX_EMPLOYEES] of IUnknown;
    FInvArray: Array[1 .. MAX_INVENTORYITEMS] of IUnknown;
    procedure LoadListBox;
    procedure CreateInventoryItem(AIndex: Integer; APartNo,
    ADescription: string; AInStock: Integer; AUnitPrice: Double);
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation
{$R *.DFM}

procedure TfrmMain.CreateInventoryItem(AIndex: Integer; APartNo: string; ADescription: string; AInStock: Integer; AUnitPrice: Double);
var
  ListItem: TListItem;
begin
  FInvArray[AIndex] := TInventoryItem.Create(APartNo, ADescription,AInStock, AUnitPrice);
  ListItem := lvInventory.Items.Add;
  ListItem.Caption := APartNo;
  ListItem.SubItems.Add(ADescription);
  ListItem.SubItems.Add(FloatToStrF(AUnitPrice, ffCurrency, 5, 2));
  ListItem.SubItems.Add(IntToStr(AInStock));
  ListItem.Data := Pointer(FInvArray[AIndex]);
end;

procedure TfrmMain.FormCreate(Sender: TObject);
begin
    // Create some employees
  FEmpArray[1] := TEmployee.Create(‘Smith, Tom’, 19200.00);
  FEmpArray[2] := TEmployee.Create(‘Doe, John’, 38000.00);
  FEmpArray[3] := TEmployee.Create(‘Williams, Fred’, 26500.00);
  FEmpArray[4] := TEmployee.Create(‘Jones, Bob’, 90000.00);
  FEmpArray[5] := TEmployee.Create(‘Adams, Tim’, 42500.00);

  // Load employees into the list box
  LoadListBox;
  // Create some inventory items
  CreateInventoryItem(1, ‘P5409’, ‘Widget’, 35, 1.19);
  CreateInventoryItem(2, ‘X1234’, ‘Gadget’, 4, 14.95);
  CreateInventoryItem(3, ‘J7749’, ‘Doodad’, 17, 8.79);

end;

procedure TfrmMain.LoadListBox;
var
  Index: Integer;
  Emp: IEmployee;
begin
  lbEmployees.Items.BeginUpdate;
  try
    lbEmployees.Items.Clear;
    for Index := 1 to MAX_EMPLOYEES do begin
      Emp := FEmpArray[Index] as IEmployee;
      lbEmployees.Items.Add(Emp.GetName + #9 +
        FloatToStrF(Emp.GetSalary, ffCurrency, 8, 2));
    end;
  finally
    lbEmployees.Items.EndUpdate;
  end;
end;

procedure TfrmMain.btnByNameClick(Sender: TObject);
begin
  SortArray(FEmpArray, Ord(eoName));
  LoadListBox;
end;

procedure TfrmMain.btnBySalaryClick(Sender: TObject);
begin
  SortArray(FEmpArray, Ord(eoSalary));
  LoadListBox;
end;

procedure TfrmMain.lvInventoryColumnClick(Sender: TObject; Column: TListColumn);
begin
  lvInventory.CustomSort(nil, Column.Index)
end;

procedure TfrmMain.lvInventoryCompare(Sender: TObject; Item1, Item2: TListItem; Data: Integer; var Compare: Integer);
var
  I1, I2: IUnknown;
begin
  I1 := IUnknown(Item1.Data);
  I2 := IUnknown(Item2.Data);
  Compare := (I1 as ICompare).CompareWith((I2 as ICompare), Data);
end;

end.  

TfrmMain’s FormCreate method creates an array of employees and an array of inventory items. Obviously, a real application would get its data from a database or data file of some type, but these hardcoded arrays will suffice for demonstration purposes.

When you run this program, you’ll see a TPageControl with two pages on it. The first page contains a TListBox that is used to display the list of employees.

Figure 1.3 shows the first page of the SortDemo application.


Figure 1.3  SortDemo application: Employee list.

By default, the employee names are displayed in the order that we created them. Clicking on the Sort by Name or Sort by Salary buttons sorts the employee array by name or salary, respectively. The sort routine used is a simple bubble sort that orders the objects based on the ICompare interface’s CompareWith method. Click the List View tab to display the list of inventory items, again, in the order that we created them. Click any of the four headers: Part #, Description, Unit Price, or In Stock to sort the list by that column. This code uses the ICompare interface in a completely different manner.

Figure 1.4 shows the second page of the SortDemo application.


Figure 1.4  SortDemo application: Inventory item list.

If you’re not familiar with the TListView’s sorting capabilities, take a look at the following code snippet from Listing 1.10:


procedure TfrmMain.lvInventoryColumnClick(Sender: TObject; Column: TListColumn);

begin
  lvInventory.CustomSort(nil, Column.Index)
end;

procedure TfrmMain.lvInventoryCompare(Sender: TObject; Item1, Item2: TListItem; Data: Integer; var Compare: Integer);
var
  I1, I2: IUnknown;
begin
  I1 := IUnknown(Item1.Data);
  I2 := IUnknown(Item2.Data);
  Compare := (I1 as ICompare).CompareWith((I2 as ICompare), Data);
end;
 

TListView’s CustomSort method uses the method assigned to its OnCompare event to compare two items in the list view. Because I used the Data property of the list view items to hold a pointer to the TInventoryItem’s IUnknown interface, I can simply typecast the Data property back to an IUnknown and call the TInventoryItem.CompareWith method to compare the items.

Obviously, TListView has its own internal sort engine that calls TListView’s OnCompare method at the appropriate times. What’s important to understand here is that by declaring the ICompare interface generically, it can be used in numerous instances. If instead of declaring an ICompare interface, we had declared an ISort interface, then that code would not have been usable in this particular situation.

At this point, we’ve pretty well covered the basics of interfaces. The next section discusses some additional features of interfaces that are slightly more advanced.

END OF PROMOTIONAL ARTICLE
This article was taken from Eric Harmon’s “Delphi COM Programming” (Macmillian Publishing). Polish translation – “Programowanie COM w Delphi”(TRANSLATOR S.C.) - by Paweł Głowacki will be available by the end of May, 2000.