(This got a bit long. You might want to print it out and save it for the
next time you go to the bathroom, or maybe if you are having trouble
sleeping.)
Someone recently suggested the Singleton pattern as a way of
encapsulating a bunch of constants. For those not familiar with the
idea, a "Singleton" class is a class that allows only one instance of
itself. Repeated attempts to create another instance are denied in one
way or another.
So I thought I'd write one in Delphi. This would normally be a trivial
problem, but I had a few extra rules:
1. Syntactically, I wanted the creation to be identical to that of a
normal object. You should be able to say
ASingleton := TSingleton.Create;
to create it.
2. Repeated attempts to instantiate a Singleton object would silently
deny the instantiation and simply return the first and only Singleton.
3. Anyone who inherits from the Singleton class will have a Singleton
class themselves, without having to do any work, save for calling the
inherited constructor.
As it turns out, any two of these three can be done quite easily, but
getting all three to work together is tough. Here's what I've
discovered:
1. The creation syntax of the Singleton class can be identical to a
normal constructor, because of the way Delphi does its constructors. To
wit, it is impossible for the client to tell if it is calling a
constructor proper or just a class method. Compare this to C++, where
you have that crazy new operator (oh, I suppose you could overload it).
C++ implementations generally cede this point and resort to hiding the
constructor and using a static method like "Instance" (Gamma, et al).
Note that if you want to hide the constructor "Create", a public class
method of the same name is pretty much the only way to do it. Just
declaring "constructor Create" in the private section doesn't work,
because the compiler will default to finding the first public Create
constructor or class method in the inheritance chain. Yuck.
2. By using a class method instead of a constructor, we can effectively
deny the creation of any new Singletons and instead return a previous
instance. I know Delphi allows for exceptions in constructors, and will
supposedly clean up after itself, but I had difficulty with this.
Specifically, after the destructor executed, I got a message box with
the red X, but no text. Tracing it through the de{*word*81} didn't help. It
just happens. So, my scheme is as follows:
constructor TSingleton.ActualCreate; // This is private and virtual.
begin
inherited Create;
// Class-specific creation stuff.
end;
class function TSingleton.Create: TSingleton; // Public and virtual.
begin
// Pseudo-code:
if AlreadyCreated(Self) then
Result := PrevInstance(Self)
else
Result := Self.ActualCreate;
end;
What "Self" is doing in the class function will become clear below.
3. Here's the tricky one: Making sure that people who inherit from
TSingleton don't have to do any work at all to make their class a
Singleton. In the implementation section of the unit, there is an
instance of a TSingletonRegistry class. TSingletonRegistry has a
TStringList, which holds the one allowed instance for each Singleton
class. It uses the metaclass information as a key. So, in the Create
function above, Self is actually the metaclass that describes the class
trying to be instantiated (since the method is virtual, we guarantee
that we'll always have the right metaclass, not just a "class of
TSingleton"). It passes that to the SingletonRegistry, which gets its
String description (using ClassName) and looks up its associated object,
if one exists.
This is slick, and it works well. So if you're still following, here's
where we have problems: A class that inherits from TSingleton has to do
more work than I like. Certainly a class should override the
ActualCreate constructor, inserting its own creation stuff and calling
inherited. However, a class *shouldn't* have to override the class
function Create. All of the checks are already correctly done in
TSingleton, by virtue of being a virtual function. But, Create can only
return a TSingleton instance, which isn't good enough.
Imagine I have a TMySingleton class. Its Create function should return
an instance of TMySingleton. But if it's going to override the original
Create function in TSingleton, it has to have the same Result type,
namely TSingleton. A variable of type TMySingleton, then, can't call
TMySingleton.Create, because that only returns a TSingleton, and thus
the statement will generate a compile-time error.
I have discovered two solutions, neither of which are satisfactory:
1. Don't override. Instead, just make a new virtual class function
called Create, which returns a TMySingleton instance. The body of the
function would then be
Result := TMySingleton(inherited Create);
or, perhaps more robustly,
Result := inherited Create as Self;
However, this requires the class writer to write more code than I like.
It violates the reusability principle, I think.
2. Make the original TSingleton.Create function return a Pointer. Then
everyone can override without thinking, or not even override at all.
However, this destroys all the type-checking that makes Delphi and OO so
nice.
The ultimate solution would be to have a function that always returns
its class type:
class function TSingleton.Create: WhateverTypeIAm; virtual;
This is generally called "covariance", and isn't allowed in too many
languages (C++, and some more arcane languages in academia).
The only way I could see to implement this in Delphi is through its
virtual constructor mechanism, but I haven't been able to raise
exceptions properly in constructors.
Thoughts, anyone?
Dave