While working on an internal project I came into a situation where a user (that is, a fellow programmer) would have to create a hierarchy of classes. As it turned out, this hierarchy would contain almost no implementation, just the class declarations, with one exceptions – every class would still have to be responsible for creating its children objects. Then I had a thought. Maybe I could use attributes and RTTI to do that in one central place instead of in every object.
The Problem
I want to implement following classes …
type TObjectB = class FData1: integer; FData2: string; FData3: boolean; end; TObjectA = class strict private FObjectB: TObjectB; public constructor Create; destructor Destroy; override; end; { TObjectA } constructor TObjectA.Create; begin inherited Create; FObjectB := TObjectB.Create; end; destructor TObjectA.Destroy; begin FreeAndNil(FObjectB); inherited; end;
… but I’m too lazy to write constructor and destructor. What can I do?
The Result
Given the proper infrastructure, the code above can be rewritten as follows.
type TObjectB = class FData1: integer; FData2: string; FData3: boolean; end; TObjectA = class(TGpManaged) strict private [GpManaged] FObjectB: TObjectB; end;
All the implementation is hidden in the TGpManaged class, which is described below. It is implemented in the GpAutoCreate unit, which is a part of my open-sourced GpDelphiUnits package, together with the test program TestGpAutoCreate.
The Solution
The TGpManaged class implements only a constructor and a destructor. The constructor will automatically create fields in the derived class and destructor will automatically destroy them.
type TGpManaged = class public constructor Create; destructor Destroy; override; end;
As it is not always a good idea to automatically create/destroy everything, the fields that are to be managed in this way must be marked with a [GpManaged] attribute which is implemented in the same unit.
type GpManagedAttribute = class(TCustomAttribute) public type TConstructorType = (ctNoParam, ctParBoolean); strict private FBoolParam : boolean; FConstructorType: TConstructorType; public class function IsManaged(const obj: TRttiNamedObject): boolean; static; class function GetAttr(const obj: TRttiNamedObject; var ma: GpManagedAttribute): boolean; static; constructor Create; overload; constructor Create(boolParam: boolean); overload; property BoolParam: boolean read FBoolParam; property ConstructorType: TConstructorType read FConstructorType; end;
The attribute can be specified in two ways – either as [GpManaged] (which will call the parameterless Create constructor to create the attribute object) or as a [GpManaged(false)] or [GpManaged(true)] which will call the Create with a boolean parameter. A field marked with the former version will be created by a call to the parameterless constructor and a field marked with the latter version will be created by a call to a constructor accepting one boolean parameter. Support for constructors with different parameter lists can be added at will.
The boolean-parameter-constructor version was added specifically for the TObjectList creation. Calling TObjectList.Create will create an object list owning its items, which is OK in most cases. If you, however, want the object list not to own its items, you have to create it as follows.
[GpManaged(false)]
FList2: TObjectList;
For further details on the GpManagedAttribute implementation, see the source code.
Creating Fields
Fields marked with any version of the [GpManaged] attribute are created in the TGpManaged.Create constructor.
The code first accesses the enhanced RTTI context and finds the information for the object that is being constructed (ctx.GetType(Self.ClassType)). Next it iterates over all fields defined in this object.
For each field it verifies if the feld was marked with the [GpManaged] attribute. If not, next field is tested.
Otherwise, the code loops over all methods called Create. (I intentionally left out the support for constructors not named Create.) For each method the code checks whether the constructor has appropriate number and type of parameters.
In both cases, the code calls ctor.Invoke(f.FieldType.AsInstance.MetaclassType) to invoke the constructor. Appropriate parameters are passed in the second parameter. The result is stored into the field by calling the f.SetValue and whole procedure is repeated for the next field.
constructor TGpManaged.Create; var ctor : TRttiMethod; ctx : TRttiContext; f : TRttiField; ma : GpManagedAttribute; params: TArray<TRttiParameter>; t : TRttiType; begin ctx := TRttiContext.Create; t := ctx.GetType(Self.ClassType); for f in t.GetFields do begin if not GpManagedAttribute.GetAttr(f, ma) then continue; //for f for ctor in f.FieldType.GetMethods('Create') do begin if ctor.IsConstructor then begin params := ctor.GetParameters; if (ma.ConstructorType = GpManagedAttribute.TConstructorType.ctNoParam) and (Length(params) = 0) then begin f.SetValue(Self, ctor.Invoke(f.FieldType.AsInstance.MetaclassType, [])); break; //for ctor end else if (ma.ConstructorType = GpManagedAttribute.TConstructorType.ctParBoolean) and (Length(params) = 1) and (params[0].ParamType.TypeKind = tkEnumeration) and SameText(params[0].paramtype.name, 'Boolean') then begin f.SetValue(Self, ctor.Invoke(f.FieldType.AsInstance.MetaclassType, [ma.BoolParam])); break; //for ctor end; end; end; //for ctor end; //for f end;
Destroying Fields
Fields are destroyed in the similar manner except that the Destroy destructor is called instead of constructor. The code is much simpler because it doesn’t have to check which destructor to call.
destructor TGpManaged.Destroy; var ctx : TRttiContext; dtor: TRttiMethod; f : TRttiField; t : TRttiType; begin ctx := TRttiContext.Create; t := ctx.GetType(Self.ClassType); for f in t.GetFields do begin if not GpManagedAttribute.IsManaged(f) then continue; //for f for dtor in f.FieldType.GetMethods('Destroy') do begin if dtor.IsDestructor then begin dtor.Invoke(f.GetValue(Self), []); f.SetValue(Self, nil); break; //for dtor end; end; //for dtor end; //for f end;
Potential Problems
You should always keep in mind that this approach is much slower than the “manual” way. I didn’t make any tests but I wouldn’t be surprised if the automatic way is 100 times slower. It is, however, fast enough to manage objects that are created/destroyed only occasionally.
The other problem is that you have to change the parent of your classes to TGpManaged. If this is not an option and you still want to use the “automagic” lifecycle management, you will have to copy my code into your base classes.
Update 2012-10-29
As suggested by [Stefan Glienke] in comments I have refactored field creation/destruction code into class procedures CreateManagedChildren and DestroyManagedChildren which you can call from your base classes to achieve the same functionality.
Very cool
ReplyDeleteIn order to use any class, you may be able to hook the constructor and destructor (if both are virtual), following the interceptor pattern.
ReplyDeleteSuch a nice feature should be made available at RTL level, IMHO.
With a common/standard way of implementing dependency injection, for the same price.
But I'm not sure that Embarcadero is going into this direction, sadly.
Really cool. However I would extract the autocreate logic into a separate (static) class and only call it from your TGpManaged.Create/Destroy. That way everyone can easily use it even not inheriting from TGpManaged.
ReplyDeleteAlso a small adjustment. Checking for the boolean Parameter can be made easier by just checking for the typeinfo: params[0].ParamType.Handle = TypeInfo(Boolean)
Stefan, thanks on both counts! Extracting the autocreate logic is a great idea and I totally forgot about that old Typinfo boolean trick.
DeleteUpdated code is in the SVN. Thanks for your suggestions!
Deleteseems lacking owner/child constructor automation like NewObject := TNewObject.Create(const OwnerObject);
ReplyDeleteSorry, I don't understand.
DeleteThat's pretty cool. I like it when developers implement higher levels of thinking.
ReplyDelete