Tutorial Interfaces
Warum dieses Tutorial?
Es ist mir aufgefallen das immer wieder Missversändnisse und Unklarheiten über den Nutzen und
Einsatz von Interfaces auftreten.
Deshalb habe ich mich Entschlossen das etwas aufzuhellen.
Dieses Tutorial beinhaltet natürlich meine Sicht darauf und ich lasse mich gerne verbessern.
Was ist ein Interface?
Ein Interface, übersetzt Schnittstelle, ist genau das was der Name uns mitteilt, es ist eine Schnittstelle zwischen verschiedenen
Programmteilen. Der Vorteil liegt in der Trennung von Definition und Implementation.
Ein CodeTeil der eine Schnittstelle benutzt muss nicht wissen wie diese Umgesetzt (Implementiert) ist.
Betrachten wir es wie eine USB Verbindung. Wir haben das Interface (den Stecker, die Buchse) und da können die verschiedensten
Geräte verbunden werden. Alles was jedes Ende wissen muss, ist wie über die Schnittstelle (Interface) kommuniziert wird.
Ob da nun ein Drucker, eine Kamera oder was auch immer daran hängt ist erst einmal unwichtig.
In diesem Tutorial gehe ich erst einmal auf eine recht einfache Anwendung von Interfaces ein.
Ich denke jeder kennt das Problem: Programmeinstellungen, Anwendereinstellunge etc. müssen zwischengespeichert werden, damit der
Anwender seine Einstellungen etc. bei jedem Programmstart wieder findet.
Die Lösung die fast jeder da gerne zumindest am Anfang verwendet sind Inifiles. Einfach zu benutzen und effektiv.
Irgendwann dann sollen die Einstellungen in eine Datenbank oder ähnliches geschrieben werden.
Es steht ein grösserer Umbau an.
Die Lösung? Ja genau Interfaces!
Das TCustomIniFile hat eigentlich schon alles was wir brauchen. Wir könnten einfach ein neue Klasse ableiten und diese benutzen.
Einfacher wird es aber wenn wir daraus den benötigten Teil in ein Interface auslagern und in unseren CodeTeilen dieses benutzen.
Das sieht dann z.B so aus:
Delphi-Quellcode:
iConfigReadWriter = interface
['{AE9CC6E5-F0B8-4D39-8F6C-799423C60A37}']
function SectionExists(const Section: string): Boolean;
function ReadString(const Section, Ident, Default: string): string;
procedure WriteString(const Section, Ident, Value: string);
function ReadInteger(const Section, Ident: string; Default: Longint): Longint;
procedure WriteInteger(const Section, Ident: string; Value: Longint);
function ReadBool(const Section, Ident: string; Default: Boolean): Boolean;
procedure WriteBool(const Section, Ident: string; Value: Boolean);
function ReadBinaryStream(const Section, Name: string; Value: TStream): Integer;
function ReadDate(const Section, Name: string; Default: TDateTime): TDateTime;
function ReadDateTime(const Section, Name: string; Default: TDateTime): TDateTime;
function ReadFloat(const Section, Name: string; Default: Double): Double;
function ReadTime(const Section, Name: string; Default: TDateTime): TDateTime;
procedure WriteBinaryStream(const Section, Name: string; Value: TStream);
procedure WriteDate(const Section, Name: string; Value: TDateTime);
procedure WriteDateTime(const Section, Name: string; Value: TDateTime);
procedure WriteFloat(const Section, Name: string; Value: Double);
procedure WriteTime(const Section, Name: string; Value: TDateTime);
procedure ReadSection(const Section: string; Strings: TStrings);
procedure ReadSections(Strings: TStrings); overload;
procedure ReadSections(const Section: string; Strings: TStrings); overload;
procedure ReadSubSections(const Section: string; Strings: TStrings; Recurse: Boolean = False);
procedure ReadSectionValues(const Section: string; Strings: TStrings);
procedure EraseSection(const Section: string);
procedure DeleteKey(const Section, Ident: string);
function ValueExists(const Section, Ident: string): Boolean;
end;
Anstatt einer Methode unserer Klassen mit der Signatur
procedure SaveConfig(Value : TcustomIniFile);
können wir die Signatur ändern zu
procedure SaveConfig(Value : iConfigReadWriter);
für unseren restlichen Code ändert sich nichts da die Signaturen der Methoden gleich sind.
Value.WriteString('DATA','DUMP','Meine Daten');
Dies erfordert natürlich das wir die Erzeugung und Freigebe unserer "Schnittstelle" vom eigentlichen Code trennen.
Also anstatt einer methode:
Delphi-Quellcode:
procedure tuseini.BadSaveData;
var
lini: TCustomIniFile;
begin
lini := TIniFile.Create('Was auch immer');
try
lini.WriteString('Data', 'Dump', 'meine Daten');
// etc
finally
lini.free;
end;
end;
haben wir zumindest:
Delphi-Quellcode:
procedure tuseini.BetterSaveSettings(Writer: TCustomIniFile);
begin
Writer.WriteString('Data', 'Dump', 'meine Daten');
end;
procedure tuseini.BetterSaveData;
var
lini: TCustomIniFile;
begin
lini := TIniFile.Create('Was auch immer');
try
BetterSaveSettings(lini);
finally
lini.free;
end;
end;
Aus meiner Sicht ist dies die optimale Lösung:
Delphi-Quellcode:
procedure tuseini.SaveSettings(Writer: iConfigReadWriter);
begin
Writer.WriteString('Test', 'Dummy', 'Default');
end;
procedure tuseini.ReadSettings(Reader: iConfigReadWriter);
begin
fmySetting := Reader.ReadString('Test', 'Dummy', 'Empty');
end;
Unser Klasse muss nur das Interface kennen, was dahinter wirklich passiert ist nicht wichtig für die Benutzung.
Im Testprojekt ist jetzt auch ein Datamodul dass dieses Interface implementiert.
Es sind nicht alle Methoden befüllt sollte aber den zeigen wie man so einfach die Implementation ändern kann, ohne in der
benutzenden Klasse etwas zu ändern.
Delphi-Quellcode:
TDataModule1 = class(TDataModule, iConfigReadWriter)
FDConnection1: TFDConnection;
FDGUIxWaitCursor1: TFDGUIxWaitCursor;
FDPhysSQLiteDriverLink1: TFDPhysSQLiteDriverLink;
procedure DataModuleDestroy(Sender: TObject);
procedure DataModuleCreate(Sender: TObject);
private
procedure DeleteKey(const Section, Ident: string);
procedure EraseSection(const Section: string);
function ReadBinaryStream(const Section, Name: string; Value: TStream): Integer;
function ReadBool(const Section, Ident: string; Default: Boolean): Boolean;
function ReadDate(const Section, Name: string; Default: TDateTime): TDateTime;
function ReadDateTime(const Section, Name: string; Default: TDateTime): TDateTime;
function ReadFloat(const Section, Name: string; Default: Double): Double;
function ReadInteger(const Section, Ident: string; Default: Longint): Longint;
procedure ReadSection(const Section: string; Strings: TStrings);
procedure ReadSections(const Section: string; Strings: TStrings); overload;
procedure ReadSections(Strings: TStrings); overload;
procedure ReadSectionValues(const Section: string; Strings: TStrings);
function ReadString(const Section, Ident, Default: string): string;
procedure ReadSubSections(const Section: string; Strings: TStrings; Recurse: Boolean = False);
function ReadTime(const Section, Name: string; Default: TDateTime): TDateTime;
function SectionExists(const Section: string): Boolean;
function ValueExists(const Section, Ident: string): Boolean;
procedure WriteBinaryStream(const Section, Name: string; Value: TStream);
procedure WriteBool(const Section, Ident: string; Value: Boolean);
procedure WriteDate(const Section, Name: string; Value: TDateTime);
procedure WriteDateTime(const Section, Name: string; Value: TDateTime);
procedure WriteFloat(const Section, Name: string; Value: Double);
procedure WriteInteger(const Section, Ident: string; Value: Longint);
procedure WriteString(const Section, Ident, Value: string);
procedure WriteTime(const Section, Name: string; Value: TDateTime);
{ Private-Deklarationen }
public
{ Public-Deklarationen }
end;
Dies habe ich im testProjekt über eine simple Factory gelöst.
Im Anhang findet Ihr ein Projekt das dies Umsetzt.
Bei Interesse kann ich auch noch weiter in die Interface problematik einsteigen, Supports, Delegeation etc..
Habe das Tutorial.zip erweitert mit einer
DB Lösung