Was bedeutet es, auf eine Schnittstelle zu programmieren?

Ich höre immer wieder die Aussage auf den meisten programmbezogenen Seiten:

Programmiere auf eine Schnittstelle und nicht auf eine Implementierung

Aber ich verstehe die Implikationen nicht?
Beispiele würden helfen.

EDIT: Ich habe viele gute Antworten erhalten, auch wenn Sie es mit ein paar Codeschnipsel für ein besseres Verständnis des Themas ergänzen würden. Vielen Dank!

Sie suchen wahrscheinlich so etwas:

public static void main(String... args) { // do this - declare the variable to be of type Set, which is an interface Set buddies = new HashSet(); // don't do this - you declare the variable to have a fixed type HashSet buddies2 = new HashSet(); } 

Warum wird es als gut erachtet, es auf die erste Art zu tun? Nehmen wir an, Sie entscheiden später, dass Sie eine andere Datenstruktur verwenden müssen, z. B. ein LinkedHashSet, um die functionalität des LinkedHashSet zu nutzen. Der Code muss wie folgt geändert werden:

 public static void main(String... args) { // do this - declare the variable to be of type Set, which is an interface Set buddies = new LinkedHashSet(); // < - change the constructor call // don't do this - you declare the variable to have a fixed type // this you have to change both the variable type and the constructor call // HashSet buddies2 = new HashSet(); // old version LinkedHashSet buddies2 = new LinkedHashSet(); } 

Das scheint nicht so schlimm zu sein, oder? Aber was ist, wenn Sie Getter auf die gleiche Weise geschrieben haben?

 public HashSet getBuddies() { return buddies; } 

Dies müsste auch geändert werden!

 public LinkedHashSet getBuddies() { return buddies; } 

Hoffentlich sehen Sie, selbst mit einem kleinen Programm wie diesem haben Sie weit reichende Auswirkungen auf das, was Sie als den Typ der Variablen deklarieren. Mit Objekten, die so weit hin und her gehen, hilft es definitiv, das Programm leichter zu programmieren und zu pflegen, wenn man sich darauf verlässt, dass eine Variable als Schnittstelle deklariert wird und nicht als spezifische Implementierung dieser Schnittstelle (in diesem Fall als a deklarieren) Set, kein LinkedHashSet oder was auch immer). Es kann genau das sein:

 public Set getBuddies() { return buddies; } 

Es gibt noch einen anderen Vorteil darin, dass (zumindest für mich) der Unterschied mir hilft, ein Programm besser zu gestalten. Aber hoffentlich geben meine Beispiele eine Idee ... hoffe es hilft.

Eines Tages wurde ein Junior-Programmierer von seinem Chef angewiesen, eine Anwendung zu schreiben, um Geschäftsdaten zu analysieren und alles in hübschen Berichten mit Metriken, Grafiken und all dem Zeug zu verdichten. Der Chef gab ihm eine XML-Datei mit der Bemerkung “Hier sind einige Beispielgeschäftsdaten”.

Der Programmierer begann zu programmieren. Ein paar Wochen später hatte er das Gefühl, dass die Metriken und Graphen und Zeug genug waren, um den Chef zufrieden zu stellen, und er präsentierte seine Arbeit. “Das ist großartig”, sagte der Chef, “aber kann es auch Geschäftsdaten aus dieser SQL-database zeigen, die wir haben?”.

Der Programmierer ging zurück zum Codieren. Es gab Code für das Lesen von Geschäftsdaten aus XML, die in seiner gesamten Anwendung verstreut waren. Er schrieb alle diese Snippets um und verpackte sie mit einer “if” Bedingung:

 if (dataType == "XML") { ... read a piece of XML data ... } else { .. query something from the SQL database ... } 

Als der Chef mit der neuen Iteration der Software konfrontiert wurde, antwortete der Chef: “Das ist großartig, aber kann er auch Geschäftsdaten von diesem Web-Service berichten?” Er erinnerte sich an all die langweiligen if-statementen, die er WIEDER schreiben musste, und der Programmierer wurde wütend. “Erst XML, dann SQL, jetzt Webdienste! Was ist die ECHTE Quelle für Geschäftsdaten?”

Der Chef antwortete: “Alles, was es bieten kann”

In diesem Moment war der Programmierer erleuchtet .

Mein erstes Lesen dieser Aussage ist sehr anders als jede Antwort, die ich bisher gelesen habe. Ich stimme all den Leuten zu, die sagen, dass die Verwendung von Schnittstellentypen für Ihre Methode, Parameter usw. sehr wichtig sind, aber das ist es nicht, was diese Aussage für mich bedeutet.

Meine Annahme ist, dass es Ihnen sagt, Code zu schreiben, der nur davon abhängt, was die Schnittstelle (in diesem Fall verwende ich “Schnittstelle” offengelegte Methoden entweder einer class oder eines Schnittstellentyps), die Sie verwenden, sagt es in der Dokumentation. Dies ist das Gegenteil des Schreibens von Code, der von den Implementierungsdetails der functionen abhängt, die Sie aufrufen. Sie sollten alle functionsaufrufe als schwarze Felder behandeln (Sie können Ausnahmen davon machen, wenn beide functionen Methoden der gleichen class sind, aber im Idealfall immer beibehalten werden).

Beispiel: Angenommen, es gibt eine Screen class mit den Methoden Draw(image) und Clear() . Die Dokumentation sagt etwas wie “die Zeichenmethode zeichnet das angegebene Bild auf dem Bildschirm” und “die klare Methode löscht den Bildschirm”. Wenn Sie Bilder nacheinander anzeigen möchten, müssen Sie Clear() gefolgt von Draw() wiederholt aufrufen. Das würde für die Schnittstelle kodieren. Wenn Sie für die Implementierung codieren, können Sie beispielsweise nur die Draw() -Methode aufrufen, da Sie von der Implementierung von Draw() wissen, dass sie intern Clear() aufruft, bevor Sie eine Zeichnung erstellen. Das ist schlecht, weil Sie jetzt auf Implementierungsdetails angewiesen sind, die Sie beim Betrachten der exponierten Schnittstelle nicht kennen.

Ich freue mich darauf, zu sehen, ob jemand diese Interpretation des Satzes in der OP-Frage teilt, oder ob ich völlig außerhalb der Basis bin …

Es ist eine Möglichkeit, Verantwortlichkeiten / Abhängigkeiten zwischen Modulen zu trennen. Durch das Definieren einer bestimmten Schnittstelle (einer API) stellen Sie sicher, dass sich die Module auf beiden Seiten der Schnittstelle nicht gegenseitig stören.

Angenommen, Modul 1 kümmert sich um die Anzeige von Bankkontodaten für einen bestimmten Benutzer, und module2 holt Bankkontodaten aus dem “beliebigen” Back-End.

Durch die Definition einiger Typen und functionen mit den zugehörigen Parametern, zB einer Struktur, die eine Banktransaktion definiert, und einiger Methoden (functionen) wie GetLastTransactions (AccountNumber, NbTransactionsWanted, ArrayToReturnTheseRec) und GetBalance (AccountNumer) kann das Modul1 arbeiten um die benötigten Informationen zu erhalten und sich nicht darum zu kümmern, wie diese Informationen gespeichert oder berechnet werden oder was auch immer. Umgekehrt wird das Module2 nur auf den Methodenaufruf reagieren, indem es die Informationen gemäß der definierten Schnittstelle bereitstellt, aber sich nicht darum kümmern, wo diese Informationen angezeigt, gedruckt oder was auch immer …

Wenn ein Modul geändert wird, kann die Implementierung der Schnittstelle variieren, aber solange die Schnittstelle gleich bleibt, müssen die Module, die die API verwenden, im schlimmsten Fall neu kompiliert / neu erstellt werden, aber ihre Logik muss nicht geändert werden sowieso.

Das ist die Idee einer API.

Eine Schnittstelle definiert die Methoden, auf die ein Objekt reagieren soll.

Wenn Sie an der Schnittstelle codieren, können Sie das zugrunde liegende Objekt ändern und Ihr Code wird weiterhin funktionieren (weil Ihr Code unabhängig von der WHO ist, die den Auftrag ausführt oder WIE der Auftrag ausgeführt wird). Auf diese Weise erhalten Sie Flexibilität.

Wenn Sie beim Codieren einer bestimmten Implementierung das zugrunde liegende Objekt ändern müssen, wird der Code höchstwahrscheinlich beschädigt , da das neue Objekt möglicherweise nicht auf die gleichen Methoden reactjs.

Um ein klares Beispiel zu geben:

Wenn Sie mehrere Objekte halten müssen, haben Sie sich möglicherweise für einen Vektor entschieden .

Wenn Sie auf das erste Objekt des Vektors zugreifen möchten, könnten Sie schreiben:

  Vector items = new Vector(); // fill it Object first = items.firstElement(); 

So weit, ist es gut.

Später haben Sie entschieden, dass Sie aus “irgendeinem” Grund die Implementierung ändern müssen (nehmen wir an, der Vector erzeugt einen Engpass aufgrund exzessiver Synchronisation)

Sie erkennen, dass Sie eine ArrayList instad verwenden müssen.

Nun, du Code wird brechen …

 ArrayList items = new ArrayList(); // fill it Object first = items.firstElement(); // compile time error. 

Du kannst nicht. Diese Zeile und alle Zeilen, die die Methode firstElement () verwenden, würden brechen.

Wenn Sie ein bestimmtes Verhalten benötigen und Sie diese Methode unbedingt benötigen, ist es vielleicht in Ordnung (obwohl Sie die Implementierung nicht ändern können). Wenn Sie jedoch nur das erste Element abrufen müssen (dh, es gibt nichts Spezielles) Wenn der Vektor anders als die Implementierung die Methode firstElement () hat, dann würde die Flexibilität der Änderung durch die Verwendung der Schnittstelle und nicht der Implementierung möglich sein.

  List items = new Vector(); // fill it Object first = items.get( 0 ); // 

In diesem Formular codieren Sie nicht die get-Methode von Vector , sondern die get-Methode von List .

Es spielt keine Rolle, wie das zugrunde liegende Objekt die Methode ausführt, solange es auf den Vertrag von “Holen Sie das 0. Element der Sammlung” reactjs.

Auf diese Weise können Sie später zu jeder anderen Implementierung wechseln:

  List items = new ArrayList(); // Or LinkedList or any other who implements List // fill it Object first = items.get( 0 ); // Doesn't break 

Dieses Beispiel mag naiv aussehen, ist aber die Basis, auf der die OO-Technologie basiert (sogar auf Sprachen, die nicht statisch typisiert sind wie Python, Ruby, Smalltalk, Objective-C usw.)

Ein komplexeres Beispiel ist die functionsweise von JDBC . Sie können den Treiber ändern, aber die meisten Anrufe funktionieren auf die gleiche Weise. Zum Beispiel könnten Sie den Standard-Treiber für Oracle-databaseen verwenden oder Sie könnten einen ausgereifteren wie den von Weblogic oder Webpshere bereitgestellten verwenden. Natürlich ist es nicht magisch, dass du dein Produkt noch testen musst, aber zumindest hast du keine Sachen wie:

  statement.executeOracle9iSomething(); 

vs

 statement.executeOracle11gSomething(); 

Ähnliches passiert mit Java Swing.

Zusätzlicher Messwert:

Designprinzipien aus Designmustern

Effektives Java-Objekt: Auf Objekte über ihre Schnittstellen verweisen

(Dieses Buch zu kaufen ist eines der besten Dinge, die man im Leben tun kann – und natürlich lesen -)

Im core geht es in dieser Aussage um Abhängigkeiten. Wenn ich meine class Foo zu einer Implementierung ( Bar statt IBar ) IBar dann ist Foo jetzt abhängig von Bar . Aber wenn ich meine class Foo zu einer Schnittstelle ( IBar statt Bar ) IBar dann kann die Implementierung variieren und Foo ist nicht mehr von einer bestimmten Implementierung abhängig. Dieser Ansatz liefert eine flexible, lose gekoppelte Codebasis, die einfacher wiederverwendet, neu strukturiert und in Einzelteilen getestet werden kann.

Eine Schnittstelle ist wie ein Vertrag zwischen Ihnen und der Person, die die Schnittstelle hergestellt hat, die Ihr Code ausführen wird, was sie anfordern. Außerdem möchten Sie Dinge so codieren, dass Ihre Lösung das Problem um ein Vielfaches lösen kann. Denken Sie daran, Code erneut zu verwenden. Wenn Sie eine Implementierung codieren, denken Sie nur an die Instanz eines Problems, das Sie zu lösen versuchen. Unter diesem Einfluss werden Ihre Lösungen weniger allgemein und fokussierter sein. Das macht das Schreiben zu einer allgemeinen Lösung, die sich an einer Schnittstelle wesentlich schwieriger hält.

Nimm einen roten 2×4 Lego Block und befestige ihn an einem blauen 2×4 Lego Block, so dass einer auf dem anderen sitzt. Entferne nun den blauen Block und ersetze ihn durch einen gelben 2×4 Lego Block. Beachten Sie, dass der rote Block nicht geändert werden musste, obwohl die “Implementierung” des angehängten Blocks variierte.

Jetzt geh und hol dir eine andere Art von Block, der nicht die Lego “Schnittstelle” teilt. Versuche es mit dem roten 2×4 Lego zu verbinden. Um dies zu erreichen, müssen Sie entweder den Lego oder den anderen Block wechseln, indem Sie etwas Plastik wegschneiden oder neues Plastik oder Kleber hinzufügen. Beachten Sie, dass Sie durch die Variation der “Implementierung” gezwungen sind, sie oder den Client zu ändern.

In der Lage zu sein, Implementierungen variieren zu lassen, ohne den Client oder den Server zu ändern – das bedeutet, dass es auf Schnittstellen programmiert werden muss.

Schau, ich wusste nicht, dass dies für Java war und mein Code basiert auf C #, aber ich glaube, dass es den Punkt liefert.

Jedes Auto hat Türen.

Aber nicht jede Tür verhält sich gleich, wie in Großbritannien sind die Taxi-Türen rückwärts. Eine universelle Tatsache ist, dass sie “Öffnen” und “Schließen”.

 interface IDoor { void Open(); void Close(); } class BackwardDoor : IDoor { public void Open() { // code to make the door open the "wrong way". } public void Close() { // code to make the door close properly. } } class RegularDoor : IDoor { public void Open() { // code to make the door open the "proper way" } public void Close() { // code to make the door close properly. } } class RedUkTaxiDoor : BackwardDoor { public Color Color { get { return Color.Red; } } } 

Wenn Sie ein Autotürreparateur sind, ist es Ihnen egal, wie die Tür aussieht, oder ob sie sich in die eine oder andere Richtung öffnet. Ihre einzige Anforderung ist, dass die Tür wie eine Tür funktioniert, wie IDoor.

 class DoorRepairer { public void Repair(IDoor door) { door.Open(); // Do stuff inside the car. door.Close(); } } 

Der Reparateur kann mit RedUkTaxiDoor, RegularDoor und BackwardDoor umgehen. Und jede andere Art von Türen, wie LKW-Türen, Limousinentüren.

 DoorRepairer repairer = new DoorRepairer(); repairer.Repair( new RegularDoor() ); repairer.Repair( new BackwardDoor() ); repairer.Repair( new RedUkTaxiDoor() ); 

Übernehmen Sie dies für Listen, Sie haben LinkedList, Stack, Warteschlange, die normale Liste, und wenn Sie Ihre eigene, MyList. Sie implementieren alle die IList-Schnittstelle, die sie zum Implementieren von Hinzufügen und Entfernen benötigt. Also, wenn Ihre class Elemente in einer bestimmten Liste hinzufügen oder entfernen …

 class ListAdder { public void PopulateWithSomething(IList list) { list.Add("one"); list.Add("two"); } } Stack stack = new Stack(); Queue queue = new Queue(); ListAdder la = new ListAdder() la.PopulateWithSomething(stack); la.PopulateWithSomething(queue); 

Allen Holub hat 2003 einen großartigen Artikel für JavaWorld zu diesem Thema mit dem Titel Why extensions is evil geschrieben . Sein Beispiel für die statement “Programm an die Schnittstelle” ist, wie Sie aus seinem Titel entnehmen können, dass Sie Schnittstellen gerne implementieren, aber sehr selten das Schlüsselwort extends zur Unterklasse verwenden. Er verweist unter anderem auf das sogenannte zerbrechliche Problem der Basisklasse . Aus Wikipedia:

ein grundlegendes Architekturproblem von objektorientierten Programmiersystemen, wo Basisklassen (Oberklassen) als “fragil” gelten, da scheinbar sichere Modifikationen an einer Basisklasse, wenn sie von den abgeleiteten classn geerbt werden, zu Fehlfunktionen der abgeleiteten classn führen können. Der Programmierer kann nicht einfach feststellen, ob eine Änderung der Basisklasse sicher ist, indem er isoliert die Methoden der Basisklasse untersucht.

Zusätzlich zu den anderen Antworten füge ich noch mehr hinzu:

Sie programmieren zu einer Schnittstelle, weil es einfacher zu handhaben ist. Die Schnittstelle kapselt das Verhalten der zugrunde liegenden class. Auf diese Weise ist die class eine Blackbox. Ihr ganzes wirkliches Leben ist das Programmieren zu einer Schnittstelle. Wenn Sie ein Fernsehgerät, ein Auto oder eine Stereoanlage verwenden, handeln Sie nicht an den Implementierungsdetails, sondern an der Schnittstelle und Sie gehen davon aus, dass die Schnittstelle bei Änderungen der Implementierung (z. B. Dieselmotor oder Gas) gleich bleibt. Durch das Programmieren auf einer Schnittstelle können Sie Ihr Verhalten beibehalten, wenn nicht unterbrechende Details geändert, optimiert oder behoben werden. Dies vereinfacht auch die Aufgabe des Dokumentierens, Lernens und Verwendens.

Außerdem können Sie mit einer Schnittstelle programmieren, was das Verhalten Ihres Codes ist, bevor Sie ihn schreiben. Sie erwarten von einer class, etwas zu tun. Sie können dieses etwas testen, bevor Sie den eigentlichen Code schreiben. Wenn Ihre Schnittstelle sauber und fertig ist und Sie gerne mit ihr interagieren, können Sie den eigentlichen Code schreiben, der die Dinge erledigt.

“Programm zu einer Schnittstelle” kann flexibler sein.

Zum Beispiel schreiben wir eine class Printer, die einen Druckdienst anbietet. Derzeit müssen 2 classn ( Cat und Dog ) gedruckt werden. Also schreiben wir Code wie unten

 class Printer { public void PrintCat(Cat cat) { ... } public void PrintDog(Dog dog) { ... } ... } 

Wie wäre es mit einer neuen class? Bird benötigt auch diesen Druckdienst? Wir müssen die Printer class ändern, um eine neue Methode PrintBird () hinzuzufügen. Wenn wir die Printer-class entwickeln, wissen wir wahrscheinlich nicht, wer sie verwenden wird. So, wie man Printer schreibt? Programm zu einer Schnittstelle kann helfen, siehe unten Code

 class Printer { public void Print(Printable p) { Bitmap bitmap = p.GetBitmap(); // print bitmap ... } } 

Mit diesem neuen Drucker kann alles gedruckt werden, solange es Interface Printable implementiert. Die Methode GetBitmap () ist hier nur ein Beispiel. Das Entscheidende ist, eine Schnittstelle und keine Implementierung verfügbar zu machen.

Ich hoffe, es ist hilfreich.

Im Wesentlichen sind Schnittstellen die etwas konkretere Darstellung von allgemeinen Konzepten der Interoperation – sie liefern die Spezifikation dafür, was all die verschiedenen Optionen, die Sie für eine bestimmte function “einstecken” wollen, in ähnlicher Weise tun sollten, so dass Code, der sie verwendet, nicht sein wird abhängig von einer bestimmten Option.

Zum Beispiel fungieren viele DB-Bibliotheken als Schnittstellen, da sie mit vielen verschiedenen tatsächlichen DBs (MSSQL, MySQL, PostgreSQL, SQLite usw.) arbeiten können, ohne dass der Code, der die DB-Bibliothek verwendet, überhaupt geändert werden muss.

Alles in allem ermöglicht es Ihnen, Code zu erstellen, der flexibler ist. Er bietet Ihren Kunden mehr Möglichkeiten, wie sie ihn verwenden können. Außerdem können Sie Code möglicherweise an mehreren Stellen einfacher wiederverwenden, anstatt neuen spezialisierten Code schreiben zu müssen.

Wenn Sie eine Schnittstelle programmieren, wenden Sie eher das Prinzip der niedrigen Kopplung / hohen Kohäsion an. Indem Sie auf eine Schnittstelle programmieren, können Sie leicht die Implementierung dieser Schnittstelle (die spezifische class) wechseln.

Dies bedeutet, dass Ihre Variablen, Eigenschaften, Parameter und Rückgabetypen anstelle einer konkreten Implementierung einen Schnittstellentyp haben sollten.

Das bedeutet, dass Sie beispielsweise IEnumerable Foo(IList mylist) anstelle von ArrayList Foo(ArrayList myList) .

Verwenden Sie die Implementierung nur beim Erstellen des Objekts:

 IList list = new ArrayList(); 

Wenn Sie dies getan haben, können Sie später den Objekttyp ändern, vielleicht möchten Sie später LinkedList anstelle von ArrayList verwenden, das ist kein Problem, da Sie überall sonst nur von “IList” sprechen.

Eine auf der Schnittstelle basierende Programmierung stellt das Fehlen einer starken Kopplung mit einem bestimmten Objekt zur Laufzeit bereit. Da die Objektvariablen in Java polymorph sind, kann sich der Objektverweis auf die Superklasse auf ein Objekt einer seiner Unterklassen beziehen. Objekte, die mit Supertyp deklariert wurden, können Objekten zugewiesen werden, die zu einer bestimmten Implementierung des Supertyps gehören.

Beachten Sie, dass als Schnittstelle eine abstrakte class verwendet werden kann.

Bildbeschreibung hier eingeben

Programmierung basierend auf Implementierung:

 Motorcycle motorcycle = new Motorcycle(); motorcycle.driveMoto(); 

Programmierung basierend auf Schnittstelle:

 Vehicle vehicle; vehicle = new Motorcycle(); // Note that DI -framework can do it for you vehicle.drive(); 

Es ist im Grunde, wo Sie eine Methode / Schnittstelle wie create(param) create( 'apple' ) : create( 'apple' ) wo die Methode create(param) von einer abstrakten class / Schnittstelle fruit , die später von konkreten classn implementiert wird. Dies ist anders als Unterklassen. Sie erstellen einen Vertrag, den classn erfüllen müssen. Dies reduziert auch die Kopplung und macht Dinge flexibler, wo jede konkrete class sie anders implementiert.

Der Client-Code ist sich der spezifischen Objekttypen nicht bewusst und weiß nichts von den classn, die diese Objekte implementieren. Client-Code kennt nur die Schnittstelle create(param) und verwendet sie, um create(param) . Es ist so, als würde man sagen: “Es ist mir egal, wie du es bekommst oder ich es mache, ich will nur, dass du es mir gibst.”

Eine Analogie dazu ist eine Reihe von Ein-und Aus-Tasten. Das ist eine Schnittstelle on() und off() . Sie können diese Tasten auf mehreren Geräten, einem Fernseher, Radio, Licht verwenden. Sie alle gehen mit ihnen anders um, aber das interessiert uns nicht, alles, was uns interessiert, ist es, sie einzuschalten oder auszuschalten.