|
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 DelphiHEAVY-WEIGHT: The TCollection PatternThe 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 detailed examination of this pattern and a step by step tutorial is described later in this article. LIGHT-WEIGHT: Using TList or TObjectListThe 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 TObjectListYou 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:
he goes on to say:
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 - EncapsulationSo, 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 castingWhy 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 syndromeTCollection 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.
|
Create THairs and subclass from TCollection. | |
Create THair and subclass from TCollectionItem. | |
Add 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.
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
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
Create an array property Item with getter method GetItem. Define these as returning THair types. | |
Create 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;
Implement 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.
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;
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;
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
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.
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;
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.