it-swarm-eu.dev

Muss ich eine Schnittstelle verwenden, wenn nur eine Klasse sie jemals implementieren wird?

Ist es nicht der springende Punkt einer Schnittstelle, dass mehrere Klassen eine Reihe von Regeln und Implementierungen einhalten?

255
Lamin Sanneh

Genau genommen, nein, tust du nicht, YAGNI gilt. Der Zeitaufwand für die Erstellung der Benutzeroberfläche ist jedoch minimal, insbesondere wenn Sie über ein praktisches Tool zur Codegenerierung verfügen, 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, würde ich sagen, dass es besser ist, sich auf die Seite zu stellen, um die Definition einer Schnittstelle zu unterstützen.

Wenn Sie eine Schnittstelle auch nur für eine einzelne Klasse verwenden, erhalten Sie eine weitere Scheinimplementierung für Komponententests, die nicht in Produktion ist. Antwort von Avner Shahar-Kashtan erweitert diesen Punkt.

209
yannis

Ich würde antworten, ob Sie eine Schnittstelle benötigen oder nicht nicht hängt davon ab, wie viele Klassen sie implementieren werden. Schnittstellen sind ein Werkzeug zum Definieren von Verträgen zwischen mehreren Subsystemen Ihrer Anwendung. Was also wirklich zählt, ist, 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:

  • Wenn Sie die Klasse Foo direkt auf die Klasse BarImpl verweisen lassen, verpflichten Sie sich nachdrücklich, Foo jedes Mal zu ändern, wenn Sie BarImpl ändern. Sie behandeln sie im Grunde genommen als eine Codeeinheit, die auf zwei Klassen aufgeteilt ist.
  • Wenn Sie Foo auf die Schnittstelle verweisen lassen Bar, verpflichten Sie sich stattdessen zu Vermeiden Sie Änderungen an Foo, wenn Sie BarImpl ändern.

Wenn Sie Schnittstellen an den wichtigsten Punkten Ihrer Anwendung definieren, überlegen Sie genau, welche Methoden sie unterstützen sollten und welche nicht, und Sie kommentieren die Schnittstellen klar, um zu beschreiben, wie sich eine Implementierung verhalten soll (und wie nicht). Ihre Anwendung wird viel einfacher zu verstehen sein, da diese kommentierten Schnittstellen eine Art Spezifikation der Anwendung enthalten - eine Beschreibung, wie sie beabsichtigt sich 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).

Zusätzlich zu all dem (oder tatsächlich deswegen) fördern Schnittstellen die separate Kompilierung. Da Schnittstellen einfach zu kompilieren sind und weniger Abhängigkeiten aufweisen als ihre Implementierungen, bedeutet dies, dass Sie Foo normalerweise neu kompilieren können, wenn Sie die Klasse Bar schreiben, um eine Schnittstelle BarImpl zu verwenden Foo neu kompilieren. In großen Anwendungen kann dies viel Zeit sparen.

150
sacundim

Schnittstellen sind dazu bestimmt, ein Verhalten zu definieren, d. H. 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. KUSS (halte es einfach, dumm)

102
superM

Während Sie theoretisch keine Schnittstelle haben sollten, nur um eine Schnittstelle zu haben, Yannis Rizos 'Antwort Hinweise auf weitere Komplikationen:

Wenn Sie Komponententests schreiben und Schein-Frameworks wie Moq oder FakeItEasy verwenden (um die beiden neuesten zu nennen, die ich verwendet habe), erstellen Sie implizit eine andere Klasse, die die Schnittstelle implementiert. Durch Durchsuchen des Codes oder statische Analyse wird möglicherweise behauptet, 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 Schnittstellen sinnvoll ist.

Aber warte, da ist noch mehr. Es gibt weitere Szenarien, in denen implizite Schnittstellenimplementierungen vorhanden sind. Wenn Sie beispielsweise den WCF-Kommunikationsstapel von .NET verwenden, wird ein Proxy für einen Remotedienst generiert, der wiederum die Schnittstelle implementiert.

In einer sauberen Codeumgebung stimme ich den restlichen Antworten hier zu. Achten Sie jedoch auf alle Frameworks, Muster oder Abhängigkeiten, die möglicherweise Schnittstellen verwenden.

62

Nein, Sie brauchen sie nicht, und ich halte es für ein Anti-Pattern, automatisch Schnittstellen für jede Klassenreferenz zu erstellen.

Die Herstellung von Foo/FooImpl ist für alles sehr teuer. Die IDE kann die Schnittstelle/Implementierung kostenlos erstellen, aber wenn Sie im Code navigieren, haben Sie die zusätzliche kognitive Last von F3/F12 auf foo.doSomething(), die Sie zum führt Schnittstellensignatur und nicht die eigentliche Implementierung, die Sie möchten. Außerdem haben Sie die Unordnung von zwei Dateien anstelle von einer für alles.

Sie sollten es also nur tun, wenn Sie es tatsächlich für etwas brauchen.

Nun zu den Gegenargumenten:

Ich benötige Schnittstellen für Frameworks zur Abhängigkeitsinjektion

Schnittstellen zur Unterstützung von Frameworks sind Legacy. In Java waren Schnittstellen eine Anforderung 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 tatsächliche Implementierungen haben, ist eine Schnittstelle geeignet. Wir würden diese Diskussion wahrscheinlich überhaupt nicht führen, wenn Ihre Codebasis sowohl ein FooImpl als auch ein TestFoo hätte.

Wenn Sie jedoch ein Verspottungsframework wie Moq, EasyMock oder Mockito verwenden, können Sie Klassen verspotten und benötigen keine Schnittstellen. Es ist analog zur Einstellung von foo.method = mockImplementation in einer dynamischen Sprache, in der Methoden zugewiesen werden können.

Wir benötigen Schnittstellen, um dem Abhängigkeitsinversionsprinzip (DIP) zu folgen

DIP sagt, dass Sie bauen, um von Verträgen (Schnittstellen) und nicht von Implementierungen abhängig zu sein. 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, sie 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 jeder Klassen-/Methodenaufrufebene überbeansprucht, nicht nur an den Hauptmodulgrenzen. Ein Zeichen dafür, dass Sie DIP zu häufig anwenden, ist, dass sich Ihre Benutzeroberfläche und Implementierung im Sperrschritt ändern, sodass Sie zwei Dateien berühren müssen, um eine Änderung anstelle von einer vorzunehmen. Wenn DIP entsprechend angewendet wird, sollte sich Ihre Schnittstelle nicht häufig ändern müssen. Ein weiteres Zeichen ist, dass Ihre Schnittstelle nur einen echten Verbraucher hat (eine eigene Anwendung). Eine andere Geschichte, wenn Sie eine Klassenbibliothek für den Verbrauch in vielen verschiedenen Apps erstellen.

Dies ist eine Folge von Onkel Bob Martins Verspottungspunkt - Sie sollten sich nur an wichtigen architektonischen Grenzen lustig machen müssen. In einer Webanwendung sind der HTTP- und der DB-Zugriff die Hauptgrenzen. Alle dazwischen liegenden Klassen-/Methodenaufrufe sind es nicht. Gleiches gilt für DIP.

Siehe auch:

38
wrschneider

Es sieht so aus, als könnten die Antworten auf beiden Seiten des Zauns wie folgt zusammengefasst werden:

Entwerfen Sie gut und platzieren Sie Schnittstellen dort, wo Schnittstellen benötigt werden.

Wie ich in meiner Antwort auf Yannis Antwort bemerkt habe, glaube ich nicht, dass Sie jemals eine feste Regel für 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 erstellen eine Car -Klasse. In Ihrer Klasse benötigen Sie sicherlich eine UI-Ebene. In diesem speziellen Beispiel hat es die Form eines IginitionSwitch, SteeringWheel, GearShift, GasPedal und BrakePedal. Da dieses Auto ein AutomaticTransmission enthält, benötigen Sie kein ClutchPedal. (Und da dies ein schreckliches Auto ist, gibt es keine Klimaanlage, kein Radio oder keinen Sitz. Tatsächlich fehlen auch die Dielen - Sie müssen sich nur an diesem Lenkrad festhalten und auf das Beste hoffen!)

Welche dieser Klassen benötigen eine Schnittstelle? Die Antwort könnte alle oder keine sein - abhängig von 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. Und 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 alleine existieren oder sie sind außerhalb des ICabin-Kontexts bedeutungslos.

Es ist der gleiche Grund, warum Sie keine privaten Mitglieder einem Unit-Test unterziehen - sie existieren nur , um die öffentliche API zu unterstützen, und daher sollte ihr Verhalten von getestet werden Testen der API.

Wenn Ihre Klasse also nur zur Unterstützung einer anderen Klasse existiert und Sie sie konzeptionell als nicht wirklich mit einer eigenen Domain ansehen, ist es in Ordnung, die Schnittstelle zu überspringen. Aber wenn Ihre Klasse wichtig genug ist, dass Sie sie für erwachsen genug halten, um eine eigene Domain zu haben, geben Sie ihr eine Schnittstelle.


BEARBEITEN:

Häufig (einschließlich in dieser Antwort) lesen Sie Dinge wie "Domäne", "Abhängigkeit" (häufig verbunden mit "Injektion"), die Ihnen zu Beginn des Programmierens nichts bedeuten (sie haben es sicher nicht getan) mir etwas bedeuten). Für Domain bedeutet es genau, wie es sich anhört:

Das Gebiet, über das Herrschaft oder Autorität ausgeübt wird; die Besitztümer eines Souveräns oder Gemeinwesens oder dergleichen. Wird auch im übertragenen Sinne verwendet. [WordNet sense 2] [1913 Webster]

Betrachten wir in meinem Beispiel das IgnitionSwitch. In einem Meatspace-Auto ist der Zündschalter verantwortlich für:

  1. Authentifizierung (keine Identifizierung) des Benutzers (er benötigt den richtigen Schlüssel)
  2. Den Starter mit Strom versorgen, damit er das Auto tatsächlich starten kann
  3. Versorgung des Zündsystems mit Strom, damit es weiterarbeiten kann
  4. Strom abschalten, damit das Auto anhält.
  5. Je nachdem, wie Sie es sehen, gibt es in den meisten (allen?) Neueren Autos einen Schalter, der verhindert, dass der Schlüssel von der Zündung abgezogen wird, während sich das Getriebe außerhalb des Parks befindet. Dies könnte also Teil seiner Domäne sein. (Eigentlich bedeutet das, dass ich mein System überdenken und neu gestalten muss ...)

Diese Eigenschaften bilden die Domäne des IgnitionSwitch oder mit anderen Worten, was es weiß und wofür es verantwortlich ist.

Das IgnitionSwitch ist nicht verantwortlich für das GasPedal. Der Zündschalter kennt das Gaspedal in keiner Weise. Beide arbeiten völlig unabhängig voneinander (obwohl ein Auto ohne beide ziemlich wertlos wäre!).

Wie ich ursprünglich sagte, hängt es von Ihrem Design ab. Sie können ein IgnitionSwitch mit zwei Werten entwerfen: Ein (True) und Aus (False). Sie können es auch so gestalten, dass der dafür bereitgestellte Schlüssel und eine Vielzahl anderer Aktionen authentifiziert werden. Das ist der schwierige Teil, wenn man als Entwickler entscheidet, wo man die Linien in den Sand zieht - und ehrlich gesagt ist es die meiste Zeit völlig relativ. Diese Linien im Sand sind jedoch wichtig - dort befindet sich Ihre API und somit sollten sich Ihre Schnittstellen befinden.

22
Wayne Werner

Nein (YAGNI) , es sei denn, Sie planen, Tests für andere Klassen über diese Schnittstelle zu schreiben, und diese Tests würden davon profitieren, die Schnittstelle zu verspotten.

8
stijn

Von MSDN :

Schnittstellen eignen sich besser für Situationen, in denen Ihre Anwendungen viele möglicherweise nicht verwandte Objekttypen benötigen, um bestimmte Funktionen bereitzustellen.

Schnittstellen sind flexibler als Basisklassen, da Sie eine einzelne 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 in Fällen nützlich, in denen 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 im Fall einer einzelnen 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.

8
lwm

Seit Sie diese Frage gestellt haben, haben Sie wahrscheinlich bereits die Interessen einer Schnittstelle gesehen, die mehrere Implementierungen verbirgt. Dies kann durch das Prinzip der Abhängigkeitsinversion manifestiert werden.

Die Notwendigkeit, eine Schnittstelle zu haben oder nicht, 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 für B erstellen, ermöglicht es Ihnen, Ihre Arbeit fortzusetzen, ohne sich um die Implementierung von B sorgen zu müssen, da alle Details von der Schnittstelle ausgeblendet werden. Somit wird eine verteilte Programmierung möglich.

Obwohl Modul B nur eine Implementierung seiner Schnittstelle hat, ist die Schnittstelle immer noch notwendig.

Zusammenfassend verbirgt eine Schnittstelle Implementierungsdetails vor ihren Benutzern. Die Programmierung auf die Schnittstelle hilft dabei, mehr Dokumente zu schreiben, da Verträge definiert werden müssen, modularere Software geschrieben werden muss, Unit-Tests gefördert und die Entwicklungsgeschwindigkeit beschleunigt werden müssen.

5
Hui Wang

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 Verhaltensweisen offenlegt" - Schnittstelle (Computing) Wenn dies also ein Verhalten oder eine Reihe von Verhaltensweisen ist, die die Klasse unterstützt, ist dies eine Schnittstelle wahrscheinlich das richtige Muster. Wenn die Verhaltensweisen jedoch dem von der Klasse verkörperten Konzept innewohnen (sind), möchten Sie wahrscheinlich überhaupt keine Schnittstelle.

Die erste Frage ist, welche Art von Sache oder Prozess Sie darstellen möchten. Führen Sie dann die praktischen Gründe für die Umsetzung dieser Art auf eine bestimmte Weise an.

5
Joshua Drake

Alle Antworten hier sind sehr gut. In der Tat benötigen Sie die meiste Zeit nicht , um eine andere Schnittstelle zu implementieren. Aber es gibt Fälle, in denen Sie es trotzdem tun möchten. Hier ist ein Fall, in dem ich es mache:

Die Klasse implementiert eine andere Schnittstelle, die ich nicht verfügbar machen möchte
Tritt häufig bei Adapterklassen auf, die den Code von Drittanbietern überbrücken.

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 bestimmte Technologie, die nicht durchlaufen sollte
Meistens bei der Interaktion mit externen Bibliotheken. Selbst wenn es nur eine Implementierung gibt, verwende ich eine Schnittstelle, um sicherzustellen, dass keine unnötige Kopplung mit der externen Bibliothek eingeführt 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
}

Cross Layer Kommunikation
Selbst wenn nur eine UI-Klasse jemals eine der Domänenklassen implementiert, ermöglicht dies eine bessere Trennung zwischen diesen Ebenen und vor allem eine zyklische Abhängigkeit.

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
Meistens, 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;
}

Am Ende benutze ich die Schnittstelle aus dem gleichen Grund, aus dem ich ein privates Feld benutze: Andere Objekte sollten keinen Zugriff auf Dinge haben, auf die sie nicht zugreifen sollten. 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 Schnittstellen zu kontrollieren, die Sie haben.

Nachdem Sie sich für die Erstellung von Schnittstellen für nahezu alles entschieden haben, ist es einfach, den Code für zerhackte Spaghetti zu erhalten. Ich verweise auf die größere Weisheit von Ayende Rahien, der einige sehr weise Worte zu diesem Thema veröffentlicht hat:

http://ayende.com/blog/153889/limit-your-abstractions-analyzing-a-ddd-application

Dies ist sein erster Beitrag einer ganzen Reihe, also lesen Sie weiter!

3
Lord Spa

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 sich auf alle Implementierungen auswirkt. Das Erstellen einer Schnittstelle hilft Ihnen in allen zukünftigen Fällen, in denen Sie möglicherweise eine andere Klasse erstellen möchten, die sie implementiert.

2
John Demetriou

Ein Grund, warum Sie in diesem Fall möglicherweise noch eine Schnittstelle einführen möchten, ist die Befolgung des Dependency Inversion Principle . Das heißt, das Modul, das die Klasse verwendet, hängt von einer Abstraktion davon (d. H. Der Schnittstelle) ab, anstatt von einer konkreten Implementierung. Es entkoppelt Komponenten auf hoher Ebene von Komponenten auf niedriger Ebene.

2
dodgy_coder

Es ist nicht immer erforderlich, eine Schnittstelle für eine Klasse zu definieren.

Einfache Objekte wie Wertobjekte haben nicht mehrere Implementierungen. Sie müssen auch nicht verspottet werden. Die Implementierung kann selbst getestet werden. Wenn andere von ihnen abhängige Klassen getestet werden, kann das Objekt mit dem tatsächlichen Wert verwendet werden.

Denken Sie daran, dass das Erstellen einer Schnittstelle Kosten verursacht. Es muss entlang der Implementierung aktualisiert werden, es benötigt eine zusätzliche Datei und einige IDE haben Probleme beim Zoomen der Implementierung, nicht der Schnittstelle.

Daher würde ich Schnittstellen nur für Klassen höherer Ebenen definieren, bei denen Sie eine Abstraktion von der Implementierung wünschen.

Beachten Sie, dass Sie mit einer Klasse eine kostenlose Schnittstelle erhalten. Neben der Implementierung definiert eine Klasse eine Schnittstelle aus dem Satz öffentlicher Methoden. Diese Schnittstelle wird von allen abgeleiteten Klassen implementiert. Es handelt sich streng genommen nicht um eine Schnittstelle, sie kann jedoch genauso verwendet werden. Daher denke ich nicht, dass es notwendig ist, eine Schnittstelle neu zu erstellen, die bereits unter dem Namen der Klasse existiert.

1
Florian F