In software engineering, the
singleton pattern is a design pattern used to implement the mathematical
concept of a singleton, by restricting the instantiation of a class to one
object. This is useful when exactly one object is needed to coordinate actions across
the system. The concept is sometimes generalized to systems that operate more
efficiently when only one object exists, or that restrict the instantiation to
a certain number of objects.
Basically, there can be only one
instance of a singleton class throughout the system. In Delphi there are three
common methods (that I aware of) to implement singleton class.
1.
Using global variable
2.
Using global function
3.
Internally managed by the class
Personally I call the last one as the "true" singleton implementation. You will see the reason later when I explain the method.
1. Using global variable
In this method, the singleton object is referenced by a global variable. Usually the object is instantiated in initialization section of the unit and later be freed in finalization unit. Something like this:
Code:
unit
GlobalVarSingleton;
interface
type
TConfig=class
private
FName: string;
FTimeStamp: TDateTime;
public
property Name: string read FName write
FName;
property TimeStamp: TDateTime read
FTimeStamp write FTimeStamp;
end;
var
Config: TConfig; // this is our global variable of our
singleton
implementation
initialization
Config := TConfig.Create;
finalization
// this line will raise EAccessViolation if
the instance ever got
// freed without setting Config variable to
nil.
Config.Free;
end.
This is the simplest method to
achieve singleton class. However this method actually is still far from
actually be singleton class. Since the caller has full control of constructing
and destructing the corresponding object. Caller can simply choose to free
existing one and create another one, which making information already
accumulated in the old one lost.
Pros:
Pros:
·
The most efficient, with the
smallest overhead. Since we work with direct reference, no cpu cycles need to
be wasted on stack operation.
·
The simplest to use.
Cons:
·
The most fragile of all methods. a
bit confusion on your side (or any other coders) could easily lead to incorrect
free-ing of the singleton object. Which lead to several errors which seemingly
unrelated at first sight.
Note that although this method is the most efficient one, but in modern computers the overhead caused by other methods usually is insignificant.
2. Using global function
This technique is also called lazy loading, because the object will not be intantiated until the first call to the function. This method uses local unit variable instead of a global one. The global function checks if the singleton object is already created or not. When not, it creates the object and returns it. Something like this:
Code:
unit
GlobalFunctionSingleton;
interface
type
TConfig=class
private
FName: string;
FTimeStamp: TDateTime;
public
property Name: string read FName write
FName;
property TimeStamp: TDateTime read
FTimeStamp write FTimeStamp;
end;
function
Config: TConfig; // note that Config now is a
function
implementation
var
uConfig: TConfig;
function
Config: TConfig;
begin
if uConfig=nil then
uConfig := TConfig.Create;
Result := uConfig;
end;
initialization
finalization
// this line will raise EAccessViolation if
the instance ever got
// (incorrectly) freed through the instance
returned by Config function
uConfig.Free;
end.
Note that Config in the first
unit (GlobalVarSingleton) is a global variable, but in the above Config
is a global function;
Pros:
Pros:
1.
You can not accidentally create new
instance of your singleton.
2.
Better for "dead code
removal". Since we are lazy loading instead of instatiating in
initialization section, if in our application we never call Config or
use any part of TConfig, then codes related with them will not be
compiled into the final executable. Different if we always instatiated in
initialization, where some codes related with TConfig will always be
included in the final executable, even if we never actually use it on other
part of our program.
Cons:
·
You still can (albeit usually it's
accidentally) free the singleton. This will lead to access violation for
subsequent calls to Config, and another one when the code enters
finalization section.
3. Internally managed by the class
In this method, the singleton restrictions is done internally by the class. Not by some external code. The process is similar with global function technique, by checking a local unit variable.
True Requirements of Singleton
The other methods might be able to provide only one instance throughout the system's lifetime. But a little bit carelessness will yield into access violation error. Since singleton objects usually applied to important or key objects, making a little error on these will make the whole system unusable. A restart is inevitable.
Let's sum up the requirements of "true" singleton.
1.
Except for the first time, creating
new instance of "true" singleton should always returns existing one.
2.
Destroying the singleton object will
not really destroy it, unless done as designed (e.g. when application is
terminated, or when another key/container object is destroyed).
And our true singleton goes something like this:
Code:
unit
TrueSingleton;
interface
uses
SysUtils
;
type
TConfig=class
private
FName: string;
FTimeStamp: TDateTime;
public
class function NewInstance: TObject;
override;
procedure FreeInstance; override;
property Name: string read FName write
FName;
property TimeStamp: TDateTime read
FTimeStamp write FTimeStamp;
end;
implementation
var
uConfig: TObject;
uFinalized: Boolean;
{
TConfig }
procedure
TConfig.FreeInstance;
begin
if uFinalized then
inherited FreeInstance;
end;
class
function TConfig.NewInstance: TObject;
begin
if uConfig=nil then
uConfig := inherited NewInstance;
Result := uConfig;
end;
initialization
finalization
uFinalized := True;
uConfig.Free;
end.
Class Function NewInstance
When a class is to be instantiated, the class asks memory manager to provide memory location large enough for new instance of the class. This is done by virtual class function NewInstance. We can override this method if we want to reuse a special memory location.
In our sample code, we want any construction call of TConfig to always result in instance pointed by uConfig. Of course if uConfig has not been initiated (indicated by its value of nil), we want to call "original" NewInstance which will handle the process with memory manager.
Procedure FreeInstance
When an object is destroyed, it will call virtual procedure FreeInstance. This method is responsible to "return" the memory previously occupied by the object to memory manager. So this is where the object got wiped out.
If we want to prevent our singleton to be wiped out before designated time (i.e. before our program is terminated), this method is the best place. So, let's override this method. In the overriding method we checks if the freeing is done in finalization section or not. When the freeing is done in finalization section (seen from the uFinalized flag) we continue to the "original" FreeInstance otherwise we ignore the freeing request.
Pros:
When a class is to be instantiated, the class asks memory manager to provide memory location large enough for new instance of the class. This is done by virtual class function NewInstance. We can override this method if we want to reuse a special memory location.
In our sample code, we want any construction call of TConfig to always result in instance pointed by uConfig. Of course if uConfig has not been initiated (indicated by its value of nil), we want to call "original" NewInstance which will handle the process with memory manager.
Procedure FreeInstance
When an object is destroyed, it will call virtual procedure FreeInstance. This method is responsible to "return" the memory previously occupied by the object to memory manager. So this is where the object got wiped out.
If we want to prevent our singleton to be wiped out before designated time (i.e. before our program is terminated), this method is the best place. So, let's override this method. In the overriding method we checks if the freeing is done in finalization section or not. When the freeing is done in finalization section (seen from the uFinalized flag) we continue to the "original" FreeInstance otherwise we ignore the freeing request.
Pros:
1.
More reliable. There is no chance
that you accidentally free the singleton.
2.
"Dead code
removal"-friendly.
3.
Less possible memory leak, since you
can keep using this pattern of code that is very good practice in preventing
memory leak:
Code:
vObject := TMyClass.Create;
try
...
...
...
finally
vObject.Free;
end;
4.
This method might help promoting low
coupling principle in your projects. Especially if you use same base framework
for many of your projects.
Cons:
·
Higher overhead compared to other
methods. However this is very insignificant compared to the power of nowadays
computers.
·
You can not do local
variables initialization and finalization in by usual overriding of Create
and Destroy methods. Instead you must do initializations in NewInstance
and finalizations in FreeInstance.
No comments:
Post a Comment