
|
|
Creating a subclassable singleton class in Delphiby Craig Manley, 1 december 2002 (updated 11 december 2006).After searching for techniques to create singleton classes in Delphi, I found none that could be subclassed. See for example Creating a real singleton class in Delphi. I therefore decided to create such a class myself, roughly based on the example in that link. The Singleton class' unit
Usage:Below is the listing of this unit. To create a new singleton class, you must subclass this class and implement the CreateSingleton() method. This method is called automatically and only once - when the Create() constructor is called for the first time. Use this method to initialize your singleton object.
unit ssSingleton;
interface
uses SysUtils;
type
EssSingletonException = class(Exception);
TssSingleton = class(TObject)
private
class function RefCount: Cardinal;
public
constructor Create;
class function NewInstance: TObject; override;
procedure FreeInstance; override;
procedure CreateSingleton; virtual; abstract;
end;
implementation
uses Classes;
type
// Define structure that contains the reference count and object pointer.
PssSingletonInstance = ^TssSingletonInstance;
TssSingletonInstance = record
refcount: Cardinal;
instance: TObject;
end;
// The instances: classname => TssSingletonInstance pairs
var
instances: TStrings;
procedure CleanupInstances;
var
i: integer;
begin
for i := instances.Count-1 downto 0 do begin
if PssSingletonInstance(instances.Objects[i])^.refcount > 1 then begin
PssSingletonInstance(instances.Objects[i]).instance.Free;
if (i <= instances.Count) then begin
Dispose(PssSingletonInstance(instances.Objects[i]));
instances.Delete(i);
end;
end
else
PssSingletonInstance(instances.Objects[i]).instance.Free;
end;
instances.Free; // Added 2006-12-11, thanks to Graeme Geldenhuys.
end;
constructor TssSingleton.Create;
begin
inherited;
if (RefCount <= 1) then
CreateSingleton;
end;
procedure TssSingleton.FreeInstance;
var
i: integer;
instance: PssSingletonInstance;
begin
i := instances.IndexOf(ClassName);
if i < 0 then begin // no instance found!
inherited;
Exit;
end;
instance := PssSingletonInstance(instances.Objects[i]);
if (instance^.refcount > 1) then
Dec(instance^.refcount)
else begin
inherited FreeInstance; // free instance here
// free and remove the PssSingletonInstance from the list.
Dispose(PssSingletonInstance(instances.Objects[i]));
instances.Delete(i);
end;
end;
class function TssSingleton.NewInstance: TObject;
var
i: integer;
instance: PssSingletonInstance;
begin
i := instances.IndexOf(ClassName);
if i >= 0 then begin // instance found
instance := PssSingletonInstance(instances.Objects[i]);
end
else begin
new(instance);
instance^.instance := inherited NewInstance;
instance^.refcount := 0;
instances.AddObject(ClassName,TObject(instance));
end;
Inc(instance^.refcount);
Result := instance^.instance;
end;
class function TssSingleton.RefCount: Cardinal;
var
i: integer;
instance: PssSingletonInstance;
begin
i := instances.IndexOf(ClassName);
if i >= 0 then begin // instance found
instance := PssSingletonInstance(instances.Objects[i]);
Result := instance^.refcount;
end
else
raise EssSingletonException.Create('Class "' + ClassName +
'" not found in instances list!');
end;
initialization
instances := TStringList.Create;
finalization
CleanupInstances;
end.
This is how it works:The NewInstance() method overrides the same method in the parent class, TObject. This method must return an instance. Only on the 1st call to get a singleton instance from the Singleton class should the parent method be called (this method then actually allocates memory etc. to create a new instance). All subsequent calls must return the same instance, so in these cases, the parent method must not be called. Because it must return the same instance every time (that's singleton), the instance must be stored in a global variable somewhere in the unit, but because this instance must also be associated with the class it's being used in in order to prevent it from being shared amongst different classes with unpredictable results, it must be associated with the name of the class it was created in. To do this, all instances are stored in a TStrings and are pointed to by their class names. TStrings are useful for this because you can associate strings with objects (or any other 32 bit data such as pointers).About threads:This class is not thread safe just as much of the VCL isn't thread safe. Using this class in Delphi TThreads is safe though because they are designed to work with the non-thread-safe VCL. You can easily make this class thread safe by wrapping all tricky bits in critical sections, but for most Delphi programs, this isn't necessary and is just a (small) performance burden.Two simple examples of subclasses of the Singleton classBelow are 2 listings. They are both singleton classes meant for storing and retrieving the name of a fruit or vegetable. These are of course not very useful classes but are only meant for demonstration anyway. Both classes descend from TSingleton and add a private field FData with and accessor property Data. I named these classes SingletonFruit and SingletonVeg respectively. All descendents of TSingleton must override the CreateSingleton() method. This method is called automatically from the constructor when the constructor is called for the first time.
unit SingletonFruit;
interface
uses ssSingleton;
type
TSingletonFruit = class(TssSingleton)
private
FData: string;
public
procedure CreateSingleton; override;
property Data: string read FData write FData;
end;
implementation
procedure TSingletonFruit.CreateSingleton;
begin
FData := 'Banana'; // the default fruit name
end;
end.
unit SingletonVeg;
interface
uses ssSingleton;
type
TSingletonVeg = class(TssSingleton)
private
FData: string;
public
procedure CreateSingleton; override;
property Data: string read FData write FData;
end;
implementation
procedure TSingletonVeg.CreateSingleton;
begin
FData := 'Sprouts'; // the default vegetable name
end;
end.
A simple usage exampleBelow is the listing of a test program to check if this concept really works.
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils,
SingletonFruit in 'SingletonFruit.pas',
SingletonVeg in 'SingletonVeg.pas';
var
fruit1, fruit2: TSingletonFruit;
veg1, veg2: TSingletonVeg;
begin
fruit1 := TSingletonFruit.Create;
fruit2 := TSingletonFruit.Create;
veg1 := TSingletonVeg.Create;
veg2 := TSingletonVeg.Create;
fruit1.Data := 'Apple';
fruit2.Data := 'Lemon';
veg1.Data := 'Tomato';
veg2.Data := 'Potato';
writeln('Fruit 1 contains: ' + fruit1.Data);
writeln('Fruit 2 contains: ' + fruit2.Data);
writeln('Vegetable 1 contains: ' + veg1.Data);
writeln('Vegetable 2 contains: ' + veg2.Data);
end.
Executing the program above results in this output as expected: Fruit 1 contains: Lemon Fruit 2 contains: Lemon Vegetable 1 contains: Potato Vegetable 2 contains: Potato Some singleton ideasThe singleton class above is a module wide singleton. If your application
consists of an EXE and a DLL, each with the same singleton class, then
you'll end up with 2 singleton instances instead of 1 as you might expect. |