Ist das nicht der springende Punkt einer Schnittstelle, bei der mehrere Klassen eine Reihe von Regeln und Implementierungen einhalten?
Ist das nicht der springende Punkt einer Schnittstelle, bei der mehrere Klassen eine Reihe von Regeln und Implementierungen einhalten?
Antworten:
Genau genommen, nein, tust du nicht, YAGNI gilt. Der Zeitaufwand für die Erstellung der Benutzeroberfläche ist jedoch minimal, insbesondere wenn Sie ein praktisches Tool zur Codegenerierung haben, das den größten Teil der Arbeit für Sie erledigt. Wenn Sie sich nicht sicher sind, ob Sie die Schnittstelle von benötigen oder nicht, ist es besser, die Definition einer Schnittstelle zu unterstützen.
Darüber hinaus bietet die Verwendung einer Schnittstelle auch für eine einzelne Klasse eine weitere Scheinimplementierung für Komponententests, die nicht in Betrieb ist. Die Antwort von Avner Shahar-Kashtan wird in diesem Punkt erweitert.
Ich würde antworten, ob Sie eine Schnittstelle benötigen oder nicht, hängt nicht davon ab, wie viele Klassen sie implementieren. Schnittstellen sind ein Tool zum Definieren von Verträgen zwischen mehreren Subsystemen Ihrer Anwendung. Entscheidend ist also, wie Ihre Anwendung in Subsysteme unterteilt ist. Es sollte Schnittstellen als Front-End für gekapselte Subsysteme geben, unabhängig davon, wie viele Klassen sie implementieren.
Hier ist eine sehr nützliche Faustregel:
Foo
direkt auf die Klasse bezieht BarImpl
, verpflichten Sie sich nachdrücklich, sich Foo
jedes Mal zu ändern, wenn Sie sich ändern BarImpl
. Sie behandeln sie im Grunde als eine Codeeinheit, die auf zwei Klassen aufgeteilt ist.Foo
sich auf die Benutzeroberfläche beziehen, Bar
verpflichten Sie sich stattdessen, Änderungen beim Ändern zu vermeiden .Foo
BarImpl
Wenn Sie Schnittstellen an den Schlüsselpunkten Ihrer Anwendung definieren, überlegen Sie genau, welche Methoden diese unterstützen sollen und welche nicht, und kommentieren Sie die Schnittstellen klar, um zu beschreiben, wie sich eine Implementierung verhalten soll (und wie nicht). Ihre Anwendung wird viel einfacher zu verstehen, weil diese kommentiert Schnittstellen eine Art bieten wird Spezifikation der anwendungs eine Beschreibung, wie es beabsichtigt zu verhalten. Dies macht es viel einfacher, den Code zu lesen (anstatt zu fragen, was zum Teufel dieser Code tun soll, können Sie fragen, wie dieser Code das tut, was er tun soll).
Darüber hinaus (oder gerade deswegen) fördern Schnittstellen die separate Kompilierung. Da die Kompilierung von Schnittstellen trivial ist und weniger Abhängigkeiten aufweist als deren Implementierungen, bedeutet dies, dass Sie normalerweise eine Neukompilierung durchführen können, ohne dass eine Neukompilierung erforderlich ist, wenn Sie eine Klasse Foo
zur Verwendung einer Schnittstelle schreiben . In großen Anwendungen kann dies viel Zeit sparen.Bar
BarImpl
Foo
Foo
auf der Schnittstelle hängt Bar
dann BarImpl
kann modifiziert werden , ohne neu kompilieren zu müssen Foo
. 4. Feinere Zugriffskontrolle als bei öffentlichen / privaten Angeboten (die gleiche Klasse über verschiedene Schnittstellen zwei Clients zur Verfügung stellen).
If you make class Foo refer directly to class BarImpl, you're strongly committing yourself to change Foo every time you change BarImpl
Welche Änderungen können beim Ändern von BarImpl in Foo durch Verwendung von vermieden werden interface Bar
? Da sich die Signaturen und Funktionen einer Methode in BarImpl nicht ändern, muss Foo auch ohne Schnittstelle nicht geändert werden (der gesamte Zweck der Schnittstelle). Ich spreche über das Szenario, in dem nur eine Klasse BarImpl
Bar implementiert. Für Szenarien mit mehreren Klassen verstehe ich das Prinzip der Abhängigkeitsinversion und wie es für die Benutzeroberfläche hilfreich ist.
Schnittstellen sind dazu bestimmt, ein Verhalten zu definieren, dh eine Reihe von Prototypen von Funktionen / Methoden. Die Typen, die die Schnittstelle implementieren, implementieren dieses Verhalten. Wenn Sie sich also mit einem solchen Typ befassen, wissen Sie (teilweise), welches Verhalten er hat.
Es ist nicht erforderlich, eine Schnittstelle zu definieren, wenn Sie wissen, dass das von ihr definierte Verhalten nur einmal verwendet wird. KISS (halte es einfach, dumm)
Während Sie theoretisch keine Schnittstelle haben sollten, nur um eine Schnittstelle zu haben, gibt die Antwort von Yannis Rizos Hinweise auf weitere Komplikationen:
Wenn Sie Unit-Tests schreiben und Mock-Frameworks wie Moq oder FakeItEasy verwenden (um die beiden zuletzt verwendeten zu nennen), erstellen Sie implizit eine weitere Klasse, die die Schnittstelle implementiert. Das Durchsuchen des Codes oder die statische Analyse könnte behaupten, dass es nur eine Implementierung gibt, aber tatsächlich gibt es die interne Scheinimplementierung. Wann immer Sie anfangen, Mocks zu schreiben, werden Sie feststellen, dass das Extrahieren von Interfaces Sinn macht.
Aber warte, es gibt noch mehr. Es gibt weitere Szenarien, in denen implizite Schnittstellenimplementierungen vorhanden sind. Beispielsweise generiert der WCF-Kommunikationsstapel von .NET einen Proxy für einen Remotedienst, der die Schnittstelle erneut implementiert.
In einer sauberen Code-Umgebung stimme ich den restlichen Antworten hier zu. Beachten Sie jedoch alle Frameworks, Muster oder Abhängigkeiten, die Sie haben und die möglicherweise Schnittstellen verwenden.
Nein, Sie brauchen sie nicht, und ich halte es für ein Anti-Pattern, automatisch Schnittstellen für jede Klassenreferenz zu erstellen.
Die Erstellung von Foo / FooImpl für alles ist mit echten Kosten verbunden. Die IDE kann die Schnittstelle / Implementierung kostenlos erstellen, aber wenn Sie im Code navigieren, haben Sie die zusätzliche kognitive Belastung von F3 / F12 foo.doSomething()
, wenn Sie zur Schnittstellensignatur und nicht zur gewünschten Implementierung gelangen. Außerdem haben Sie die Unordnung von zwei Dateien anstelle von einer für alles.
Sie sollten es also nur dann tun, wenn Sie es tatsächlich für etwas brauchen.
Wenden wir uns nun den Gegenargumenten zu:
Ich benötige Interfaces für Dependency Injection Frameworks
Schnittstellen zur Unterstützung von Frameworks sind Legacy. In Java waren Schnittstellen eine Voraussetzung für dynamische Proxys (vor CGLIB). Heute braucht man es normalerweise nicht mehr. Es wird als Fortschritt und Segen für die Entwicklerproduktivität angesehen, dass Sie sie in EJB3, Spring usw. nicht mehr benötigen.
Ich brauche Mocks für Unit-Tests
Wenn Sie Ihre eigenen Mocks schreiben und zwei aktuelle Implementierungen haben, ist eine Schnittstelle angemessen. Wir hätten diese Diskussion wahrscheinlich gar nicht erst, wenn Ihre Codebasis sowohl ein FooImpl als auch ein TestFoo hätte.
Wenn Sie jedoch ein Mocking-Framework wie Moq, EasyMock oder Mockito verwenden, können Sie Klassen verspotten und benötigen keine Schnittstellen. Dies entspricht der Einstellung foo.method = mockImplementation
in einer dynamischen Sprache, in der Methoden zugewiesen werden können.
Wir brauchen Schnittstellen, die dem Dependency Inversion Principle (DIP) folgen
DIP sagt, Sie bauen auf Verträge (Schnittstellen) und nicht auf Implementierungen. Aber eine Klasse ist schon ein Vertrag und eine Abstraktion. Dafür sind die öffentlichen / privaten Schlüsselwörter gedacht. In der Universität war das kanonische Beispiel so etwas wie eine Matrix- oder Polynomklasse - Verbraucher haben eine öffentliche API, um Matrizen zu erstellen, hinzuzufügen usw., aber es ist ihnen egal, ob die Matrix in spärlicher oder dichter Form implementiert ist. Es war weder IMatrix noch MatrixImpl erforderlich, um diesen Punkt zu beweisen.
Außerdem wird DIP häufig auf allen Klassen- / Methodenaufrufebenen und nicht nur an wichtigen Modulgrenzen übermäßig angewendet. Ein Anzeichen dafür, dass Sie DIP zu häufig anwenden, ist, dass sich Ihre Schnittstelle und Implementierung im Gleichschritt ändern, sodass Sie zwei Dateien berühren müssen, um eine Änderung anstelle einer vorzunehmen. Wenn DIP richtig angewendet wird, bedeutet dies, dass sich Ihre Schnittstelle nicht oft ändern muss. Ein weiteres Anzeichen ist, dass Ihre Schnittstelle nur einen echten Verbraucher (eine eigene Anwendung) hat. Eine andere Geschichte, wenn Sie eine Klassenbibliothek für den Konsum in vielen verschiedenen Apps erstellen.
Dies ist eine Folge von Onkel Bob Martins Äußerungen über das Verspotten - Sie sollten sich nur über wichtige architektonische Grenzen lustig machen müssen. In einer Webanwendung sind der HTTP- und der DB-Zugriff die Hauptgrenzen. Alle Klassen- / Methodenaufrufe dazwischen sind es nicht. Gleiches gilt für DIP.
Siehe auch:
new Mockup<YourClass>() {}
und die gesamte Klasse, einschließlich ihrer Konstruktoren, wird verspottet, unabhängig davon, ob es sich um eine Schnittstelle, eine abstrakte Klasse oder eine konkrete Klasse handelt. Sie können das Konstruktorverhalten auch "überschreiben", wenn Sie dies möchten. Ich nehme an, es gibt in Mockito oder Powermock gleichwertige Möglichkeiten.
Es sieht so aus, als könnten die Antworten auf beiden Seiten des Zauns folgendermaßen zusammengefasst werden:
Entwerfen Sie gut und platzieren Sie Schnittstellen dort, wo sie benötigt werden.
Wie ich in meiner Antwort auf Yannis Antwort bemerkt habe , glaube ich nicht, dass Sie jemals eine harte und schnelle Regel über Schnittstellen haben können. Die Regel muss per Definition flexibel sein. Meine Regel für Schnittstellen lautet, dass eine Schnittstelle überall dort verwendet werden sollte, wo Sie eine API erstellen. Und eine API sollte überall dort erstellt werden, wo Sie die Grenze von einem Verantwortungsbereich in einen anderen überschreiten.
Nehmen wir zum Beispiel an, Sie bauen eine Car
Klasse. In Ihrer Klasse benötigen Sie auf jeden Fall eine UI-Ebene. In diesem speziellen Beispiel, nimmt es die Form eines IginitionSwitch
, SteeringWheel
, GearShift
, GasPedal
, und BrakePedal
. Da dieses Auto eine enthält AutomaticTransmission
, brauchen Sie keine ClutchPedal
. (Und da es sich um ein schreckliches Auto handelt, gibt es keine Klimaanlage, kein Radio und keinen Sitz. Tatsächlich fehlen auch die Dielenbretter - man muss sich nur an das Lenkrad klammern und auf das Beste hoffen!)
Welche dieser Klassen benötigen also eine Schnittstelle? Die Antwort könnte alle oder keine sein - je nach Ihrem Design.
Sie könnten eine Schnittstelle haben, die so aussieht:
Interface ICabin
Event IgnitionSwitchTurnedOn()
Event IgnitionSwitchTurnedOff()
Event BrakePedalPositionChanged(int percent)
Event GasPedalPositionChanged(int percent)
Event GearShiftGearChanged(int gearNum)
Event SteeringWheelTurned(float degree)
End Interface
Zu diesem Zeitpunkt wird das Verhalten dieser Klassen Teil der ICabin-Schnittstelle / API. In diesem Beispiel sind die Klassen (falls vorhanden) wahrscheinlich einfach, mit einigen Eigenschaften und einer oder zwei Funktionen. Was Sie implizit mit Ihrem Design angeben, ist, dass diese Klassen nur existieren, um die konkrete Implementierung von ICabin zu unterstützen, die Sie haben, und sie können nicht für sich existieren, oder sie sind außerhalb des ICabin-Kontexts bedeutungslos.
Es ist der gleiche Grund, warum Sie private Mitglieder nicht einem Komponententest unterziehen - sie dienen nur zur Unterstützung der öffentlichen API, und daher sollte ihr Verhalten durch Testen der API getestet werden.
Wenn Ihre Klasse also nur zur Unterstützung einer anderen Klasse vorhanden ist und Sie sie konzeptionell als nicht wirklich eine eigene Domäne aufweisend betrachten, ist es in Ordnung, die Schnittstelle zu überspringen. Wenn Ihre Klasse jedoch so wichtig ist, dass Sie der Meinung sind, dass sie erwachsen genug ist, um eine eigene Domäne zu haben, geben Sie ihr eine Schnittstelle.
BEARBEITEN:
Häufig (einschließlich dieser Antwort) lesen Sie Dinge wie "Domain", "Abhängigkeit" (häufig in Verbindung mit "Injection"), die für Sie zu Beginn des Programmierens keine Bedeutung haben (das haben sie sicher nicht) mir etwas bedeuten). Für die Domain bedeutet dies genau, wie es sich anhört:
Das Gebiet, über das Herrschaft oder Autorität ausgeübt wird; die Besitztümer eines Souveräns oder Commonwealth oder dergleichen. Auch im übertragenen Sinne verwendet. [WordNet sense 2] [1913 Webster]
In den Begriffen meines Beispiels - lassen Sie uns das betrachten IgnitionSwitch
. In einem Meatspace-Auto ist der Zündschalter verantwortlich für:
Diese Eigenschaften bilden die Domäne des IgnitionSwitch
Wissens und der Verantwortlichen.
Der IgnitionSwitch
ist nicht verantwortlich für die GasPedal
. Der Zündschalter kennt das Gaspedal in keiner Weise. Sie arbeiten beide völlig unabhängig voneinander (obwohl ein Auto ohne sie beide ziemlich wertlos wäre!).
Wie ich ursprünglich sagte, hängt es von Ihrem Design ab. Sie können eine entwerfen IgnitionSwitch
, die zwei Werte hat: On ( True
) und Off ( False
). Sie können es auch so gestalten, dass der dafür vorgesehene Schlüssel und eine Vielzahl anderer Aktionen authentifiziert werden. Das ist der schwierige Teil, ein Entwickler zu sein, zu entscheiden, wo die Linien im Sand gezogen werden sollen - und ehrlich gesagt ist es die meiste Zeit absolut relativ. Diese Linien im Sand sind jedoch wichtig - dort befindet sich Ihre API und daher sollten sich Ihre Schnittstellen befinden.
Von MSDN :
Schnittstellen eignen sich besser für Situationen, in denen Ihre Anwendungen viele möglicherweise nicht zusammenhängende Objekttypen benötigen, um bestimmte Funktionen bereitzustellen.
Schnittstellen sind flexibler als Basisklassen, da Sie eine einzige Implementierung definieren können, die mehrere Schnittstellen implementieren kann.
Schnittstellen sind in Situationen besser, in denen Sie die Implementierung nicht von einer Basisklasse erben müssen.
Schnittstellen sind nützlich, wenn Sie die Klassenvererbung nicht verwenden können. Beispielsweise können Strukturen nicht von Klassen erben, aber sie können Schnittstellen implementieren.
Im Allgemeinen ist es für eine einzelne Klasse nicht erforderlich, eine Schnittstelle zu implementieren. In Anbetracht der Zukunft Ihres Projekts kann es jedoch hilfreich sein, das erforderliche Verhalten von Klassen formal zu definieren.
Um die Frage zu beantworten: Es steckt mehr dahinter.
Ein wichtiger Aspekt einer Schnittstelle ist die Absicht.
Eine Schnittstelle ist "ein abstrakter Typ, der keine Daten enthält, aber Verhalten offenlegt" - Schnittstelle (Computing) Wenn es sich also um ein Verhalten oder eine Reihe von Verhalten handelt, die von der Klasse unterstützt werden, ist wahrscheinlich das richtige Muster für eine Schnittstelle. Wenn jedoch die Verhaltensweisen dem von der Klasse verkörperten Konzept eigen sind, möchten Sie wahrscheinlich überhaupt keine Schnittstelle.
Die erste Frage, die Sie stellen müssen, ist, welche Art von Dingen oder Prozessen Sie darstellen möchten. Folgen Sie dann den praktischen Gründen für die Implementierung dieser Art auf eine bestimmte Weise.
Seit Sie diese Frage gestellt haben, haben Sie vermutlich bereits die Interessen einer Schnittstelle erkannt, die mehrere Implementierungen verbirgt. Dies kann durch das Abhängigkeitsinversionsprinzip manifestiert werden.
Die Notwendigkeit einer Schnittstelle hängt jedoch nicht von der Anzahl ihrer Implementierungen ab. Die eigentliche Rolle einer Schnittstelle besteht darin, dass sie einen Vertrag definiert, der angibt, welcher Dienst bereitgestellt werden soll, anstatt wie er implementiert werden soll.
Sobald der Vertrag definiert ist, können zwei oder mehr Teams unabhängig voneinander arbeiten. Angenommen, Sie arbeiten an einem Modul A und es hängt vom Modul B ab. Die Tatsache, dass Sie eine Schnittstelle auf B erstellen, ermöglicht es Ihnen, Ihre Arbeit fortzusetzen, ohne sich um die Implementierung von B zu kümmern, da alle Details von der Schnittstelle verborgen werden. Somit wird eine verteilte Programmierung möglich.
Obwohl Modul B nur eine Implementierung seiner Schnittstelle hat, ist die Schnittstelle immer noch erforderlich.
Zusammenfassend verbirgt eine Schnittstelle Implementierungsdetails vor ihren Benutzern. Das Programmieren auf die Benutzeroberfläche hilft, mehr Dokumente zu schreiben, da der Vertrag definiert werden muss, modularere Software geschrieben werden muss, Unit-Tests gefördert und die Entwicklungsgeschwindigkeit beschleunigt werden sollen.
Alle Antworten hier sind sehr gut. Tatsächlich werden die meisten der Zeit , dass Sie nicht brauchen eine andere Schnittstelle zu implementieren. Aber es gibt Fall , in dem Sie können wollen , es trotzdem zu tun. Hier ist ein Fall, in dem ich es mache:
Die Klasse implementiert eine andere Schnittstelle, die ich nicht
häufig mit Adapterklasse verfügbar machen möchte, die Code von Drittanbietern überbrückt.
interface NameChangeListener { // Implemented by a lot of people
void nameChanged(String name);
}
interface NameChangeCount { // Only implemented by my class
int getCount();
}
class NameChangeCounter implements NameChangeListener, NameChangeCount {
...
}
class SomeUserInterface {
private NameChangeCount currentCount; // Will never know that you can change the counter
}
Die Klasse verwendet eine spezielle Technologie, die nicht durchläuft,
wenn sie mit externen Bibliotheken interagiert. Selbst wenn es nur eine Implementierung gibt, verwende ich eine Schnittstelle, um sicherzustellen, dass keine unnötige Kopplung mit der externen Bibliothek hergestellt wird.
interface SomeRepository { // Guarantee that the external library details won't leak trough
...
}
class OracleSomeRepository implements SomeRepository {
... // Oracle prefix allow us to quickly know what is going on in this class
}
Layerübergreifende Kommunikation Auch wenn nur eine UI-Klasse jemals eine der Domänenklassen implementiert, ermöglicht sie eine bessere Trennung zwischen diesen Layern und vermeidet vor allem zyklische Abhängigkeiten.
package project.domain;
interface UserRequestSource {
public UserRequest getLastRequest();
}
class UserBehaviorAnalyser {
private UserRequestSource requestSource;
}
package project.ui;
class OrderCompleteDialog extends SomeUIClass implements project.domain.UserRequestSource {
// UI concern, no need for my domain object to know about this method.
public void displayLabelInErrorMode();
// They most certainly need to know about *that* though
public UserRequest getLastRequest();
}
Für die meisten Objekte sollte nur eine Teilmenge der Methode verfügbar sein. Tritt
meistens auf, wenn ich eine Konfigurationsmethode für die konkrete Klasse habe
interface Sender {
void sendMessage(Message message)
}
class PacketSender implements Sender {
void sendMessage(Message message);
void setPacketSize(int sizeInByte);
}
class Throttler { // This class need to have full access to the object
private PacketSender sender;
public useLowNetworkUsageMode() {
sender.setPacketSize(LOW_PACKET_SIZE);
sender.sendMessage(new NotifyLowNetworkUsageMessage());
... // Other details
}
}
class MailOrder { // Not this one though
private Sender sender;
}
Letztendlich benutze ich die Schnittstelle aus dem gleichen Grund, aus dem ich das private Feld benutze: Ein anderes Objekt sollte keinen Zugriff auf Dinge haben, auf die es keinen Zugriff haben sollte. Wenn ich einen solchen Fall habe, führe ich eine Schnittstelle ein, auch wenn nur eine Klasse sie implementiert.
Schnittstellen sind wirklich wichtig, aber versuchen Sie, die Anzahl der vorhandenen Schnittstellen zu kontrollieren.
Nachdem man die Entwicklung von Schnittstellen für so gut wie alles hinter sich gelassen hat, kann man leicht mit zerhacktem Spaghetti-Code enden. Ich verweise auf die größere Weisheit von Ayende Rahien, der einige sehr weise Worte zu diesem Thema geschrieben hat:
http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application
Dies ist sein erster Beitrag in einer ganzen Reihe, also lies weiter!
Ein Grund, warum Sie in diesem Fall immer noch eine Schnittstelle einführen möchten, ist das Befolgen des Abhängigkeitsinversionsprinzips . Das heißt, das Modul, das die Klasse verwendet, hängt von ihrer Abstraktion (dh der Schnittstelle) ab, anstatt von einer konkreten Implementierung. Es entkoppelt Komponenten auf hoher Ebene von Komponenten auf niedriger Ebene.
Es gibt keinen wirklichen Grund, etwas zu tun. Schnittstellen sollen Ihnen helfen und nicht das Ausgabeprogramm. Selbst wenn das Interface von einer Million Klassen implementiert wird, gibt es keine Regel, die besagt, dass Sie eine erstellen müssen. Sie erstellen eine, damit Sie oder jeder andere, der Ihren Code verwendet, etwas ändern möchten, das in alle Implementierungen einfließt. Das Erstellen einer Schnittstelle unterstützt Sie in allen zukünftigen Fällen, in denen Sie möglicherweise eine andere Klasse erstellen möchten, die diese implementiert.
Es ist nicht immer erforderlich, eine Schnittstelle für eine Klasse zu definieren.
Einfache Objekte wie Wertobjekte sind nicht mehrfach implementiert. Sie müssen auch nicht verspottet werden. Die Implementierung kann alleine getestet werden, und wenn andere Klassen getestet werden, die davon abhängen, kann das tatsächliche Wertobjekt verwendet werden.
Denken Sie daran, dass das Erstellen einer Schnittstelle mit Kosten verbunden ist. Es muss während der Implementierung aktualisiert werden, es benötigt eine zusätzliche Datei, und einige IDEs haben Probleme beim Zoomen in der Implementierung, nicht in der Benutzeroberfläche.
Daher würde ich Interfaces nur für übergeordnete Klassen definieren, bei denen Sie eine Abstraktion von der Implementierung wünschen.
Beachten Sie, dass Sie mit einer Klasse ein kostenloses Interface erhalten. Neben der Implementierung definiert eine Klasse eine Schnittstelle aus der Menge der öffentlichen Methoden. Diese Schnittstelle wird von allen abgeleiteten Klassen implementiert. Eigentlich ist es keine Schnittstelle, aber es kann genauso verwendet werden. Ich glaube nicht, dass es notwendig ist, eine Schnittstelle neu zu erstellen, die bereits unter dem Namen der Klasse existiert.