Tuesday, June 15, 2004

Singletons and more...

I just read Nick's excellent post on Singleton datamodules explaining how he accessed a datamodule using a global function.

Which got me thinking, a fairly rare occurance these days. What went through my mind was: What if someone, ignorantly or maliciously, did a

var MyModule : TMyDatamodule;
begin
MyModule := TMyDataModule.Create(nil);
end;


I know what you're thinking. "You're supposed to use the global function!!!!". Er...this is a big team, someone forgot to document that, I was trying to figure out what would happen, I don't really like you...tons of excuses! And you'll only figure out something was wrong when you did a code review, or a synchronization or resource conflict occured...and probably have ZERO control over it if you were writing a library (like Borland is with the "Printer" variable).

Now, what could be better than to allow users to use the ".Create" convention, but return them a singleton instance on subsequent calls? It is possible. And no hacks.

Here's the scoop: Override the NewInstance function. This is a class (static for you C++ folks) function that gets called during object construction. Here's an example. I've used a form, with a label on it. Each unique instance gets a "Number" - so we can track what's going where. In this class, TForm2, (laziness written all over the name, of course):


var UseSingleTon : boolean = False;

implementation

var
iNUM : integer = 1;
Form2: TForm2 = nil;

class function TForm2.NewInstance: TObject;
begin
// don't call inherited if we already have an instance
if UseSingleTon then
if Assigned(Form2) then
begin
Result := Form2;
Exit;
end;

Result := inherited NewInstance;
end;


UseSingleton is a global variable that's in there only for demonstration. You can change it or set it to true forever.

You calling code is:

procedure TForm1.Button1Click(Sender: TObject);
var frm : Tform2;
begin
frm := Tform2.Create( self );
frm.Show;
end;

Will this work? Actually, No. It's more than just that. There are two things you want to avoid:

a) Having the base class constructor (TForm.Create) run on every subsequent call to TForm2.Create. It should run just once!
b) Even the FormCreate call should run only once.

This isn't very difficult, just needs a little more searching. Here's what I did:

procedure TForm2.AfterConstruction;
begin
if UseSingleTon and Assigned(Form2) then Exit;
inherited;
Form2 := Self;
end;

constructor TForm2.Create(AOwner: TComponent);
begin
if UseSingleTon and Assigned(Form2) then Exit;
inherited;
end;


Simply put, I'm avoiding calling the base constructors or construction based functions if the object is alive.

Next, we have to ensure that destruction is all right too - since we're going to use .Create to create the instance, we'll expect to use .Free to free it. A singleton should only be freed when the program shuts down, probably in a .Free that you have set up in a finalization section. For this I have included some "reference counting" in the code. But I'm way too lazy to type it all. So, download the code and check it out.

2 Comments:

Blogger LachlanG said...

That's a great idea Shenoy, although there's a case there for raising an exception and explaining what they did wrong rather than allowing them to continue against the project standards.

2:00 PM  
Blogger Deepak Shenoy said...

You can introduce the warning or exception in the NewInstance call. It might be a better idea to use an Assert, since this is a programmer error.

5:38 AM  

Post a Comment

<< Home