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.
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 WikipediaGUID bei Wikipedia
Kommentare
Kommentar erstellen