Computer Singleton mit Objectpascal Hauptseite

Singleton mit Objectpascal

Einleitung - "...es kann nur einen geben!"

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.

Beispiel

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.

Erzeugen der Klasse verbieten

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;

Loggerfunktion einbauen

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.

"Der Letzte macht das Licht aus" - automatisches Free und Create

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.

Fragen

Beim Kompilieren wirft FreePascal eine Warnung "constructor should be public".

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}

Weitere Fragen oder Anmerkungen?

Einfach eine Nachricht über das Kontaktformular an mich senden.

Downloads

Letzte Änderung am 12.09.2008