AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Tutorial Interfaces

Tutorial Interfaces

Ein Tutorial von Fritzew · begonnen am 12. Apr 2017 · letzter Beitrag vom 11. Okt 2017
Antwort Antwort
Seite 1 von 5  1 23     Letzte » 
Fritzew
Registriert seit: 18. Nov 2015
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
Angehängte Dateien
Dateityp: zip TutInterface.zip (7,8 KB, 114x aufgerufen)

Geändert von Fritzew (12. Apr 2017 um 13:19 Uhr)
 
a.def
 
#2
  Alt 12. Apr 2017, 11:30
Ich hoffe meine Frage ist eine berechtigte Frage:
worin liegt der Vorteil gegenüber normalen IniFiles in der fertigen ausführbaren Datei?
  Mit Zitat antworten Zitat
Delphi-Laie

 
Delphi 10.1 Berlin Starter
 
#3
  Alt 12. Apr 2017, 11:38
"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."

Nun, diese Unterscheidung zwischen Deklaration und Implementation wird auch im Unit-Konzept realisiert.
Sogar die übergebenen Variablen bei Unterprogrammen erfüllen dieses Schnittstellenkonzept.

Mithin ist mir der Vorteil der Interfaces leider nicht klargeworden.
  Mit Zitat antworten Zitat
Benutzerbild von DeddyH
DeddyH

 
Delphi 12 Athens
 
#4
  Alt 12. Apr 2017, 11:58
Man muss sich einfach darüber klar sein, dass ein Interface ganz bewusst nichts Konkretes ist, sondern lediglich eine Zusicherung. Das bedeutet, der Benutzer (im Sinne einer Klasse) des Interfaces muss die konkrete Implementation überhaupt nicht kennen, kann sich aber trotzdem sicher sein, dass alle im Interface deklarierten Properties und Methoden vorhanden sind. Dadurch wird eine einfache Erweiterbarkeit erreicht. Ein kleines Beispiel: nehmen wir an, wir haben die Klassen TDings und TConsumer. Letztere ruft eine Methode Blubb der Ersteren auf.
Delphi-Quellcode:
type
  TDings = class
  public
    procedure Blubb;
  end;

  TConsumer = class
  public
    procedure Wuppdi(const Dings: TDings);
  end;

...

procedure TConsumer.Wuppdi(const Dings: TDings);
begin
  Dings.Blubb;
end;
Alles cool, alles easy. Nun wollen wir das Programm erweitern und schreiben eine Klasse TBums, die ebenfalls eine Methode Blubb enthält. Die Consumer-Klasse soll sowohl mit TDings als auch mit TBums umgehen können. Was können wir tun? Mir fallen da ein paar Möglichkeiten ein:
- Überladen der Wuppdi-Methode
- Ableiten der TDings- und TBums-Klasse von einer gemeinsamen Elternklasse, die Blubb als (ggf. virtuelle oder dynamische) Methode einführt. Das macht dann eine Änderung des Parametertyps in den Typ der Elternklasse notwendig.
- Ändern des Parametertyps in TObject und Abfrage in der Methode, ob es sich um TDings oder TBums handelt, dann Typecast und Aufruf.

Das ist alles nicht so wirklich elegant und macht Änderungen an verschiedenen Stellen nötig. Hier kommen jetzt die Interfaces ins Spiel: man ändert einmalig den Parametertyp in einen Interfacetyp, in dem die Blubb-Methode vereinbart wird. TDings und TBums implementieren jetzt dieses Interface, TConsumer greift dann darüber auf die Methode zu. Soll später eine weitere Klasse TSchiessMichTot dazukommen, lässt man sie ebenfalls das Interface implementieren, schon kann TConsumer ohne jede Codeänderung auch damit umgehen.

Delphi-Quellcode:
type
  ISuperIntf = interface
    ['{01107754-046D-4A32-AC6F-D96BC2AECCE0}']
    procedure Blubb;
  end;

  TDings = class(TInterfacedObject, ISuperIntf)
  public
    procedure Blubb;
  end;

  TBums = class(TInterfacedObject, ISuperIntf)
  public
    procedure Blubb;
  end;

...

procedure TConsumer.Wuppdi(const SuperIntf: ISuperIntf);
begin
  SuperIntf.Blubb;
end;
Detlef
  Mit Zitat antworten Zitat
Fritzew

 
Delphi 11 Alexandria
 
#5
  Alt 12. Apr 2017, 13:14
Hallo zusammen, ich habe das Beispiel noch mit einer DB Lösung erweitert. Vielleicht wird es dann besser zu verstehen
Fritz Westermann
  Mit Zitat antworten Zitat
Bbommel

 
Delphi 12 Athens
 
#6
  Alt 12. Apr 2017, 13:39
Das ist alles nicht so wirklich elegant und macht Änderungen an verschiedenen Stellen nötig. Hier kommen jetzt die Interfaces ins Spiel: man ändert einmalig den Parametertyp in einen Interfacetyp, in dem die Blubb-Methode vereinbart wird. TDings und TBums implementieren jetzt dieses Interface, TConsumer greift dann darüber auf die Methode zu. Soll später eine weitere Klasse TSchiessMichTot dazukommen, lässt man sie ebenfalls das Interface implementieren, schon kann TConsumer ohne jede Codeänderung auch damit umgehen.
[/DELPHI]
Ich wurde trotz vieler Diskussionen bisher noch nicht von der "interface-Fraktion" überzeugt. Gerade dein Beispiel zeigt wunderbar, finde ich, dass man das gleiche Abstraktionslevel auch mit Klassen mit virtuellen und ggf. abstrakten Methoden erreichen kann. Du schreibst, dann müsse man in deinem Beispiel aber den Parametertyp von wuppdi ändern. Das muss man bei der Einführung des interface aber auch. Dafür fängt man sich bei den interfaces aber zunächst mal ein weiteres Sprachkonstrukt ein (was ja gewollt sein kann, wegen Referenzzählung, muss man dann aber auch wissen).

Ich finde interfaces immer dann so richtig überzeugend, wenn es wirklich um Schnittstellen geht (DLL, SOAP, ActiveX, ...)...

Aber das sind nur meine zwei Cent. Ich konnte bisher alles - abgesehen von der Referenzzählung - auch mit abstrakten/virtuellen Methoden erreichen kann.
  Mit Zitat antworten Zitat
Benutzerbild von DeddyH
DeddyH

 
Delphi 12 Athens
 
#7
  Alt 12. Apr 2017, 14:32
Wobei Du bei "reinen" Klassen aber immer gezwungen bist, eine festgelegte Hierarchie einzuhalten, es sei denn, Du prüfst mit "is-Orgien" jeden geeigneten Typ ab. Und wieso meinst Du, dass man einen Interface-Parameter jemals wieder ändern muss? Genau dafür ist er ja da, dass das eben nicht nötig ist.
Detlef
  Mit Zitat antworten Zitat
Bbommel

 
Delphi 12 Athens
 
#8
  Alt 12. Apr 2017, 14:44
Das ist mir jetzt noch nicht ganz klar, was du meinst. Um mal bei deinem Beispiel zu bleiben - mit einer abstrakten Klasse sieht das ja so aus:

Delphi-Quellcode:
type
  TSuperClass = class
    procedure Blubb; virtual; abstract;
  end;

  TDings = class(TSuperClass)
  public
    procedure Blubb; override;
  end;

  TBums = class(TSuperClass)
  public
    procedure Blubb; override;
  end;

...

procedure TConsumer.Wuppdi(const SuperClass: TSuperClass);
begin
  SuperClass.Blubb;
end;
Ich meine nicht, dass man den interface-Parameter jemals wieder ändern muss - ich hatte mich nur auf deine Aussage bezogen, dass man den Parameter halt einmal ändern muss, wenn man das interface neu einführt. Genauso ist es ja hier mit dem Parameter für die abstrakte Klasse: einmal muss man ihn ändern, aber danach nicht mehr.

Und was meinst du hier mit "Hierarchie einhalten"? Ich muss von der abstrakten Basisklasse ableiten - genauso wie du das interface implementieren musst. Das gibt sich doch eigentlich nichts.
  Mit Zitat antworten Zitat
Benutzerbild von DeddyH
DeddyH

 
Delphi 12 Athens
 
#9
  Alt 12. Apr 2017, 14:49
Kannst Du immer und überall sicherstellen, dass die übergebenen Instanzen von einer bestimmten Klasse abgeleitet wurden? In meinem Minimalbeispiel ist das natürlich kein Problem, aber was ist z.B. mit Ableitungen bestehender VCL-Controls? Da bliebe nur die Lösung mit den zig "is"-Abfragen, wenn man auf Interfaces verzichten will. Wenn nicht, lässt man die Ableitung einfach das Interface implementieren, fertig.
Detlef
  Mit Zitat antworten Zitat
ISurf
 
#10
  Alt 12. Apr 2017, 14:59
Und was meinst du hier mit "Hierarchie einhalten"? Ich muss von der abstrakten Basisklasse ableiten - genauso wie du das interface implementieren musst. Das gibt sich doch eigentlich nichts.
Genau da wird es interessant, sobald man nämlich bei einem komplexeren Projekt an den Punkt kommt, wo eine Klasse mehrere Interfaces implementieren muss, wäre eine Basisklasse nicht mehr ausreichend. Statt Mehrfachvererbung muss man dann eben Interfaces verwenden.

Interfaces sind in der Hinsicht flexibler, weil sie von beliebigen Klassen implementiert werden können und man dann z.B. bei einem Funktionsaufruf nur das entsprechende Interface als Parameter übergibt, dadurch kann man so einer Funktion auch beliebige Objekte übergeben, die keine gemeinsame Basisklasse brauchen.
  Mit Zitat antworten Zitat
Themen-Optionen Tutorial durchsuchen
Tutorial durchsuchen:

Erweiterte Suche
Ansicht

Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 18:37 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz