* * *

Author Topic: Issues with TCollection.Add and polymorphic objects creation  (Read 994 times)

speleomania

  • New member
  • *
  • Posts: 8
Issues with TCollection.Add and polymorphic objects creation
« on: February 02, 2017, 11:16:58 pm »
Need some advice with the following design concept:

Assuming this nested structure:
TCountry = class;
TCities = class(TCollection)
TCity = class(TCollectionItem);
TBuildings = class(TCollection);
TBuilding = class(TCollectionItem);
TRooms = class(TCollection);
TRoom = class(TCollectionItem);
and so on
The top level is a var of TCountry and TCountry contains a collection of cities, each city has a collection of buildings and so on

Now - I'm developing a framework and id like my users to be able to use it with their own versions of any of my classes. So for example I need to be able to do
TBedRoom = class(TRoom)

and the framework should be able to work fine with a collection of bedrooms instead of rooms

Issue No 1:
What is the best way to implement the top level TCountry constructor so that all particular custom classes are passed as parameters upon creation? I don't want users to have to worry about the structure, For example is this a good solution:

TBedRoom = class(TRoom)
TBedRoomClass = class of TBedRoom

MyCountry := TCountry.Create(TBedRoomClass)
This class reference should be passed down the chain until it gets to the TRooms Collection and used there to generate the specific collection item types


Issue No 2:
Is it possible to implement a TRoom constructor like this
constructor TRoom.Create(ARooms: TRooms);

The compiler doesn't seem to like this
TCollection has a method function Add which builds the child objects by calling their Create constructor. Looking at the source code I'm puzzled by the fact that neither the constructor nor the function Add are virtual.


Assume that the TRoom has a constructor with a different no of parameters than the parent TCollectionItem class
for example
constructor TRoom.Create(ACollection: TCollection; AColor: TColor);
or ideally a user class:
constructor TBedRoom.Create(ARooms:TRooms; AColor: TColor);

How do I make sure that the polymorphic constructor is called by the TCollection.Add function so that I end up with objects of the proper class and with the proper color as well

Hopefully all this makes sense if not let me know and ill try to do a better job explaining it



wp

  • Hero Member
  • *****
  • Posts: 3534
Re: Issues with TCollection.Add and polymorphic objects creation
« Reply #1 on: February 02, 2017, 11:31:59 pm »
All usages of TCollection that I have seen do not allow creation of polymorphic collection items. This is because the items are created by the Add method of the collection which takes the class to be used from the parameter specified by the constructor of the collection, i.e. all collection items are instances of the same class.

Why don't you use a TList, TObjectlist or similar? They store only pointers or objects. Here you explicitely create the items by yourself as instances of any class that you need and then you "Add" these now existing items to the list.
Lazarus trunk / fpc 3.0.0 / Win32

Akira1364

  • Full Member
  • ***
  • Posts: 212
Re: Issues with TCollection.Add and polymorphic objects creation
« Reply #2 on: February 05, 2017, 03:25:41 am »
This is what generics are for! Here's a quick example I whipped up, hopefully it's of use to you.

Code: Pascal  [Select]
  1. unit PolymorphicGenericsExample;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, Forms, Dialogs, FGL;
  9.  
  10. type
  11.  
  12.   TGeographicalLocation = class
  13.   private
  14.     FName: String;
  15.   public
  16.     constructor Create(constref AName: String);
  17.     property Name: String read FName write FName;
  18.   end;
  19.  
  20.   TCountry = class(TGeographicalLocation)
  21.   end;
  22.  
  23.   TProvince = class(TGeographicalLocation)
  24.   end;
  25.  
  26.   TCity = class(TGeographicalLocation)
  27.   end;
  28.  
  29.   TBuilding = class(TGeographicalLocation)
  30.   end;
  31.  
  32.   TLocationList = specialize TFPGObjectList<TGeographicalLocation>;
  33.  
  34.   TForm1 = class(TForm)
  35.     procedure FormCreate(Sender: TObject);
  36.     procedure FormShow(Sender: TObject);
  37.     procedure FormDestroy(Sender: TObject);
  38.   end;
  39.  
  40. var
  41.   Form1: TForm1;
  42.   ALocationArray: array[0..3] of TGeographicalLocation;
  43.   ALocationList: TLocationList;
  44.  
  45. implementation
  46.  
  47. {$R *.lfm}
  48.  
  49. constructor TGeographicalLocation.Create(constref AName: String);
  50. begin
  51.   inherited Create;
  52.   FName := AName;
  53. end;
  54.  
  55. procedure TForm1.FormCreate(Sender: TObject);
  56. var
  57.   I: Integer;
  58. begin
  59.   {There's no real reason to declare a 'class of TGeographicalLocation' type, unless you specifically need it.
  60.   Any descendant class will already be implicitly recognized as such, as shown below.}
  61.  
  62.   ALocationArray[0] := TCountry.Create('Canada');
  63.   ALocationArray[1] := TProvince.Create('Ontario');
  64.   ALocationArray[2] := TCity.Create('Toronto');
  65.   ALocationArray[3] := TBuilding.Create('My House');
  66.   ALocationList := TLocationList.Create(True);
  67.  
  68.   {The 'True' sets the FreeObjects property of ALocationList to true, meaning it will automatically
  69.   free anything inside of it when it is itself freed.}
  70.  
  71.   for I := 0 to 3 do
  72.     ALocationList.Add(ALocationArray[I]);
  73. end;
  74.  
  75. procedure TForm1.FormShow(Sender: TObject);
  76. var
  77.   I: Integer;
  78. begin
  79.   with ALocationList do
  80.     for I := 0 to 3 do
  81.       ShowMessage(Items[I].Name);
  82.  
  83.   {The items property of ALocationList will always be viewed as type TGeographicalLocation, so no manual typecasting is required.
  84.   This also shows how even though the four items in ALocationList are technically members of different classes, they will all retain and
  85.   display the correct inherited name property that was initialized in their constructor.}
  86. end;
  87.  
  88. procedure TForm1.FormDestroy(Sender: TObject);
  89. begin
  90.   ALocationList.Free;
  91. end;
  92.  
  93. end.

Edit: I should point out as well that you can get even more specific with the list specialization, as needed. For example, you could do something like this:

Code: Pascal  [Select]
  1. type
  2.  
  3.   {Now TLocationList is being declared as a full-on generic type, which can then be specialized further.}
  4.  
  5.   generic TLocationList<T: TGeographicalLocation> = class(specialize TFPGObjectList<T>)
  6.   public
  7.     {You could add base-level methods that are relevant to all TGeographicalLocation descendants here.}
  8.   end;
  9.  
  10.   TCountryList = class(specialize TLocationList<TCountry>)
  11.   public
  12.     {You could add methods that are relevant only to TCountry here. Of course, TCountryList will also inherit any methods declared in TLocationList.}
  13.   end;
« Last Edit: February 05, 2017, 07:57:32 pm by Akira1364 »

speleomania

  • New member
  • *
  • Posts: 8
Re: Issues with TCollection.Add and polymorphic objects creation
« Reply #3 on: February 06, 2017, 07:03:24 pm »
Thanks for the responses!

Akira:
I'll need some time to figure out exactly what you're trying to tell me with your code, in the meantime 2 quick comments/questions:

1) The end user shouldn't need to do
ALocationArray[2] := TCity.Create('Toronto');

This is managed internally by my unit, I don't want the end user to worry about creation and destruction of TCity objects and need for this to happen behind the scene

2) In addition - I need the end user to be able to supply their own classes - for example TTownHouse. Suppose that the TTownHouse constructor looks something like this
constructor TTownHouse.Create(AName: string; AColor: TColor)
I'm wondering how /if possible/ the user can specify the class type and any additional parameters that are needed for the custom constructor so that my unit can: 1 call the appropriate constructor and 2 supply any additional arguments for it

guess I'm asking too much here ...

Akira1364

  • Full Member
  • ***
  • Posts: 212
Re: Issues with TCollection.Add and polymorphic objects creation
« Reply #4 on: February 07, 2017, 07:39:42 pm »
1) The end user shouldn't need to do
ALocationArray[2] := TCity.Create('Toronto');

Yes, instantiating an array simply to then add its contents to a list would be a terrible and inefficient idea in production. I was just trying to demonstrate how it isn't strictly necessary to use a "class of TSomeClass" type specification in most cases, when dealing with the creation of descendant classes. After re-reading what it is it sounds like you're trying to do, I came up with another little template that I think demonstrates how you could possibly go about it. I didn't have time to comment it like I did before, but have a look to see if you can get a grasp on what it's doing yourself, and let me know if you have any questions.

Code: Pascal  [Select]
  1. unit AnotherGenericsExample;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, Forms, Dialogs, FGL;
  9.  
  10. type
  11.   TLocation = class
  12.   end;
  13.  
  14.   generic TLocationList<T: TLocation> = class(specialize TFPGObjectList<T>)
  15.   public
  16.     procedure AddNewInstance;
  17.   end;
  18.  
  19.   generic TLocationMap<T1, T2: TLocation> = class(specialize TFPGMapObject<T1, T2>)
  20.   end;
  21.  
  22.   TCity = class(TLocation)
  23.   end;
  24.  
  25.   TBuilding = class(TLocation)
  26.   end;
  27.  
  28.   TRoom = class(TLocation)
  29.   end;
  30.  
  31.   generic TCityList<T: TCity> = class(specialize TLocationList<T>)
  32.   end;
  33.  
  34.   generic TBuildingList<T: TBuilding> = class(specialize TLocationList<T>)
  35.   end;
  36.  
  37.   generic TRoomList<T: TRoom> = class(specialize TLocationList<T>)
  38.   end;
  39.  
  40.   generic TCityToBuildingRelationshipMap<CT: TCity; BT: TBuilding> = class(specialize TLocationMap<CT, BT>);
  41.  
  42.   generic TBuildingToRoomRelationshipMap<BT: TBuilding; RT: TRoom> = class(specialize TLocationMap<BT, RT>);
  43.  
  44.   generic TCountry<CT: TCity; BT: TBuilding; RT: TRoom> = class
  45.   private type
  46.     FCityToBuildingMapType = specialize TCityToBuildingRelationshipMap<CT, BT>;
  47.     FBuildingToRoomMapType = specialize TBuildingToRoomRelationshipMap<BT, RT>;
  48.     FRoomListType = specialize TRoomList<RT>;
  49.     FBuildingListType = class(specialize TBuildingList<BT>)
  50.     public
  51.       Rooms: FRoomListType;
  52.     end;
  53.     FCityListType = class(specialize TCityList<CT>)
  54.     public
  55.       Buildings: FBuildingListType;
  56.     end;
  57.   public
  58.     Cities: FCityListType;
  59.     CityToBuildingLinks: FCityToBuildingMapType;
  60.     BuildingToRoomLinks: FBuildingToRoomMapType;
  61.     constructor Create;
  62.     destructor Destroy; override;
  63.   end;
  64.  
  65.   TMyCustomCity = class(TCity)
  66.   end;
  67.  
  68.   TMyCustomBuilding = class(TBuilding)
  69.   end;
  70.  
  71.   TMyCustomRoom = class(TRoom)
  72.   end;
  73.  
  74.   TMyCustomCountry = specialize TCountry<TMyCustomCity, TMyCustomBuilding, TMyCustomRoom>;
  75.  
  76.   TForm1 = class(TForm)
  77.     procedure FormCreate(Sender: TObject);
  78.     procedure FormShow(Sender: TObject);
  79.     procedure FormDestroy(Sender: TObject);
  80.   end;
  81.  
  82. var
  83.   Form1: TForm1;
  84.   ACountry: TMyCustomCountry;
  85.  
  86. implementation
  87.  
  88. {$R *.lfm}
  89.  
  90. procedure TLocationList.AddNewInstance;
  91. begin
  92.   inherited Add(T.Create);
  93. end;
  94.  
  95. constructor TCountry.Create;
  96. begin
  97.   inherited Create;
  98.   Cities := FCityListType.Create(True);
  99.   Cities.Buildings := FBuildingListType.Create(True);
  100.   Cities.Buildings.Rooms := FRoomListType.Create(True);
  101.   CityToBuildingLinks := FCityToBuildingMapType.Create(False);
  102.   BuildingToRoomLinks := FBuildingToRoomMapType.Create(False);
  103. end;
  104.  
  105. destructor TCountry.Destroy;
  106. begin
  107.   CityToBuildingLinks.Free;
  108.   BuildingToRoomLinks.Free;
  109.   Cities.Buildings.Rooms.Free;
  110.   Cities.Buildings.Free;
  111.   Cities.Free;
  112.   inherited Destroy;
  113. end;
  114.  
  115. procedure TForm1.FormCreate(Sender: TObject);
  116. begin
  117.   ACountry := TMyCustomCountry.Create;
  118.   with ACountry do
  119.   begin
  120.     Cities.AddNewInstance;
  121.     Cities.Buildings.AddNewInstance;
  122.     Cities.Buildings.Rooms.AddNewInstance;
  123.   end;
  124. end;
  125.  
  126. procedure TForm1.FormShow(Sender: TObject);
  127. begin
  128.   with ACountry do
  129.   begin
  130.     ShowMessage(Cities.First.ClassName);
  131.     ShowMessage(Cities.Buildings.First.ClassName);
  132.     ShowMessage(Cities.Buildings.Rooms.First.ClassName);
  133.   end;
  134. end;
  135.  
  136. procedure TForm1.FormDestroy(Sender: TObject);
  137. begin
  138.   ACountry.Free;
  139. end;
  140.  
  141. end.
« Last Edit: February 08, 2017, 04:27:29 am by Akira1364 »

Remy Lebeau

  • Sr. Member
  • ****
  • Posts: 262
    • Lebeau Software
Re: Issues with TCollection.Add and polymorphic objects creation
« Reply #5 on: February 08, 2017, 10:11:31 pm »
Now - I'm developing a framework and id like my users to be able to use it with their own versions of any of my classes. So for example I need to be able to do
TBedRoom = class(TRoom)

and the framework should be able to work fine with a collection of bedrooms instead of rooms

A TCollection of TRoom objects can hold any object whose type is derived from TRoom.

Issue No 1:
What is the best way to implement the top level TCountry constructor so that all particular custom classes are passed as parameters upon creation?

I wouldn't implement this at the TCountry layer at all.

I don't want users to have to worry about the structure

Why not?  What are you expecting them to do that is not related to structure?

For example is this a good solution:

TBedRoom = class(TRoom)
TBedRoomClass = class of TBedRoom

MyCountry := TCountry.Create(TBedRoomClass)

Not really.  For one thing, that means every room in every building must be a bedroom.  What about bathrooms, or living rooms, or kitchen rooms?

Plus, I don't think it is TCountry's responsibility to control the room types anyway.  That should be the responsibility of TRooms instead, based on the type of building it belongs to.  A residential house could have bedrooms, but an office building should have offices and conference rooms instead.

You can have TRooms declare a constructor that does not expose a specific ItemType parameter, specifying TRoom when calling the base constructor, eg:

Code: [Select]
type
  TRooms = class(TCollection)
  private
    FBuilding: TBuilding;
  public
    constructor Create(ABuilding: TBuilding); reintroduce;
    property Building: TBuilding read FBuilding;
  end;

constructor TRooms.Create(ABuilding: TBuilding);
begin
  inherited Create(TRoom);
  FBuilding := ABuilding;
end;

Then you can do this in TBuilding:

Code: [Select]
constructor TBuilding.Create(ACollection: TBuildings);
begin
  inherited Create(ACollection);
  FRooms := TRooms.Create(Self);
end;

destructor TBuilding.Destroy;
begin
  FRooms.Free;
  inherited;
end;

Follow this same pattern up the hierarchy until you reach TCountry.

Or, if you want to be more restrictive, you can do something like this:

Code: [Select]
type
  TRooms = class(TCollection)
  private
    FBuilding: TBuilding;
  public
    constructor Create(ABuilding: TBuilding, ARoomType: TRoomClass); reintroduce;
    property Building: TBuilding read FBuilding;
  end;

  ...

  TResidentialBuilding = class(TBuilding)
  public
    constructor Create(ACity: TCity); reintroduce;
  end;

  TResidentialRooms = class;
  TResidentialRoom = class(TRoom)
  public
    constructor Create(ARooms: TResidentialRooms); reintroduce;
  end;

  TResidentialRooms = class(TRooms)
  public
    constructor Create(ABuilding: TResidentialBuilding); reintroduce;
  end;

  TBedRoom = class(TResidentialRoom)
    ...
  end;

  ...

  TOfficeBuilding = class(TBuilding)
  public
    constructor Create(ACity: TCity); reintroduce;
  end;

  TOfficeRooms = class;
  TOfficeRoom = class(TRoom)
  public
    constructor Create(ARooms: TOfficeRooms); reintroduce;
  end;

  TOfficeRooms = class(TRooms)
  public
    constructor Create(ABuilding: TOfficeBuilding); reintroduce;
  end;

  TConferenceRoom = class(TOfficeRoom)
    ...
  end;

...

constructor TRooms.Create(ABuilding: TBuilding, ARoomType: TRoomClass);
begin
  inherited Create(ARoomType);
  FBuilding := ABuilding;
end;

constructor TResidentialBuilding.Create(ACity: TCity);
begin
  inherited Create(ACity);
  FRooms := TResidentialRooms.Create(Self);
end;

constructor TResidentialRooms.Create(ABuilding: TResidentialBuilding);
begin
  inherited Create(ABuilding, TResidentialRoom);
end;

constructor TResidentialRoom.Create(ARooms: TResidentialRooms);
begin
  inherited Create(ARooms);
end;

constructor TOfficeBuilding.Create(ACity: TCity);
begin
  inherited Create(ACity);
  FRooms := TOfficeRooms.Create(Self);
end;

constructor TOfficeRooms.Create(ABuilding: TOfficeBuilding);
begin
  inherited Create(ABuilding, TOfficeRoom);
end;

constructor TOfficeRoom.Create(ARooms: TOfficeRooms);
begin
  inherited Create(ARooms);
end;

Yes, it is more work.  But it gives you more control over what types are acceptable at each layer of the hierarchy.

Issue No 2:
Is it possible to implement a TRoom constructor like this
constructor TRoom.Create(ARooms: TRooms);

Technically yes, especially if would declare this custom constructor as 'reintroduce'.  However, just know that this constructor won't be callable by TCollection.Add() since it does not override the existing virtual constructor that Add() calls.  But you can hide/reintroduce Add() to prevent plain TRoom objects from being added, if needed.

TCollection has a method function Add which builds the child objects by calling their Create constructor. Looking at the source code I'm puzzled by the fact that neither the constructor nor the function Add are virtual.

TCollection's constructor and Add() method are not virtual, no.  Nor do they need to be.  TCollectionItem's constructor is virtual, thus allowing TCollection.Add() to create an instance of whatever class type is specified in the TCollection constructor.

Assume that the TRoom has a constructor with a different no of parameters than the parent TCollectionItem class
for example
constructor TRoom.Create(ACollection: TCollection; AColor: TColor);
or ideally a user class:
constructor TBedRoom.Create(ARooms:TRooms; AColor: TColor);

How do I make sure that the polymorphic constructor is called by the TCollection.Add function so that I end up with objects of the proper class and with the proper color as well

You can't.  TCollection.Add() knows nothing about your custom constructor at all, and so cannot call it.  It only knows about the virtual TCollectionItem constructor.

However, you can call your custom constructor directly instead of calling Add().

All usages of TCollection that I have seen do not allow creation of polymorphic collection items.

Indy does, for example.  The TIdMessage.MessageParts property is a standard TCollection (actually a TOwnedCollection) that can hold any number of TIdMessagePart descendants (TIdText, TIdAttachment, etc) at one time.

This is because the items are created by the Add method of the collection which takes the class to be used from the parameter specified by the constructor of the collection, i.e. all collection items are instances of the same class.

That is true for TCollection.Add() (and TCollection.Insert()), but those are not the only ways to add items into the collection.  You can just create an item class directly, passing the owning TCollection to its constructor.  As long as the new item is derived from the type that is specified in the TCollection constructor, this works just fine, eg:

Code: [Select]
type
  TCity_NewYork = class(TCity)
    ...
  end;
  TCity_LosAngeles = class(TCity)
    ...
  end;
  TBuilding_EmpireState = class(TBuilding)
    ...
  end;
  TBuilding_CapitolRecords = class(TBuilding)
    ...
  end;

...

USA := TCountry.Create;
...
NewYork := TCity_NewYork.Create(USA.Cities);
TBuilding_EmpireState.Create(NewYork.Buildings);
...
LosAngeles := TCity_LosAngeles.Create(USA.Cities);
TBuilding_CapitolRecords.Create(LosAngeles.Buildings);
...

Code: [Select]
MyRoom := TBedRoom.Create(SomeBuilding.Rooms);

And really, that is all TCollection.Add() (and TCollection.Insert()) is doing internally anyway (at least in Delphi, I have not checked FreePascal):

Code: [Select]
function TCollection.Add: TCollectionItem;
begin
  Result := FItemClass.Create(Self); // <-- HERE
  Added(Result);
end;

function TCollection.Insert(Index: Integer): TCollectionItem;
begin
  Result := Add;
  Result.Index := Index;
end;
« Last Edit: February 08, 2017, 10:57:30 pm by Remy Lebeau »
Remy Lebeau
Lebeau Software - Owner, Developer
Internet Direct (Indy) open source project - Admin, Developer

 

Recent

Get Lazarus at SourceForge.net. Fast, secure and Free Open Source software downloads Open Hub project report for Lazarus