Lists and Collections
Home Up

 

Note, I might eventually re-jig this to be an article on all the ways of implementing One to Many in Delphi.

Implementing Collections / Aggregation in Delphi

HEAVY-WEIGHT:  The TCollection Pattern

The TCollection and TCollectionItem classes provided by Delphi are supposed to make it easy to implement aggregation, which means one class has a list of instances of some other class. Aggregation is a common pattern you will be using a lot in programming. e.g. to implement 'one to many' relationships.

For example, a THairs object might have a collection of THair objects.
A TCustomer might have a collection of TOrder objects.

A detailed examination of this pattern and a step by step tutorial is described later in this article.

LIGHT-WEIGHT:  Using TList or TObjectList

The two typical methods of building a low tech list or a collection in Delphi is via derivation or via encapsulation.  Here is some code by Natalie Vincent contrasting both approaches.  Read on if you would like some detailed explanation, followed by an examination of an alternative heavy weight approach, the TCollection pattern.

A simplest solution? - Derive a class from TObjectList

You can use a TList or TObjectList for your collections.  If you want to have them store only objects of a certain type (other than TObject), then you can subclass TList or TObjectList and do some internal casting to that type.  See example code, at the end of this article.

Although this technique is probably the simplest, some, like Delphi guru Danny Thorpe says not to inherit from 'worker classes' like TList (see his brilliant book Delphi Component Design 1997 p. 55) where he says:

"You'll almost always wind up using a TList in a component, but you'll never create descendants of TList.  TList is a worker drone, not a promiscuous ancestor class."

he goes on to say:

"If you want to create a list of a particular kind of item and that list is used only in the private or protected sections of your component implementation, then just use a plain TList and typecast the list's raw pointers into the type you put into the list to begin with. 

If the list is exposed in the public interface of your component, typecasting is not an option.  You cannot require a component user to typecast a list item appropriately in order to access it.  You should create a simple wrapper class (derived from TObject, not TList) that exposes only the property and function equivalents of TList that you need and whose function types and function equivalents of TList the you need and whose function return types and method parameter types match the type of data that the list contains. The wrapper class then contains a TList instance internally and uses that for actual storage."

Well, in practice it seems that you can get a lot of mileage from subclassing TList or TObjectList.  See example code, at the end of this article.

Another simple solution - a wrapping class - Encapsulation

So, another way of  implementing aggregation or composition in Delphi is to create a new class and inside, use a TList property.  This TList (or TObjectList) object would typically be created inside the containing class's constructor.   Tlist classes have all the appropriate .Add, .Remove, .Find, .GetItem (to support array properties that use the Items[] syntax)  methods you need to add and remove instances to your containing class.  All you may want to do is create your own versions of these methods, which simply delegate to the internal TList or TObjectList methods.  You will probably do some casting so that the collection is customised to deal with objects of a certain class.  See more below

The need for wrapper functions and casting

Why cast? Well, the because client code that uses the container class's TList would have to constantly typecast the result of the TList methods into the appropriate class type it is expecting, since whilst TLists can store any object of any class, objects are treated as TObjects.  So whilst you may be storing away THair objects, this type knowledge is temporarily lost once TList gets its hands on it.  Of course the object is still a THair even when it is being stored away and treated as a common TObject by TList  ;-)   So often a savvy programmer will create .Add and .Remove etc. wrapper functions in the container class that are defined to return exactly the type of class being stored.  These functions simply wrap the Tlist functionality with a typecast e.g.;

var
  FBirds : TList;    
function TBirds.GetBirds(Index: Integer): TBird;
begin
  result := TBird( FBirds[Index] );
end;    

so rather than everyone doing

  TBird( Birds[50] )    

they only have to do

  Birds[50]    

in order to get a TBird object out of the FBirds list.  Without the wrapping class, or without the casting, you would have to store the result in a TObject variable, because that's what TList natuarally stores.

The Tlist build it yourself syndrome

TCollection uses TList in its implementation, which proves that the TList approach is a valid approach. However it does get tedious hand crafting every one to many relationship in this way - it probably takes me about 10 minutes to get everything just right, perhaps more when I have indexed properties and remove methods.



ASIDE:   Aggregation vs. Composition

Note that aggregation and composition are the same concept, except composition means the container class owns the contained objects.   When the container object is deleted, the contained objects will also be deleted.   If you want composition behaviour, use TObjectList instead of a TList since TObjectList  frees its children in exactly this way (you need to have Delphi 5 to use TObjectList).



ASIDE: A python Zen approach

In a dynamically typed language you don't need wrapper functions which do typecasting, since whilst types are associated with objects, variables and lists don't care what they refer to.  I wrote this in about two minutes and shows how in Python you just 'get on with it' and work on the client problem, rather than fighting the implementation language.

class PeopleManager:
    def __init__(self):
        self.People = []
    def Add(self, person):
        self.People.append(person)
class Person:
    def __init__(self,name,age):
        self.Name = name
        self.Age = age

m = PeopleManager()
m.Add( Person('Andy',38) )
m.Add( Person('Harry',28) )

print m
print len(m.People)          



The TCollection Pattern

The merits (if any) of TCollection

For:  Power when used within Delphi component framework

TCollections are also mostly used as part of Delphi's component system, so they offer streaming support, .Assign support, and component ownership support, including the ability to notify owning components whenever a contained object has changed.  All this is great if you need it (to play ball with Delphi's designtime visual properties system), but may be confusing if you just want a simple collection.

Against:  Still need wrapper functions

Are there any less steps in using TCollection?  Well, as you will see, you still have to write wrapper functions that do typecasting when you use TColleciton.  So despite the fact that you tell the TCollection constructor the class of the object that the collection comprises, this does not save you from having to write these sorts of wrapper functions.  The only difference is that TCollection .Add calls the inherited .Add before the typecast, whilst in a homegrown solution you would call your internal Tlist .Add before the typecast - hardly a great saving.

Against:  TCollection restricts how you create instances of your collection

If truth be known, TCollection actually restricts you a little bit more since the rules of TCollection ask that you not instantiate the individual objects of your collection yourself, but instead, that you call .Add of the TCollection based class - which in turn knows what sort of object to create because you told it so when you constructed it.  This can cramp your style - or you can live with it, its probably not a big deal.

Against:  Inheritance tree rigidity

To use the TCollection pattern, you need to create two classes, one of which inherits from TCollection and the other from TCollectionItem.   However you may not want the inheritance tree of your classes to be restricted in this way.  Perhaps if Borland implemented TCollection and TCollectionItem as interfaces, things would be easier.  But then again, TCollection and TCollectionItem actually implement some functionality which you want to inherit, so perhaps its best that they are classes after all.

Against:   Cannot hold multiple classes (even if they are derived from the same base type)

Every object in the colleciton needs to be of the same class.  If you want to hold a collection of TPeople some of which are TManagers and TEmployees etc. then you are out of luck - once you register that the collection is going to hold TPeople - that's all that will be created (via the .Add). 

Not that holding a bunch of different classes (even if they are derived from the same base type) in the one collection is necessarily a good idea.

On the other hand, a regular TList solution can handle any bunch of classes, though your wrapper functions might have to be smart.  Or to hell with the wrapper functions and get you client code to just say if o is TPerson then TPerson(o) etc.

Against:   Complexity and confusion

Another obstacle to using TCollection  is that there are quite a few things TCollections are capable of and you will be confused as to what method does what and which to override or replace etc.  Here are the two classes as found in classes.pas in the Delphi VCL class library:

TCollection.gif (8763 bytes)

The simplest way to use TCollection

Here are the steps to using TCollection as an alternative to the TList approach discussed above.  We will create a THairs class which holds a collection of THair objects.

Create the pair of classes you want

bulletCreate THairs and subclass from TCollection.
bulletCreate THair and subclass from TCollectionItem.
bulletAdd as many published properties to THair as you need e.g. Length
type
  THair = class (TCollectionItem)
  private
    FLength: Integer;
  published
    property Length: Integer read FLength write FLength;
  end;      

Now we get to the more complicated bit - though we want to write as little code as possible.  We want the classes we inherited from (TCollection and TCollectionItem) to do as much work as possible.  If we end up doing too much work ourselves, then we might as well revert to our TList solution, above.

Usage:  TCollection asks that you don't directly create instances of the things being collected

Using TCollection and TCollectionItem based classes goes like this:  When you .Create(...) your TCollection class you should pass the class name of the contained class as a parameter thus:

var
  hairs : THairs;
begin
  hairs := THairs.Create(THair);      

The rules of TCollection then ask you not to instantiate individual THair objects yourself, but instead, that you call THairs.Add which in turn knows what sort of object to create because you told it so when you constructed it (by passing in the classname of THair to its constructor, see above code snippet).  So we actually do this:

var
  ahair : THair;
begin
  ahair := hairs.Add;  // create one hair
  ahair := hairs.Add;  // create another hair      

Create Add and GetItem methods that wrap and typecast TCollectionItem into the class you want

Before we can use the code shown above, we still have to build our TCollection based class.

What we do next then, is create a couple of methods in the container class THairs, which replace the methods of the parent class TCollection.  And all we do is call the inherited methods and typecast the result.  We don't override the methods, because overridden methods or function must return the same type as the methods or functions they are overriding.  And the whole point here is to change the type these methods return from TCollectionItem to THair.   Of course the actual type of the object being automatically created by TCollection class is THair, but it is returned

bulletCreate an array property Item with getter method GetItem.   Define these as returning THair types.
bulletCreate an Add method which returns a THair type.
 THairs = class (TCollection)
 private
   function GetItem(Index: Integer): THair;
 public
  function Add: THair;
  property Item[Index: Integer]: THair read GetItem;
 end;      
bulletImplement these methods with calls to the inherited class (TCollection) method.   The only thing extra we do is typecast the results to THair.
function THairs.Add: THair;
begin
  result := inherited Add as THair;
end;

function THairs.GetItem(Index: Integer): THair;
begin
  result := inherited Items[Index] as THair;
end;      

That's it.  You can now use the classes.  See above section on how to use them, or see below on a fuller example of how to drive them.

Enhancements

You can create a setter method SetItem if you want to set items in the list.  And add a Remove method if you need it.  You can even add a .AddEx method so that you can pass parameters to the Add call,

function THairs.AddEx(length : integer): THair;
begin
  result := inherited Add as THair;
  result.Length := length;
end;      

THairs.gif (2646 bytes)

which can then be used thus:

var
  m : THairs;
  i : integer;
begin
  m := THairs.Create(THair);
  m.AddEx( 20 );
  m.AddEx( 25 );
  m.AddEx( 30 );
  for i := 0 to m.Count-1 do
    memo3.lines.add('Hair ' + 
    inttostr( i )  + ' length ' + 
    inttostr( m.item[i].length) );
end;      

Modelmaker fans

If you are a fan of modelmaker then you can use this template (add it to your c:/program files/Modelmaker/templates folder and register it on the design patterns page by right clicking on the templates toolbar and selecting register template then pointing to a file e.f. collection_simple.pas which contains the following:

unit Collection_simple;

//DEFINEMACRO:TPerson=class of thing being collected

  TCodeTemplate = class (TCollection)
  private
    function GetItem(Index: Integer): <!TPerson!>;
  public
    function Add: <!TPerson!>;
    property Item[Index: Integer]: <!TPerson!> read GetItem;
  end;


implementation

{
*** TCodeTemplate ***
}
function TCodeTemplate.Add: <!TPerson!>;
begin
  result := inherited Add as <!TPerson!>;
end;

function TCodeTemplate.GetItem(Index: Integer): <!TPerson!>;
begin
  result := inherited Items[Index] as <!TPerson!>;
end;

end.      

Then you you can build tghe methods of your TCollection class in a jiffy by simply creating a class which inherits from TCollection.  Select this class and run the collection_simple template.  You will be prompted for the class you have a collection of.  The default is TPerson (a silly default, but there for historical reasons).   Change this to say, THair and hit OK.  All your methods are done!  Of course you need to create your THair class as well.

-Andy Bulka

abulka@netspace.net.au

Another way of doing collections - subclass TObjectList

Although Danny Thorpe says not to inherit from 'worker classes' like TList, here is an example of creating a class based on TObjectList (same as TList except it frees the objects it owns).

  { List Of HTTPFile Objects }
  THTTPFiles = class(TObjectList)
  private
    FOwnsObjects: Boolean;
  protected
    function GetItem(Index: Integer): THTTPFile;
    procedure SetItem(Index: Integer; AObject: THTTPFile);
  public
    function Add(AObject: THTTPFile): Integer;
    function Remove(AObject: THTTPFile): Integer;
    function IndexOf(AObject: THTTPFile): Integer;
    procedure Insert(Index: Integer; AObject: THTTPFile);
    property OwnsObjects: Boolean read FOwnsObjects write FOwnsObjects;
    property Items[Index: Integer]: THTTPFile read GetItem write SetItem; default;
  end;
{ THTTPFiles }
function THTTPFiles.Add(AObject: THTTPFile): Integer;
begin
  Result := inherited Add(AObject);
end;
function THTTPFiles.GetItem(Index: Integer): THTTPFile;
begin
  Result := THTTPFile(inherited Items[Index]);
end;
function THTTPFiles.IndexOf(AObject: THTTPFile): Integer;
begin
  Result := inherited IndexOf(AObject);
end;
procedure THTTPFiles.Insert(Index: Integer; AObject: THTTPFile);
begin
  inherited Insert(Index, AObject);
end;
function THTTPFiles.Remove(AObject: THTTPFile): Integer;
begin
  Result := inherited Remove(AObject);
end;
procedure THTTPFiles.SetItem(Index: Integer; AObject: THTTPFile);
begin
  inherited Items[Index] := AObject;
end;

Above code taken from http://www.matlus.com/scripts/website.dll File upload (multi/part form data) example.

The minimalist approach of subclassing TObjectList.

Note that you actually don't have to define so many methods to inherit from TObjectList.  All you really need to define are any methods that involve casting to the type you want. Thanks to Natalie Vincent for this insight.

  TCarList = class(TObjectList)
  private
    function getcar(aindex: integer): TCar;
    procedure setcar(aindex: integer; const Value: TCar);
  public
    property items[aindex: integer] : TCar read getcar write setcar; default;
    function add(acar:TCar): integer;
  end;
implementation
{$R *.DFM}
{ TCarList }
function TCarList.add(acar: TCar): integer;
begin
  // This method not strictly necessary, but ensures that can only add TCar objects.
  Result := inherited Add(acar);
end;
function TCarList.getcar(aindex: integer): TCar;
begin
  result := inherited Items[aindex] as TCar;
end;
procedure TCarList.setcar(aindex: integer; const Value: TCar);
begin
  inherited Items[aindex] := Value;
end;

Using the minimalist approach to TCarList 

  TCar = class(TObject)
    function beep: string; virtual;
  end;
  TFord = class(TCar)
    function beep: string; override;
  end;
  TPorche = class(TCar)
    function beep: string; override;
  end;
procedure TForm1.FormShow(Sender: TObject);
var
  cars : TCarList;
  car : TCar;
  i : integer;
begin
  cars := TCarList.create;
  cars.add( TCar.create );
  cars.add( TFord.create );
  cars.add( TPorche.create );
  cars.add( TFord.create );
  cars.add( TFord.create );
  for i := 0 to cars.Count-1 do
    memo1.Lines.Add(cars[i].beep);

end;

Note that that cars[i] is accessing a TCar (rather than a TObject), since the casting is ocurring for us in the TCarList class.