Einführung in Interfaces
In diesem Artikel werden wir uns mit Interfaces beschäftigen, was sie sind, wofür man sie braucht und wie man sie einsetzt.
Warum Interfaces?
Interfaces sind eine Möglichkeit, den Funktionsumfang einer Klasse zu deklarieren. Ein Interface ist quasi der Bauplan einer Klasse.
Eine Klasse wird in Delphi normalerweise erstellt, indem die Klasse im Interface-Abschnitt einer Unit deklariert wird, während sie im Implementation-Abschnitt implementiert wird. Wie man sieht, sind die Abschnittsnamen gut gewählt. Interfaces werden genau wie Klassen im Interface-Abschnitt deklariert. Der Unterschied ist: es gibt keine Implementierung.
Wozu ist jetzt eine Deklaration ohne Implementierung gut?
Nehmen wir an, wir haben eine Klasse geschrieben, die wir einer anderen Klasse zur Verwendung übergeben. Diese andere Klasse interessiert sich nicht für die Implementierung unserer Klasse, sie muss nur wissen, welche Methoden und Eigenschaften die Klasse hat. Und genau das beschreibt ein Interface. In Delphi ist es leider nicht möglich, Deklaration und Implementierung zu trennen, beides muss immer in einer Unit stehen. Mit Interfaces lässt sich dieses System durchbrechen.
Was ist ein GUID?
Bevor wir uns mit Interfaces beschäftigen, benötigen wir etwas Hintergrundwissen. Da Interfaces nicht auf die Klassen von Delphi beschränkt sind, sondern von vielen Programmen und APIs verwendet werden, müssen sie eindeutige Kennungen haben: den GUID (Globally Unique Identifier). Jedes Interface muss eine solche Kennung besitzen. Wenn keine angegeben ist sucht sich das System selbst eine. Wozu wir diese benötigen werden wir später sehen.
Wie ist ein Interface aufgebaut?
Jedes Interface in Delphi stammt von IInterface ab und stellt mindestens drei Methoden bereit. Sehen wir uns die Definition von IInterface an:
type
IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
Die Methode QueryInterface benötigen wir, um festzustellen, welche Interfaces unterstützt werden. Die beiden anderen Methoden sind für die automatische Referenzzählung zuständig.
Referenzzählung
Interfaces verwalten sich selbst. Sobald ein Interface verwendet wird, wird _AddRef aufgerufen und der interne Referenzzähler erhöht. Wenn das Interface nicht mehr benötigt wird, wird _Release aufgerufen und der Referenzzähler vermindert. Sobald der Referenzzähler Null erreicht, wird das zum Interface gehörige Objekt automatisch zerstört. Wir werden später sehen, welche Vorteile und Probleme das mit sich bringt.
Ein Interface enthält nur Methoden und Eigenschaften, man kann nicht direkt auf Felder eines Objekts zugreifen. Deshalb müssen Eigenschaften mittels Getter und Setter implementiert werden. Es können weder Konstruktor noch Destruktor angegeben werden. Der Destruktor wird automatisch aufgerufen, ein Konstruktor muss immer über die Klasse aufgerufen werden.
Da ein Interface nur Methoden auflistet, die in einer Klasse implementiert sind und sowieso über das Interface zugänglich sind, haben die Sichtbarkeitsstufen public und private keine Bedeutung und sind überflüssig.
Ein paar Klassen
Um mit Interfaces arbeiten zu können, erstellen wir est mal drei Klassen: TFahrzeug, TMotorrad und TAuto.
Fangen wir mit der Grundklasse an:
type
TFahrzeug = class(TObject)
private
FMaxPassagiere: Integer;
FPassagiere: TStringList;
function GetMaxPassagiere: Integer;
function GetPassagier(Index: Integer): String;
function GetPassagiere: Integer;
function GetRaeder: Integer; virtual;
public
constructor Create;
destructor Destroy; override;
procedure Fahre;
procedure Einsteigen(const Name: String);
procedure Ausssteigen(Name: String);
property Passagiere: Integer read GetPassagiere;
property Passagier[Index: Integer]: String read GetPassagier;
property MaxPassagiere: Integer read GetMaxPassagiere;
property Raeder: Integer read GetRaeder;
end;
Unsere Klasse TFahrzeug stellt ein paar Methoden bereit, um einzusteigen, auszusteigen und zu fahren. Außerdem gibt es Eigenschaften, über die wir die Anzahl der Passagiere und ihre Namen erfahren, wie viele Passagiere maximal einsteigen dürfen und wie viele Räder das Fahrzeug hat.
type
TMotorrad = class(TFahrzeug)
private
function GetRaeder: Integer; override;
public
constructor Create;
end;
Die Klasse TMotorrad überschreibt nur den Getter für die Anzahl der Räder und besitzt einen neuen Konstruktor.
type
TAuto = class(TFahrzeug)
private
function GetRaeder: Integer; override;
public
constructor Create;
procedure Fahre_Rueckwaerts;
end;
Die Klasse TAuto implementiert eine neue Methode, Fahre_Rueckwaerts.
Diesen Klassen verpassen wir jetzt ein Interface.
Interfaces
Die Deklaration der Interfaces nehmen wir in einer neuen Unit vor. Zuerst deklarieren wir GUIDs für unsere Interfaces:
const
IID_Fahrzeug: TGUID = '{677854F0-525B-4372-9C7A-F87A00A8A117}';
IID_Motorrad: TGUID = '{75D4C150-D358-43DB-BE30-46D6423A587B}';
IID_Auto: TGUID = '{3E5503E6-70BC-480E-9473-47F1276AF5E6}';
Diese benötigen wir später. Um einen GUID zu erzeugen, kann man in Delphi einfach Strg+Umsch+G drücken.
Jetzt deklarieren wir unser erstes Interface:
type
IFahrzeug = interface(IInterface) // abgeleitet von IInterface
['{677854F0-525B-4372-9C7A-F87A00A8A117}'] // der GUID des Interface
function GetMaxPassagiere: Integer; // Methoden und Eigenschaften
function GetPassagier(Index: Integer): String;
function GetPassagiere: Integer;
function GetRaeder: Integer;
procedure Fahre;
procedure Einsteigen(const Name: String);
procedure Ausssteigen(Name: String);
property Passagiere: Integer read GetPassagiere;
property Passagier[Index: Integer]: String read GetPassagier;
property MaxPassagiere: Integer read GetMaxPassagiere;
property Raeder: Integer read GetRaeder;
end;
Im Gegensatz zu unserer Klasse sind bei unserem Interface alle Getter public. Das ist nötig, da wir uns bei den Eigenschaften auf die Getter beziehen.
Fahren wir fort mit unserem nächsten Interface:
type
IMotorrad = interface(IFahrzeug)
['{75D4C150-D358-43DB-BE30-46D6423A587B}']
end;
Unser IMotorrad besitzt alle Methoden, die wir in IFahrzeug deklariert haben, aber keine neuen Methoden (obwohl unser Objekt neue Versionen mancher Methoden implementiert). Deshalb ist IMotorrad bis auf den GUID leer.
Nun kommt unser letztes Interface:
type
IAuto = interface(IFahrzeug)
['{3E5503E6-70BC-480E-9473-47F1276AF5E6}']
procedure Fahre_Rueckwaerts;
end;
Dieses Interface besitzt also noch eine neue Methode, Fahre_Rueckwaerts.
Damit sind wir auch schon fertig mit der Deklaration unserer Interfaces.
Zusammenbauen
Damit wir die Interfaces nutzen können, müssen wir sie mit unseren Klassen verbinden.
Dazu fügen wir als Erstes unsere Interface-Unit zur Uses-Liste unserer Klassen-Unit hinzu.
Nun müssen wir den Kopf unserer Klassen etwas abändern:
type
TFahrzeug = class(TInterfacedObject, IFahrzeug);
TMotorrad = class(TFahrzeug, IMotorrad);
TAuto = class(TFahrzeug, IAuto);
Alle Klassen, die Interfaces zur Verfügung stellen, sollten von TInterfacedObject abgeleitet werden. In dieser Klasse werden nämlich die drei Methoden von IInterface implementiert. Würden wir von TObject ableiten, müssten wir diese Methoden selbst implementieren.
Indem wir den Namen unseres Interfaces einfach nach dem Vorfahrtyp einfügen, haben wir die Klasse und ihr Interface miteinander verbunden.
Wenn wir jetzt mit unseren Klassen arbeiten und Objekte erstellen, können wir mit diesen arbeiten wie bisher:
procedure AutoErstellen1;
var
Auto: TAuto;
begin
Auto := TAuto.Create;
Auto.Einsteigen('Hans');
Auto.Fahre;
Auto.Free;
end;
Wir können die Objekte aber auch über ihre Interfaces ansprechen:
procedure AutoErstellen2;
var
Auto: IAuto;
begin
Auto := TAuto.Create;
Auto.Einsteigen('Hans');
Auto.Fahre;
Auto := nil;
end;
Hier ist aber etwas komisch. Was soll die letzte Zeile? Haben wir hier nicht ein Speicherleck geschaffen? Nein, das haben wir nicht. Warum, das sehen wir jetzt.
Verwaltung
Wir haben gelernt, dass sich Interfaces selbst verwalten. Was heisst das jetzt in der Praxis? Sehen wir uns das zweite Beispiel noch einmal an und achten dabei auf den Referenzzähler des Interfaces:
procedure AutoErstellen2;
var
Auto: IAuto;
begin
Auto := TAuto.Create; // Neue Referenz, RefCount = 1
Auto.Einsteigen('Hans');
Auto.Fahre;
Auto := nil; // Referenz gelöscht, RefCount = 0 -> Objekt freigeben
end;
Jedesmal, wenn wir eine neue Referenz erstellen, wird automatisch _AddRef aufgerufen und der Referenzzähler erhöht. Beim Löschen einer Referenz wird _Release aufgerufen und der Refernzzähler vermindert. Da in unserem Beispiel eine Referenz erstellt und diese auch wieder gelöscht wird, ist am Ende der Referenzzähler Null und das Objekt wird automatisch zerstört. Sehen wir uns noch ein Beispiel an:
procedure AutoErstellen3;
var
Auto1, Auto2: IAuto;
begin
Auto1 := TAuto.Create; // Neue Referenz, RefCount = 1
Auto1.Einsteigen('Hans');
Auto2 := Auto1; // Neue Referenz, RefCount = 2
Auto2.Einsteigen('Peter');
Auto1.Fahre;
Auto1 := nil; // Referenz gelöscht, RefCount = 1
Auto2 := nil; // Referenz gelöscht, RefCount = 0 -> Objekt freigeben
end;
Erst, wenn wir alle Referenzen löschen, wird das Objekt freigegeben.
Um die Referenzzählung nicht durcheinanderzubringen, sollten Referenzen an Prozeduren nur mit const übergeben werden:
procedure Fahre_Auto(const Auto: IAuto);
begin
Auto.Einsteigen('Ich');
Auto.Fahre;
end;
So wird keine neue lokale Referenz angelegt und der Referenzzähler bleibt unverändert.
Objekte und Interfaces mischen
Es ist generell keine gute Idee, Objektreferenzen und Interfaces zu mischen. Der Grund ist der Referenzzähler. Sehen wir uns ein Beispiel an:
procedure AutoErstellen4;
var
Auto: TAuto;
AutoIntf: IAuto;
begin
Auto := TAuto.Create;
AutoIntf := Auto;
Auto.Einsteigen('Hans');
AutoIntf.Fahre;
AutoIntf := nil;
Auto.Free;
end;
Wenn wir diesen Code ausführen, bekommen wir eine Zugriffsverletzung. Sehen wir uns also den Referenzzähler an:
procedure AutoErstellen4;
var
Auto: TAuto;
AutoIntf: IAuto;
begin
Auto := TAuto.Create; // RefCount = 0
AutoIntf := Auto; // Neue Referenz, RefCount = 1
Auto.Einsteigen('Hans');
AutoIntf.Fahre;
AutoIntf := nil; // Referenz löschen, RefCount = 0 -> Objekt freigeben
Auto.Einsteigen('Peter'); // Zugriffsverletzung,
Auto.Free; // Objekt ist schon freigegeben
end;
Wie wir sehen, verändert die Zuweisung einer Objektreferenz nicht den Referenzzähler, nur die Zuweisung eines Interface.
Um unser Problem zu lösen, können wir _AddRef explizit aufrufen. Leider wird dann das Zerstören wieder schwieriger:
procedure AutoErstellen4a;
var
Auto: TAuto;
AutoIntf: IAuto;
begin
Auto := TAuto.Create; // RefCount = 0
AutoIntf := Auto; // Neue Referenz, RefCount = 1
AutoIntf._AddRef; // RefCount = 2
Auto.Einsteigen('Hans');
AutoIntf.Fahre;
AutoIntf := nil; // Referenz löschen, RefCount = 1
Auto.Einsteigen('Peter');
// Objekt löschen:
AutoIntf := Auto; // Neue Referenz, RefCount = 2
AutoIntf._Release; // RefCount = 1
AutoIntf := nil; // Referenz löschen, RefCount = 0 -> Objekt freigeben
end;
Besser ist es allerdings, eine solche Mischung von Anfang an zu vermeiden und nur das Interface zu benutzen.
Der Nutzen
Wann ist denn nun ein Interface nützlich und wofür brauche ich sowas? Nun, ein klassisches Beispiel sind PlugIns. Angenommen, mein Programm verwendet PlugIns, die mit meinen Objekten etwas anstellen. Jetzt könnte ich entweder meine komplette Klassendefinition in die PlugIns übernehmen oder mir ein paar Interfaces basteln und nur diese übernehmen. Die Benutzung von Interfaces hat zwei Vorteile:
- Meine PlugIns bleiben schön klein
- Ich kann meine Objekte einfach im Hauptprogramm ändern, solange ich die Interfaces nicht ändere.
Gerade der zweite Punkt heisst aber auch: Einmal definiert darf sich ein Interface nicht mehr ändern. Wenn sich die Definition ändert, so muss auch ein neues Interface geschaffen werden. Am Beispiel von DirectX lässt sich das gut demonstrieren. Hier gibt es etwa die interfaces IDirectDraw2 und IDirectDraw7, die beide ein Interface auf das gleiche Objekt definieren, einmal mit den Methoden von DirectX 2 und einmal mit den Methoden von DirectX 7. Auf diese Weise könnne auch Programme laufen, die noch nie etwas von DirectX 7 gehört haben.
Interfaces unterscheiden
Manchmal muss man einer Prozedur eine Basisklasse übergeben und erst in der Prozedur entscheiden, was man mit der Klasse vorhat. Ein Beispiel dafür ist die VCL-Ereignisprozedur:
TNotifyEvent = procedure(Sender: TObject) of object;
So etwas ist auch mit Interfaces möglich. Und hier kommen auch endlich die GUIDs ins Spiel. Diese werden nämlich benötigt, um Interfaces voneinander zu unterscheiden. Dafür gibt es in der Unit SysUtils die Funktion Supports.
Hier ein Beispiel: Nehmen wir an, in einer DLL steht folgene Prozedur:
procedure Zusteigen_und_rueckwaertsfahren(const Fahrzeug: IFahrzeug);
begin
Fahrzeug.Einsteigen('Hans');
if Supports(Fahrzeug, IID_Auto) then
(Fahrzeug as IAuto).Fahre_Rueckwaerts
else
ShowMessage('Ich kann nicht rückwärts fahren!');
end;
Diese DLL benötigt nur die Unit mit den Interfaces. Hier kann man schön sehen, wie man Interfaces unterscheiden und auf sie zugreifen kann. Solange ich ein IFahrzeug übergebe wird der Code immer funktionieren, sogar wenn ein abgeleitetes Interface zum Zeitpunkt der Programmierung noch gar nicht existiert hat.
Nehmen wr an, nach einiger Zeit gibt es ein neues Programm mit einer neuen Klasse TLastwagen:
type
TLastwagen = class(TAuto, ILastwagen);
Die DLL funktioniert weiterhin und kann jetzt sogar mit einem Lastwagen umgehen:
procedure Aufrufen;
var
Lastwagen: ILastwagen;
begin
Lastwagen := TLastwagen.Create;
Zusteigen_und_rueckwaertsfahren(Lastwagen as IFahrzeug);
Lastwagen := nil;
end;
Die Umwandlung in ein anderes Interface muss übrigens immer mit as erfolgen. Dabei wird immer eine neue Referenz angelegt und gegebenenfalls wieder gelöscht.
Deshalb funktioniert Folgendes nicht:
procedure Zusteigen_und_rueckwaertsfahrenA(const Fahrzeug: IFahrzeug);
begin
Fahrzeug.Einsteigen('Hans');
if Supports(Fahrzeug, IID_Auto) then
IAuto(Fahrzeug).Fahre_Rueckwaerts // Hier gibt's einen Fehler
else
ShowMessage('Ich kann nicht rückwärts fahren!');
end;
Gleichheit von Interfaces
Obwohl sich mehrere Interfaces auf das gleiche Objekt beziehen können, müssen sie nicht identisch sein. Folgender Code verdeutlicht das:
procedure Drei;
var
Intf1, Intf2: IAuto;
begin
Intf1 := TAuto.Create;
Intf2 := Intf1;
if (Intf1 = Intf2) then
ShowMessage('Diese Meldung wird vielleicht nicht erscheinen);
Intf2 := nil;
Intf1 := nil;
end;
Deshalb ist es unmöglich, festzustellen, ob zwei Interfaces das selbe Objekt beschreiben.
Um dieses Problem zu beheben, müsste jede erzeugte Klasse eine eindeutige ID erhalten, die über das Interface überprüft werden kann. Da verschiedene Interfaces das gleiche Objekt repräsentieren, wäre bei den Interfaces auch die ID gleich.
Kurz zusammengefasst
- Interfaces sind, vereinfacht gesagt, abstrakte Klassen ohne Implementierung
- Interfaces werden über einen GUID eindeutig identifiziert
- Interfaces beinhalten nur Methoden und Eigenschaften, die Sichtbarkeit ist immer public
- Interfaces verwalten sich selbst und zerstören sich automatisch, wenn sie nicht mehr benötigt werden
- Mit Interfaces ist es sehr einfach, Speicherlecks zu produzieren
- Interfaces und Objekte zu mischen ist keine gute Idee
- Interfaces vereinfachen die Erstellung von PlugIns
- Interfaces sollten nach der Deklaration nicht mehr geändert werden
- Niemals Interfaces auf Gleichheit überprüfen
Anhang
Beispielunits (1 KB)
Weblinks
Interfaces bei Wikipedia
GUID bei Wikipedia
Kommentare
Kommentar erstellen