| Computer | Singleton mit Objectpascal | Hauptseite |
Manchmal ist es nötig, dass eine Klasse projektweit nur ein einziges Mal erzeugt werden kann. Sei es, dass Ressourcen
nicht unnötig belegt werden oder Daten an einer zentralen Stelle zusammenfließen sollen. Eine Möglichkeit dies
umzusetzen, ist das Singleton-Muster, auf deutsch: Einzelstück. Ein Beispiel für die Verwendung und die Implementation
ist in diesem Artikel beschrieben.
Für das Tutorial sollte man grundlegende Erfahrung in der objektorientierten Programmierung mit ObjectPascal besitzen.
Die Beispiele wurden mit FreePascal (Version 2.2.0) entwickelt, sind aber von Prinzip her genauso auf Delphi anzuwenden. Lauffähige
Beispiele können am Ende dieses Dokuments heruntergeladen werden.
An dieser Stelle noch ein herzliches Dankeschön an Marcus Viererbe, der mich auf einen größeres Problem in der
ursprünglichen Version dieser Anleitung hinwies.
Die Freepascal Free Component Library bringt in der Unit EventLog eine Klasse mit, mit der Programme um Logging-Funktionalitäten erweitert werden können, das TEventLog. Die Logeinträge werden standardmäßig an das Systemlog gesendet, könne aber auch alternativ in eine Datei geschrieben werden. In diesem Fall kommt es jedoch zu Problemen, wenn mehr als eine TEventLog-Instanz existiert.
program TestEventLogger;
uses
Classes, SysUtils, EventLog;
var
A, B: TEventLog;
begin
A := TEventLog.Create(nil);
B := TEventlog.Create(nil);
A.LogType := ltFile;
B.LogType := ltFile;
A.Active := True;
B.Active := True;
A.Info('Info from A.');
B.Info('Info from B.');
FreeAndNil(A);
FreeAndNil(B);
end.
Die Linuxversion überschreibt die Logdatei von A mit der Ausgabe von B, unter Windows crasht das Programm mit einer Exception. Die Ursache ist klar, zwei Objekte versuchen gleichzeitig auf die selbe Datei zuzugreifen. Das kann man natürlich in diesem Fall leicht beheben, aber was macht man mit einem Projekt, welches aus zehntausend verschiedenen Units besteht, in denen die Logaufrufe verstreut sind? Ein klarer Fall für ein Singleton.
Dazu basteln wir uns eine neue Klasse in einer eigenen Unit. Erste Anforderung: die Klasse darf nicht einfach so erzeugt werden können, dies wollen wir unter unserer Kontrolle belassen. Das ist einfach zu realisieren, wir verstecken einfach den Konstruktor.
type
TSingleton = class(TObject)
protected
constructor Create;
end;
Damit ist es nun nicht mehr möglich diese Klasse zu erzeugen, bis auf eine kleine Ausnahme. Die Methoden der Klasse selbst
(und ihre Nachfahren) können noch auf den Konstruktor zugreifen.
Aber wie kommen wir dann überhaupt an eine Instanz? Ohne Konstruktor kann man ja keine erzeugen und hat damit keine Methode, die darauf
zugreifen kann. Das Problem löst sich auf recht einfache Weise mit Hilfe der Klassenmethoden auf. Diese sind
unabhängig von eine konkreten Instanz (ein Konstruktor selbst ist ja eigentlich auch nichts anderes) sind aber
Methoden der Klasse und dürfen damit auch auf Protected und Private Methoden zugreifen.
type
TSingleton = class(TObject)
public
class function GetInstance: TSingleton;
constructor Create;
end;
GetInstance gibt also eine TSingleton-Instanz zurück und diese Methode könnte auch ein entsprechendes Objekt erzeugen. Dieses Objekt speichern wir als globale Variable im Implementation-Teil der Unit, damit es von außen nicht sichtbar ist. Die Funktion GetInstance muss dann nur noch prüfen ob das Objekt bereits existiert, im negativen Fall ein neues erzeugen und die Instanz zurückgeben.
implementation var Singleton: TSingleton; class function TSingleton.GetInstance: TSingleton; begin if (Singleton = nil) then Singleton := TSingleton.Create; Result := Singleton; end;
Damit haben wir ein Singleton, allerdings ein recht nutzloses. Zeit dem ganzen auch noch etwas Funktionalität zu geben. Wir könnten jetzt einige Methoden zum Logging einbauen und diese an ein TEventLog weiterreichen, in Hinblick auf die Einfacheit des Beispiels, werden wir aber bloss eine NurLesen-Property zur Verfügung stellen, über die alle Log-Funktionen ablaufen. Der geneigte Leser kann ja gerne eine bessere Schnittstelle implementieren.
type
TSingleton = class(TObject)
private
FEventLog: TEventLog;
protected
constructor Create;
public
class function GetInstance: TSingleton;
destructor Destroy;
property EventLog: TEventLog read FEventLog;
end;
Das Eventlog wird im Konstruktor initialisiert und im Destruktor wieder zerstört.
constructor TSingleton.Create; begin inherited Create; FEventlog := TEventlog.Create(nil); FEventLog.LogType := ltFile; FEventLog.Active := True; end; destructor TSingleton.Destroy; begin FreeAndNil(FEventLog); inherited Destroy; end;
Unser Programm vom Anfang ändern wir jetzt einfach ab. Statt dem Erzeugen von zwei TSingletons (was ja nicht mehr möglich ist), weisen wir A und B einfach die Eigenschaft EventLog von GetInstance zu.
program TestEventLogger;
uses
Classes, SysUtils, EventLog, SingletonTest;
var
A, B: TEventLog;
begin
A := TSingleton.GetInstance.EventLog;
B := TSingleton.GetInstance.EventLog;
A.Info('Info from A.');
B.Info('Info from B.');
end.
Und nun funktioniert alles so wie gewünscht. In der Logdatei stehen beide Meldungen, von A und B.
Dass das Singletonobjekt bei Beendigung des Programms auch brav zerstört wird, können wir leicht sicherstellen.
finalization FreeAndNil(Singleton);
Umgekehrt können wir es natürlich auch automatisch bei Programmstart erstellen. Dazu fügen wir eine neue Klassenmethode hinzu und ändern GetInstance etwas ab.
type
TSingleton = class(TObject)
private
FEventLog: TEventLog;
protected
constructor Create;
public
class function GetInstance: TSingleton;
class procedure CreateSingleton;
destructor Destroy;
property EventLog: TEventLog read FEventLog;
end;
class function TSingleton.GetInstance: TSingleton; begin Result := Singleton; end; class procedure CreateSingleton; begin if (Singleton = nil) then Singleton := TSingleton.Create; end;
Im Initialization-Teil wird dann noch die erzeugende Methode aufgerufen. Fertig.
initialization TSingleton.CreateSingleton;
Die Erzeugung des Objekts in der GetInstance-Methode nennt man Lazy Creation. Sie hat den Vorteil, dass das Objekt nur dann erzeugt wird, wenn es auch tatsächlich gebraucht wird. Allerdings kann es zu Problemen kommen, wenn zwei Threads eines Programm zum gleichen Zeitpunkt auf GetInstance zugreifen und im selben Moment die Initialiserung doppelt abläuft. Dieses Problem wird mit der Eager Creation (also dem grundsätzlichen Erzeugen beim Start) umgangen. Die Initialisierung läuft ab, wenn das Programm in den Hauptspeicher geladen wird. Später greifen dann Threads nur auf das bereits bestehende Objekt zu. Wenn die Initialiserung der Klasse allerdings sehr aufwändig ist und in vielen Fällen die Klasse gar nicht genutzt wird, verschleudert man damit wertvolle Resourcen. Die Entscheidung muss letzendlich der Entwickler treffen.
Damit haben wir ein funktionsfähiges Singleton erstellt, mit dem wir auch produktiv arbeiten können. Verbesserungen sind natürlich noch möglich. Man könnte die Logmethoden im TSingleton erstellen und an das TEventlog weiterreichen oder aber ein Singleton direkt von TEventLog ableiten.
Völlig korrekt, normalerweise sollte ein Konstruktor public sein. Der Compiler kann natürlich nicht wissen, dass hier das Singletonkonzept genutzt wird und das Verstecken des Konstruktors gewollt ist. Sicherheitshalber gibt es also diese Warnung. Wen das stört, der kann sie leicht unterdrücken:
protected
{$WARNINGS OFF}
constructor Create;
{$WARNINGS ON}
Einfach eine Nachricht über das Kontaktformular an mich senden.