Delphi generics TObjectList<T> inheritance - delphi

I want to create a TObjectList<T> descendant to handle common functionality between object lists in my app. Then I want to further descend from that new class to introduce additional functionality when needed. I cannot seem to get it working using more than 1 level of inheritance. I probably need to understand generics a little bit more, but I've search high and low for the correct way to do this without success. Here is my code so far:
unit edGenerics;
interface
uses
Generics.Collections;
type
TObjectBase = class
public
procedure SomeBaseFunction;
end;
TObjectBaseList<T: TObjectBase> = class(TObjectList<T>)
public
procedure SomeOtherBaseFunction;
end;
TIndexedObject = class(TObjectBase)
protected
FIndex: Integer;
public
property Index: Integer read FIndex write FIndex;
end;
TIndexedObjectList<T: TIndexedObject> = class(TObjectBaseList<T>)
private
function GetNextAutoIndex: Integer;
public
function Add(AObject: T): Integer;
function ItemByIndex(AIndex: Integer): T;
procedure Insert(AIndex: Integer; AObject: T);
end;
TCatalogueItem = class(TIndexedObject)
private
FID: integer;
public
property ID: integer read FId write FId;
end;
TCatalogueItemList = class(TIndexedObjectList<TCatalogueItem>)
public
function GetRowById(AId: Integer): Integer;
end;
implementation
uses
Math;
{ TObjectBase }
procedure TObjectBase.SomeBaseFunction;
begin
end;
{ TObjectBaseList<T> }
procedure TObjectBaseList<T>.SomeOtherBaseFunction;
begin
end;
{ TIndexedObjectList }
function TIndexedObjectList<T>.Add(AObject: T): Integer;
begin
AObject.Index := GetNextAutoIndex;
Result := inherited Add(AObject);
end;
procedure TIndexedObjectList<T>.Insert(AIndex: Integer; AObject: T);
begin
AObject.Index := GetNextAutoIndex;
inherited Insert(AIndex, AObject);
end;
function TIndexedObjectList<T>.ItemByIndex(AIndex: Integer): T;
var
I: Integer;
begin
Result := Default(T);
while (Count > 0) and (I < Count) and (Result = Default(T)) do
if Items[I].Index = AIndex then
Result := Items[I]
else
Inc(I);
end;
function TIndexedObjectList<T>.GetNextAutoIndex: Integer;
var
I: Integer;
begin
Result := 0;
for I := 0 to Count - 1 do
Result := Max(Result, Items[I].Index);
Inc(Result);
end;
{ TCatalogueItemList }
function TCatalogueItemList.GetRowById(AId: Integer): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to Pred(Self.Count) do
if Self.Items[I].Id = AId then
begin
Result := I;
Break;
end;
end;
end.
/////// ERROR HAPPENS HERE ////// ???? why is beyond me
It appears that the following declaration:
>>> TCatalogueItemList = class(TIndexedObjectList<TCatalogueItem>) <<<<
causes the following compiler error:
[DCC Error] edGenerics.pas(106): E2010 Incompatible types:
'TCatalogueItem' and 'TIndexedObject'
However the compiler shows the error at the END of the compiled unit (line 106), not on the declaration itself, which does not make any sense to me...
Basically the idea is that I have a generic list descending from TObjectList that I can extend with new functionality on an as needs basis. Any help with this would be GREAT!!!
I should add, using Delphi 2010.
Thanks.

Your error is in the type casting, and the compiler error is OK (but it fails to locate the correct file in my Delphi XE3).
Your ItemByIndex method is declared:
TIndexedObjectList<T>.ItemByIndex(AIndex: Integer): T;
But then you have the line:
Result := TIndexedObject(nil);
This is fine for the parent class TIndexedObjectList, where the result of the function is of type TIndexedObject, but is not OK for the descendant class TCatalogueItemList, where the result of the function is of the type TCatalogueItem.
As you may know, a TCatalogueItem instance is assignment compatible with a TIndexedObject variable, but the opposite is not true. It translates to something like this:
function TCatalogueItemList.ItemByIndex(AIndex: Integer): TCatalogueItem;
begin
Result := TIndexedObject(nil); //did you see the problem now?
To initialize the result to a nil value, you can call the Default() pseudo-function, like this:
Result := Default(T);
In Delphi XE or greater, the solution is also generic. Rather than type-casting the result as a fixed TIndexedObjectList class, you apply a generic type casting use the T type
Result := T(nil);
//or
Result := T(SomeOtherValue);
But, in this specific case, type-casting a nil constant is not needed, since nil is a special value that is assignment compatible with any reference, so you just have to replace the line with:
Result := nil;
And it will compile, and hopefully work as you expect.

Related

Delphi TThread descendant return result

SITUATION. I have created an unit with some classes to solve algebra stuff (congruences and systems), I am showing you the code:
type
TCongrError = class(Exception)
end;
type
TCongruence = class(TComponent)
//code stuff
constructor Create(a, b, n: integer); virtual;
end;
type
TCongrSystem = array of TCongruence;
type
TCongruenceSystem = class(TThread)
private
resInner: integer;
FData: TCongrSystem;
function modinv(u, v: integer): integer; //not relevant
protected
procedure Execute; override;
public
constructor Create(data: TCongrSystem; var result: integer; hasClass: boolean);
end;
I have decided to use TThread because this class has an Execute method that could take some time to finish due to the length of the parameters passed to the constructor. Here's the implementation:
constructor TCongruenceSystem.Create(data: TCongrSystem; var result: integer; hasClass: boolean);
begin
inherited Create(True);
FreeOnTerminate := true;
FData := data;
setClass := hasClass;
resInner := result;
end;
procedure TCongruenceSystem.Execute;
var sysResult, i, n, t: integer;
begin
sysResult := 0;
n := 1;
//computation
Queue( procedure
begin
ShowMessage('r = ' + sysResult.ToString);
resInner := sysResult;
end );
end;
PROBLEM
If you look at the Queue you see that I am using (just as test) the ShowMessage and it is showing the correct value of sysResult. The second line by the way has some problems that I cannot understand.
The constructor has var result: integer so I can have side-effect from the passed variable and then I can assign resInner := result;. At the end (in the Queue) I am giving resInner the value of sysResult and I expect result to be updated too due to the side effect of var. Why doesn't this happen?
I have made another test changing the constructor like this:
constructor TCongruenceSystem.Create(data: TCongrSystem; result: TMemo; hasClass: boolean);
//now of course I have resInner: TMemo
And changing the Queue to this:
Queue( procedure
begin
ShowMessage('r = ' + sysResult.ToString);
resInner.Lines.Add(sysResult.ToString);
end ); //this code now works properly in both cases! (showmessage and memo)
In the constructor I am passing TMemo which is a reference and ok, but isn't the original var result: integer passed as reference too? Why then it doesn't work?
I want to do this because I'd like to do something like this:
//I put var a: integer; inside the public part of the TForm
test := TCongruenceSystem.Create(..., a, true);
test.OnTerminate := giveMeSolution;
test.Start;
test := nil;
Where giveMeSolution is just a simple procedure that uses the variable a containing the result of the system. If this is not possible what could I do? Basically the result at the end of Execute is just an integer number that has to be passed to the main thread.
I have read about ReturnValue but I am not sure how to use it.
Basically the result at the end of Execute is just an integer number that has to be passed to the main thread.
I have read about ReturnValue but I am not sure how to use it.
Using the ReturnValue property is very easy:
type
TCongruenceSystem = class(TThread)
...
protected
procedure Execute; override;
public
property ReturnValue; // protected by default
end;
procedure TCongruenceSystem.Execute;
var
...
begin
// computation
ReturnValue := ...;
end;
test := TCongruenceSystem.Create(...);
test.OnTerminate := giveMeSolution;
test.Start;
....
procedure TMyForm.giveMeSolution(Sender: TObject);
var
Result: Integer;
begin
Result := TCongruenceSystem(Sender).ReturnValue;
...
end;
Let's assume a class field FFoo : integer; ;
procedure TFoo.Foo(var x : integer);
begin
FFoo := x;
end;
Here what you are doing is assigning the value of x to FFoo. Inside the method Foo you are free to modify the value of the variable passed in as x but integers are otherwise value types that are copied on assignment. If you want to keep a reference to an external integer variable you would need to declare FFoo (or, in your case, resInner) as a PInteger (pointer to an integer). For example (simplifying) :
TCongruenceSystem = class(TThread)
private
resInner: PInteger;
protected
procedure Execute; override;
public
constructor Create(result: PInteger);
end;
where
constructor TCongruenceSystem.Create(result: PInteger);
begin
inherited Create(True);
FreeOnTerminate := true;
resInner := result;
end;
which you would call as test := TCongruenceSystem.Create(#a); and assign:
{ ** See the bottom of this answer for why NOT to use }
{ Queue with FreeOnTerminate = true ** }
Queue( procedure
begin
ShowMessage('r = ' + sysResult.ToString);
resInner^ := sysResult;
end );
The reason it works with TMemo is that classes are reference types - their variables do not hold values but rather point to the address of the object in memory. When you copy a class variable you are only copying a reference (ie: a pointer) whereas for value types the contents of the variable are copied on assignment.
With that said, there's nothing stopping you from keeping the argument typed as var x : integer and taking a reference in your constructor :
constructor TCongruenceSystem.Create(var result: Integer);
begin
inherited Create(True);
FreeOnTerminate := true;
resInner := #result; {take the reference here}
end;
but this gives the caller the impression that once the constructor is complete that you have made any modifications to the variable you intend to and they are free to dispose of the integer. Passing explicitly as PInteger gives the caller a hint that your object will keep a reference to the integer they provide and that need to ensure the underlying variable remains valid while your class is alive.
And... with all that said, I still fundamentally don't like this idea. By taking in a variable reference like this you are offloading an atypical lifetime management issue to the caller. Passing pointers is best done in place where they are used at the point of transfer only. Holding onto a foreign pointer is messy and it's too easy for mistakes to happen. A far better approach here would be to provide a completion event and have the consumer of your class attach a handler.
For example :
{ define a suitable callback signature }
TOnCalcComplete = procedure(AResult : integer) of object;
TCongruenceSystem = class(TThread)
private
Fx, Fy : integer;
FOnCalcComplete : TOnCalcComplete;
protected
procedure Execute; override;
public
constructor Create(x,y: integer);
property OnCalcComplete : TOnCalcComplete read FOnCalcComplete write FOnCalcComplete;
end;
constructor TCongruenceSystem.Create(x: Integer; y: Integer);
begin
inherited Create(true);
FreeOnTerminate := true;
Fx := x;
Fy := y;
end;
procedure TCongruenceSystem.Execute;
var
sumOfxy : integer;
begin
sumOfxy := Fx + Fy;
sleep(3000); {take some time...}
if Assigned(FOnCalcComplete) then
Synchronize(procedure
begin
FOnCalcComplete(sumOfxy);
end);
end;
Which you would then call as :
{ implement an event handler ... }
procedure TForm1.CalcComplete(AResult: Integer);
begin
ShowMessage(IntToStr(AResult));
end;
procedure TForm1.Button1Click(Sender: TObject);
var
LCongruenceSystem : TCongruenceSystem;
begin
LCongruenceSystem := TCongruenceSystem.Create(5, 2);
LCongruenceSystem.OnCalcComplete := CalcComplete; { attach the handler }
LCongruenceSystem.Start;
end;
You'll also notice that I used Synchronize here instead of Queue. On this topic, please have a read of this question (I'll quote Remy...):
Ensure all TThread.Queue methods complete before thread self-destructs
Setting FreeOnTerminate := True in a queued method is asking for a memory leak.

Casting in generic class to interface delphi

I'm getting a IEnumVariant from a .NET class library and I am trying to use a generic class to convert this to a IEnumerator
There is a compiler error, "Operator not applicable to this operand type" when attempting to cast an IInterface to the generic type T
I've seen workarounds when attempting to type cast to a class, but these don't work for an interface.
Using Supports as suggested by Rob seems to have problems as well as TypeInfo returns nil for the parameterized type.
uses WinApi.ActiveX, Generics.Collections;
type
TDotNetEnum<T: IInterface> = class(TInterfacedObject, IEnumerator<T>)
strict private
FDotNetEnum: IEnumVariant;
FCurrent: T;
function MoveNext: Boolean;
procedure Reset;
function GetCurrent: TObject;
function IEnumerator<T>.GetCurrent = GenericGetCurrent;
function GenericGetCurrent: T;
public
constructor Create(const ADotNetObject: OleVariant);
//// I can get it to work using this constructor
// constructor Create(const ADotNetObject: OleVariant; const AGUID: TGUID);
end;
implementation
uses System.Rtti, SysUtils, mscorlib_TLB, ComObj;
constructor TDotNetEnum<T>.Create(const ADotNetObject: OleVariant);
var
netEnum: IEnumerable;
begin
netEnum := IUnknown(ADotNetObject) as mscorlib_TLB.IEnumerable;
FDotNetEnum := netEnum.GetEnumerator();
end;
function TDotNetEnum<T>.GenericGetCurrent: T;
begin
result := FCurrent;
end;
function TDotNetEnum<T>.GetCurrent: TObject;
begin
result := nil;
end;
function TDotNetEnum<T>.MoveNext: Boolean;
var
rgvar: OleVariant;
fetched: Cardinal;
ti: TypeInfo;
guid: TGUID;
begin
OleCheck(FDotNetEnum.Next(1, rgvar, fetched));
result := fetched = 1;
if not result then
FCurrent := nil
else
begin
FCurrent := IUnknown(rgvar) as T; // <-- Compiler error here
//// Doesn't work using Supports either
// ti := TypeInfo(T); // <-- returns nil
// guid := GetTypeData(#ti)^.Guid;
// Supports(IUnknown(rgvar), guid, FCurrent);
end;
end;
procedure TDotNetEnum<T>.Reset;
begin
OleCheck(FDotNetEnum.Reset);
end;
Am I missing something in order to get that case to the generic interface type to work ?
I do have the alternative constructor which I CAN get the guid from so that
TDotNetEnum<IContact>.Create(vContactList, IContact);
works but the ideal
TDotNetEnum<IContact>.Create(vContactList);
doesn't
Using as to cast interfaces is only valid for interfaces that have GUIDs. The compiler cannot assume that T has a GUID when it's compiling your generic class, so it cannot accept an expression of the form val as T.
This has been covered before, but in reference to the Supports function, which has the same limitation as the as operator.
The solution is to use RTTI to fetch the interface's GUID, and then use that to type-cast the interface value. You could use Supports:
guid := GetTypeData(TypeInfo(T))^.Guid;
success := Supports(IUnknown(rgvar), guid, FCurrent);
Assert(success);
You could also call QueryInterface directly:
guid := GetTypeData(TypeInfo(T))^.Guid;
OleCheck(IUnknown(rgvar).QueryInterface(guid, FCurrent));

Delphi static method of a class returning property value

I'm making a Delphi VCL application. There is a class TStudent where I have two static functions: one which returns last name from an array of TStudent and another one which returns the first name of the student. Their code is something like:
class function TStudent.FirstNameOf(aLastName: string): string;
var i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if studentsArray[i].LastName = aLastName then
begin
result := studentsArray[i].FirstName;
Exit;
end;
end;
result := 'no match was found';
end;
class function TStudent.LastNameOf(aFirstName: string): string;
var i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if studentsArray[i].FirstName = aFirstName then
begin
result := studentsArray[i].LastName;
Exit;
end;
end;
result := 'no match was found';
end;
My question is how can I avoid writing almost same code twice. Is there any way to pass the property as parameter of the functions.
You can use an anonymous method with variable capture for this linear search. This approach gives you complete generality with your predicate. You can test for equality of any field, of any type. You can test for more complex predicates for instance an either or check.
The code might look like this:
class function TStudent.LinearSearch(const IsMatch: TPredicate<TStudent>;
out Index: Integer): Boolean;
var
i: Integer;
begin
for i := low(studentsArray) to high(studentsArray) do
begin
if IsMatch(studentsArray[i]) then
begin
Index := i;
Result := True;
exit;
end;
end;
Index := -1;
Result := False;
end;
Now all you need to do is provide a suitable predicate. The definition of TPredicate<T>, from the System.SysUtils unit, is:
type
TPredicate<T> = reference to function (Arg1: T): Boolean;
So you would code your method like this:
class function TStudent.GetFirstName(const LastName: string): string;
var
Index: Integer;
IsMatch: TPredicate<TStudent>;
begin
IsMatch :=
function(Student: TStudent): Boolean
begin
Result := Student.LastName=LastName;
end;
if not LinearSearch(IsMatch, Index) then
begin
raise ...
end;
Result := studentsArray[Index].FirstName;
end;
And likewise for GetLastName.
If your Delphi does not support anonymous methods then you won't be able to use variable capture and will have to find a more convoluted approach using of object method types. However, the basic idea will be much the same.
I haven't tested it, but I believe this could be one solution.
uses TypInfo;
class function TStudent.GetProperty( propertyName: string, searchValue : Variant ) : Variant ;
var i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if GetPropValue( studentsArray[i], propertyName ) = searchValue
result := GetPropValue( studentsArray[i], propertyName );
end;
// your code in case of not finding anything
end;
If you are using Delphi 2010 or later, you could use Extended RTTI:
uses
..., Rtti;
type
TStudent = class
public
FirstName: String;
LastName: String;
class function GetNameOf(const aFieldToFind, aNameToFind, aFieldToReturn: string): string;
end;
class function TStudent.GetNameOf(const aFieldToFind, aNameToFind, aFieldToReturn: string): string;
var
i : integer;
ctx: TRttiContent;
StudentType: TRttiType;
Field: TRttiField;
Value: TValue;
begin
ctx := TRttiContext.Create;
StudentType := ctx.GetType(TStudent);
Field := StudentType.GetField(aFieldToFind);
for i := 0 to Length(studentsArray) - 1 do
begin
if Field.GetValue(#studentsArray[i]).AsString = aNameToFind then
begin
Result := StudentType.GetField(aFieldToReturn).GetValue(#studentsArray[i]).AsString;
Exit;
end;
end;
Result := 'no match was found';
end;
Then you can call it like this:
FirstName := TStudent.GetNameOf('LastName', 'Smoe', 'FirstName');
LastName := TStudent.GetNameOf('FirstName', 'Joe', 'LastName');
If you restructure the TStudent record a little, everything gets easier. Instead of having multiple string fields with different names, declare an array of strings with an enumeration range.
Give the enumeration meaningful names and add a search function where the search field and result field can be specified.
Type
TStudentField = (sfFirstName,sfLastName); // Helper enumeration type
TStudent = record
Field: array[TStudentField] of String;
class function SearchNameOf(searchField: TStudentField;
const aSearchName: string; resultField: TStudentField): string; static;
end;
Here is a test example:
program ProjectTest;
{$APPTYPE CONSOLE}
Type
TStudentField = (sfFirstName,sfLastName);
TStudent = record
Field: array[TStudentField] of String;
class function SearchNameOf(searchField: TStudentField; const aSearchName: string; resultField: TStudentField): string; static;
end;
var
studentsArray : array of TStudent;
class function TStudent.SearchNameOf(searchField: TStudentField; const aSearchName: string; resultField: TStudentField): string;
var
i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if (studentsArray[i].Field[searchField] = aSearchName) then
begin
Result := studentsArray[i].Field[resultField];
Exit;
end;
end;
result := 'no match was found';
end;
begin
SetLength(studentsArray,2);
studentsArray[0].Field[sfFirstName] := 'Buzz';
studentsArray[0].Field[sfLastName] := 'Aldrin';
studentsArray[1].Field[sfFirstName] := 'Neil';
studentsArray[1].Field[sfLastName] := 'Armstrong';
WriteLn(TStudent.SearchNameOf(sfFirstName,'Neil',sfLastName));
ReadLn;
end.
You could use a several properties with index specifier backed by single getter function just as you do for regular array properties:
TDefault = class(TObject)
private
class function GetProp(const FindWhat: string; FindWhere: Integer): string;
static;
protected
/// <remarks>
/// You don't really need this one. I've added it for an illustration
/// purposes.
/// </remarks>
class property Prop[const FindWhat: string; FindWhere: Integer]: string read GetProp;
public
class property A[const FindWhat: string]: string index 0 read GetProp;
class property B[const FindWhat: string]: string index 1 read GetProp;
end;
{ ... }
class function TDefault.GetProp(const FindWhat: string; FindWhere: Integer): string;
begin
case FindWhere of
0: Result := 'Hallo!';
1: Result := 'Hello!';
end;
Result := Result + ' ' + Format('searching for "%s"', [FindWhat]);
end;
As you see, the class properties are just the same as instance properties.
And I must say its a pretty bad idea to perform a search in the property getter.

How to use TValue.AsType<TNotifyEvent> properly?

I'm trying to use RTTI to add an event handler to a control, that may already have an event handler set. The code looks something like this:
var
prop: TRttiProperty;
val: TValue;
begin
prop := FContext.GetType(MyControl.ClassInfo).GetProperty('OnChange');
val := prop.GetValue(MyControl);
FOldOnChange := val.AsType<TNotifyEvent>;
prop.SetValue(MyControl, TValue.From<TNotifyEvent>(self.MyOnChange));
end;
I want this so I can do this in MyOnChange:
begin
if assigned(FOldOnChange) then
FOldOnChange(Sender);
//additional code here
end;
Unfortunately, the compiler doesn't seem to like the line FOldOnChange := val.AsType<TNotifyEvent>;. It says
E2010 Incompatible types: 'procedure,
untyped pointer or untyped parameter'
and 'TNotifyEvent'
Anyone know why that is or how to fix it? It looks right to me...
FOldOnChange is of a method pointer type, while AsType<TNotifyEvent> is a method. The compiler thinks you're trying to assign the method to the method pointer. The solution is to append () to the method call to force it, and use the return value of the method as the value to assign to FOldOnChange.
Here's a complete example:
uses SysUtils, Rtti;
type
TEv = procedure(Sender: TObject) of object;
TObj = class
private
FEv: TEv;
public
property Ev: TEv read FEv write FEv;
class procedure Meth(Sender: TObject);
end;
class procedure TObj.Meth(Sender: TObject);
begin
end;
procedure P;
var
ctx: TRttiContext;
t: TRttiType;
p: TRttiProperty;
v: TValue;
o: TObj;
e: TEv;
begin
t := ctx.GetType(TObj);
p := t.GetProperty('Ev');
o := TObj.Create;
try
// Set value explicitly
o.Ev := TObj.Meth;
// Get value via RTTI
v := p.GetValue(o);
//e := v.AsType<TEv>; // doesn't work
e := v.AsType<TEv>(); // works
finally
o.Free;
end;
end;
begin
try
P;
except
on e: Exception do
Writeln(e.Message);
end;
end.
The new RTTI introduced in 2010 is basically just an advanced wrapper around the older TypInfo RTTI (for now). In TypInfo, event handlers are represented by the TMethod record. Try this (untested):
var
prop: TRttiProperty;
val: TValue;
evt: TNotifyEvent;
begin
prop := FContext.GetType(MyControl.ClassInfo).GetProperty('OnChange');
val := prop.GetValue(MyControl);
TMethod(FOldOnChange) := val.AsType<TMethod>;
evt := Self.MyOnChange;
prop.SetValue(MyControl, TValue.From<TMethod>(TMethod(evt));
end;

Generic factory

suppose I have a TModel:
TModelClass = class of TModel;
TModel = class
procedure DoSomeStuff;
end;
and 2 descendants:
TModel_A = class(TModel);
TModel_B = class(TModel);
and a factory :
TModelFactory = class
class function CreateModel_A: TModel_A;
class function CreateModel_B: TModel_B;
end;
Now I want to refactor a bit :
TModelFactory = class
class function CreateGenericModel(Model: TModelClass) : TModel
end;
class function TModelFactory.CreateGenericModel(Model: TModelClass) : TModel
begin
...
case Model of
TModel_A: Result := TModel_A.Create;
TModel_B: Result := TModel_B.Create;
end;
...
end;
So far it's ok, but every time I create a TModel descendant, I have to modify the factory case statement.
My question: Is this possible to create a 100% generic factory for all my TModel descendants, so every time I create a TModel descendants I don't have to modify TModelFactory ?
I tried to play with Delphi 2009 generics but didn't find valuable information, all are related to basic usage of TList<T>and so on.
Update
Sorry, but maybe I'm not clear or don't understand your answer (I'm still a noob), but what i'm trying to achieve is :
var
M: TModel_A;
begin
M: TModelFactory.CreateGenericModel(MY_CONCRETE_CLASS);
Well, you could write
class function TModelFactory.CreateGenericModel(AModelClass: TModelClass): TModel;
begin
Result := AModelClass.Create;
end;
but then you don't need a factory any more. Usually one would have a selector of a different type, like an integer or string ID, to select the concrete class the factory should create.
Edit:
To answer your comment on how to add new classes without the need to change the factory - I will give you some simple sample code that works for very old Delphi versions, Delphi 2009 should upen up much better ways to do this.
Each new descendant class only needs to be registered with the factory. The same class can be registered using several IDs. The code uses a string ID, but integers or GUIDs would work just as well.
type
TModelFactory = class
public
class function CreateModelFromID(const AID: string): TModel;
class function FindModelClassForId(const AID: string): TModelClass;
class function GetModelClassID(AModelClass: TModelClass): string;
class procedure RegisterModelClass(const AID: string;
AModelClass: TModelClass);
end;
{ TModelFactory }
type
TModelClassRegistration = record
ID: string;
ModelClass: TModelClass;
end;
var
RegisteredModelClasses: array of TModelClassRegistration;
class function TModelFactory.CreateModelFromID(const AID: string): TModel;
var
ModelClass: TModelClass;
begin
ModelClass := FindModelClassForId(AID);
if ModelClass <> nil then
Result := ModelClass.Create
else
Result := nil;
end;
class function TModelFactory.FindModelClassForId(
const AID: string): TModelClass;
var
i, Len: integer;
begin
Result := nil;
Len := Length(RegisteredModelClasses);
for i := 0 to Len - 1 do
if RegisteredModelClasses[i].ID = AID then begin
Result := RegisteredModelClasses[i].ModelClass;
break;
end;
end;
class function TModelFactory.GetModelClassID(AModelClass: TModelClass): string;
var
i, Len: integer;
begin
Result := '';
Len := Length(RegisteredModelClasses);
for i := 0 to Len - 1 do
if RegisteredModelClasses[i].ModelClass = AModelClass then begin
Result := RegisteredModelClasses[i].ID;
break;
end;
end;
class procedure TModelFactory.RegisterModelClass(const AID: string;
AModelClass: TModelClass);
var
i, Len: integer;
begin
Assert(AModelClass <> nil);
Len := Length(RegisteredModelClasses);
for i := 0 to Len - 1 do
if (RegisteredModelClasses[i].ID = AID)
and (RegisteredModelClasses[i].ModelClass = AModelClass)
then begin
Assert(FALSE);
exit;
end;
SetLength(RegisteredModelClasses, Len + 1);
RegisteredModelClasses[Len].ID := AID;
RegisteredModelClasses[Len].ModelClass := AModelClass;
end;
Result := Model.Create;
should work, too.
The solution with Model.Create works if the constructor is virtual.
If you use delphi 2009, you can use another trick using generics:
type
TMyContainer<T: TModel, constructor> (...)
protected
function CreateModel: TModel;
end;
function TMyContainer<T>.CreateModel: TModel;
begin
Result := T.Create; // Works only with a constructor constraint.
end;
If I understand your question properly, I wrote something similar here http://www.malcolmgroves.com/blog/?p=331
There is probably a simpler way to accomplish this. I seem to remember finding the built-in TClassList object that handled this, but that this point I already had this working. TClassList does not have a way to look up the stored objects by the string name, but it could still be useful.
Basically to make this work you need to register your classes with a global object. That way it can take a string input for the class name, lookup that name in a list to find the correct class object.
In my case I used a TStringList to hold the registered classes and I use the class name as the identifier for the class. In order to add the class to the "object" member of the string list I needed to wrap the class in a real object. I'll admit that I don't really understand the "class" so this may not be needed if you cast everything right.
// Needed to put "Class" in the Object member of the
// TStringList class
TClassWrapper = class(TObject)
private
FGuiPluginClass: TAgCustomPluginClass;
public
property GuiPluginClass: TAgCustomPluginClass read FGuiPluginClass;
constructor Create(GuiPluginClass: TAgCustomPluginClass);
end;
I have a global "PluginManager" object. This is where classes get registered and created. The "AddClass" method puts the class in the TStringList so I can look it up later.
procedure TAgPluginManager.AddClass(GuiPluginClass: TAgCustomPluginClass);
begin
FClassList.AddObject(GuiPluginClass.ClassName,
TClassWrapper.Create(GuiPluginClass));
end;
In each class that I create I add it to the class list in the "initialization" section.
initialization;
AgPluginManager.AddClass(TMyPluginObject);
Then, when it comes time to create the class I can lookup the name in the string list, find the class and create it. In my actual function I am checking to make sure the entry exists and deal with errors, etc. I am also passing in more data to the class constructor. In my case I am creating forms so I don't actually return the object back to the caller (I track them in my PluginManager), but that would be easy to do if needed.
procedure TAgPluginManager.Execute(PluginName: string);
var
ClassIndex: integer;
NewPluginWrapper: TClassWrapper;
begin
ClassIndex := FClassList.IndexOf(PluginName);
if ClassIndex > -1 then
begin
NewPluginWrapper := TClassWrapper(FClassList.Objects[ClassIndex]);
FActivePlugin := NewPluginWrapper.GuiPluginClass.Create();
end;
end;
Since I first wrote this I have not needed to touch the code. I just make sure to add my new classes to the list in their initialization section and everything works.
To create an object I just call
PluginManger.Execute('TMyPluginObject');
You can do generic factory like this: But the only issue you should set the generic construct method to it for each of the factory final class like this:
type
TViewFactory = TGenericFactory<Integer, TMyObjectClass, TMyObject>;
...
F := TViewFactory.Create;
F.ConstructMethod :=
function(AClass: TMyObjectClass; AParams: array of const): TMyObject
begin
if AClass = nil then
Result := nil
else
Result := AClass.Create;
end;
and the unit for the factory is:
unit uGenericFactory;
interface
uses
System.SysUtils, System.Generics.Collections;
type
EGenericFactory = class(Exception)
public
constructor Create; reintroduce;
end;
EGenericFactoryNotRegistered = class(EGenericFactory);
EGenericFactoryAlreadyRegistered = class(EGenericFactory);
TGenericFactoryConstructor<C: constructor; R: class> = reference to function(AClass: C; AParams: array of const): R;
TGenericFactory<T; C: constructor; R: class> = class
protected
FType2Class: TDictionary<T, C>;
FConstructMethod: TGenericFactoryConstructor<C, R>;
procedure SetConstructMethod(const Value: TGenericFactoryConstructor<C, R>);
public
constructor Create(AConstructor: TGenericFactoryConstructor<C, R> = nil); reintroduce; overload; virtual;
destructor Destroy; override;
procedure RegisterClass(AType: T; AClass: C);
function ClassForType(AType: T): C;
function TypeForClass(AClass: TClass): T;
function SupportsClass(AClass: TClass): Boolean;
function Construct(AType: T; AParams: array of const): R;
property ConstructMethod: TGenericFactoryConstructor<C, R> read FConstructMethod write SetConstructMethod;
end;
implementation
uses
System.Rtti;
{ TGenericFactory<T, C, R> }
function TGenericFactory<T, C, R>.ClassForType(AType: T): C;
begin
FType2Class.TryGetValue(AType, Result);
end;
function TGenericFactory<T, C, R>.Construct(AType: T; AParams: array of const): R;
begin
if not Assigned(FConstructMethod) then
Exit(nil);
Result := FConstructMethod(ClassForType(AType), AParams);
end;
constructor TGenericFactory<T, C, R>.Create(AConstructor: TGenericFactoryConstructor<C, R> = nil);
begin
inherited Create;
FType2Class := TDictionary<T, C>.Create;
FConstructMethod := AConstructor;
end;
destructor TGenericFactory<T, C, R>.Destroy;
begin
FType2Class.Free;
inherited;
end;
procedure TGenericFactory<T, C, R>.RegisterClass(AType: T; AClass: C);
begin
if FType2Class.ContainsKey(AType) then
raise EGenericFactoryAlreadyRegistered.Create;
FType2Class.Add(AType, AClass);
end;
procedure TGenericFactory<T, C, R>.SetConstructMethod(const Value: TGenericFactoryConstructor<C, R>);
begin
FConstructMethod := Value;
end;
function TGenericFactory<T, C, R>.SupportsClass(AClass: TClass): Boolean;
var
Key: T;
Val: C;
begin
for Key in FType2Class.Keys do
begin
Val := FType2Class[Key];
if CompareMem(#Val, AClass, SizeOf(Pointer)) then
Exit(True);
end;
Result := False;
end;
function TGenericFactory<T, C, R>.TypeForClass(AClass: TClass): T;
var
Key: T;
Val: TValue;
begin
for Key in FType2Class.Keys do
begin
Val := TValue.From<C>(FType2Class[Key]);
if Val.AsClass = AClass then
Exit(Key);
end;
raise EGenericFactoryNotRegistered.Create;
end;
{ EGenericFactory }
constructor EGenericFactory.Create;
begin
inherited Create(Self.ClassName);
end;
end.

Resources