Yeah, I know: Use
Tag. But no, I won’t. If I can. But sometimes I cannot. But let me explain the problem first:
Many times there is a need to have metadata which has at least one of the properties bellow…
- added at runtime
- to objects which we don’t control their class specification
…then we are in trouble… Or use the plain ol’
Tag. Store in the
Tag property (which is btw a ‘me too’ from Visual Basic) any form of coded information. Many of you put there even an object pointer. But did you thought what will happen if this pointer goes to 64-bit and the Tag’s size stays the same? …or any other thing which will break your assumptions, of course. Because using this technique you made several. In fact you break the TObject’s abstraction.
Or, of course, another approach is to use a parallel data structure (for example dictionaries in D2009 and especially in D2010) to keep this metadata – but, yes, syncing the lifetime of these two entities (the main object and the metadata) can be a royal pain. Also the object cannot ‘ask’ even using the reflection, what metadata is attached to it.
Another approach is by using attributes in Delphi 2010. Very nice indeed, but for this you must control the class specification. Also, you cannot add code.
A better ‘workaround’ one can see in some libraries which add a
TagString: string; property. Well, that’s definitely better but, of course, their implementation is limited to their library. No
TObject.TagString – no fun.
A yet (somewhat) better one is the
TagObject: TObject – but again this isn’t added to the
TObject so you’re limited to that library which uses it.
A much better one is class helpers.
As you perhaps know, the class helpers extend the functionality of the class which we want to alter, by adding “some methods” to it. But the class helpers can do more than this: from Delphi 2009 onward (IIRC) – sure in Delphi 2010 they support inheritance and overriding virtual methods between them. Hence the thing bellow will compile and work:
TPanelHelper = class helper for TPanel public procedure ChangeCaption; virtual; end; TPanelHelper2 = class helper (TPanelHelper) for TPanel public procedure ChangeCaption; override; //yes! procedure ChangeColor; //just another method shown as an example end; implementation procedure TPanelHelper.ChangeCaption; begin Caption:='['+Caption+']'; end; procedure TPanelHelper2.ChangeCaption; begin inherited ChangeCaption; //the TPanelHelper's method // inherited; - won't work. Caption:='('+Caption+')'; end; procedure TPanelHelper2.ChangeColor; begin ParentBackground:=False; if Color=clGreen then Color:=clBtnFace else Color:=clGreen; end;
So this in this way you can add dynamic functionality in the form of code. Of course, bear in mind that even if you can define and associate multiple class helpers with a single class type, only zero or one class helper applies in any specific location in source code. The class helper defined in the nearest scope will apply. Class helper scope is determined in the normal Delphi fashion (i.e. right to left in the unit’s uses clause). (snipped from Delphi’s help – thanks guys).
But we must note that we cannot override the virtual methods of the helped class.
And here is a big discussion: is good to allow the developer to inject new functionality in there? Or perhaps it looks dangerous and a mechanism similar with the events to sandbox the changes would be more appropriate? What are your opinions on this?
…returning to our main theme (adding functionality), in Delphi 2010, you can have
class constructors and
class destructors also in the Class Helpers which is a great feature for doing a bunch of things. For example…
Ok, we cannot have fields in the Class Helpers. And this is bad. But we can simulate them. And this is good. Even if we cannot have instance fields we have class fields and we can use such a class var to keep our metadata. And of course we use the class constructors and destructors to manage the life of our dictionary:
TPanelHelper2 = class helper for TPanel public class var MetaData: TDictionary<TPanel, string>; //store some strings - an example procedure PutData(aData: string); function ReadData: string; class constructor SetUp; class destructor TearDown; end; implementation procedure TPanelHelper2.PutData(aData: string); begin MetaData.AddOrSetValue(Self, aData); end; function TPanelHelper2.ReadData: string; begin if MetaData.ContainsKey(Self) then Result:=MetaData.Items[Self] else Result:=''; //or whatever end; class constructor TPanelHelper2.SetUp; begin MetaData:=TDictionary<TPanel, string>.Create; end; class destructor TPanelHelper2.TearDown; begin MetaData.Free; end; //Unfortunately we cannot override a virtual member in the Helped Class. Sigh! //procedure TPanelHelper2.FreeInstance; //begin // if MetaData.ContainsKey(Self) then // MetaData.Remove(Self); // // inherited; //end;
As you see we can do quite a lot with the class helpers. But I think that there are some improvements to make with regard to our theme:
- A compiler warning showing which class helper is actually used. Something like “Son, you use TFooHelper5 from Unit3 for the class TFoo” – Wording subject of change, of course. Also, another thing to consider is to allow many class helpers to attach to the class, bringing them more closer to a class fragments / partial class behavior.
- Allow to override the virtual / dynamic members of the helped class. (hmm… hmmmm…. mmmm….)
- Remove the “[DCC Error] Unit4.pas(75): E2169 Field definition not allowed after methods or properties” limitation – of course this will help also the Class Helpers, no? (if we believe the compiler, then this is the limitation which keeps us away for having fields in class helpers)
- As a complementary feature, explore the possibility to add a Property
TObject.ObjectList: TObjectList;which will encapsulate a somewhat similar mechanism. If someone wants to, please let me know and I’ll expand the topic. The downside here is that you must hard cast every time the members of the list to the appropriate type.
…have other ideas for this?