Warum bestehen die meisten Systemarchitekten darauf, zunächst eine Schnittstelle zu programmieren?

StackOverflow https://stackoverflow.com/questions/48605

Frage

In fast jedem Java-Buch, das ich lese, geht es um die Verwendung der Schnittstelle als Möglichkeit, den Zustand und das Verhalten zwischen Objekten auszutauschen, die bei ihrer ersten „Konstruktion“ anscheinend keine Beziehung zueinander hatten.

Wenn ich jedoch sehe, dass Architekten eine Anwendung entwerfen, beginnen sie als Erstes mit der Programmierung einer Schnittstelle.Wie kommts?Woher kennen Sie alle Beziehungen zwischen Objekten, die innerhalb dieser Schnittstelle auftreten?Wenn Sie diese Beziehungen bereits kennen, warum erweitern Sie dann nicht einfach eine abstrakte Klasse?

War es hilfreich?

Lösung

Beim Programmieren auf eine Schnittstelle muss der „Vertrag“ respektiert werden, der durch die Verwendung dieser Schnittstelle entsteht.Und wenn Ihr IPoweredByMotor Schnittstelle verfügt über eine start() Methode, zukünftige Klassen, die die Schnittstelle implementieren, sei es MotorizedWheelChair, Automobile, oder SmoothieMaker, Durch die Implementierung der Methoden dieser Schnittstelle verleihen Sie Ihrem System mehr Flexibilität, da ein Codeteil den Motor für viele verschiedene Arten von Dingen starten kann, da ein Codeteil lediglich wissen muss, auf welche Art von Dingen er reagiert start().Es spielt keine Rolle Wie Sie fangen an, nur dass sie muss beginnen.

Andere Tipps

Tolle Frage.Ich werde Sie verweisen Josh Bloch in Effective Java, der schreibt (Punkt 16), warum man die Verwendung von Schnittstellen gegenüber abstrakten Klassen bevorzugen sollte.Übrigens, wenn Sie dieses Buch noch nicht haben, kann ich es Ihnen wärmstens empfehlen!Hier ist eine Zusammenfassung dessen, was er sagt:

  1. Bestehende Klassen können einfach nachgerüstet werden, um eine neue Schnittstelle zu implementieren. Sie müssen lediglich die Schnittstelle implementieren und die erforderlichen Methoden hinzufügen.Bestehende Klassen können nicht einfach nachgerüstet werden, um eine neue abstrakte Klasse zu erweitern.
  2. Schnittstellen eignen sich ideal zum Definieren von Mix-Ins. Eine Mix-In-Schnittstelle ermöglicht es Klassen, zusätzliches, optionales Verhalten zu deklarieren (z. B. Comparable).Es ermöglicht die Kombination der optionalen Funktionalität mit der primären Funktionalität.Abstrakte Klassen können keine Mix-Ins definieren – eine Klasse kann nicht mehr als ein übergeordnetes Element erweitern.
  3. Schnittstellen ermöglichen nicht-hierarchische Frameworks. Wenn Sie eine Klasse haben, die über die Funktionalität vieler Schnittstellen verfügt, kann sie alle implementieren.Ohne Schnittstellen müssten Sie eine aufgeblähte Klassenhierarchie mit einer Klasse für jede Kombination von Attributen erstellen, was zu einer kombinatorischen Explosion führen würde.
  4. Schnittstellen ermöglichen sichere Funktionserweiterungen. Sie können Wrapper-Klassen mit dem Decorator-Muster erstellen, einem robusten und flexiblen Design.Eine Wrapper-Klasse implementiert und enthält dieselbe Schnittstelle, leitet einige Funktionen an vorhandene Methoden weiter und fügt anderen Methoden spezielles Verhalten hinzu.Dies ist mit abstrakten Methoden nicht möglich – Sie müssen stattdessen die Vererbung verwenden, die anfälliger ist.

Was ist mit dem Vorteil abstrakter Klassen, die eine grundlegende Implementierung bereitstellen?Sie können mit jeder Schnittstelle eine abstrakte Grundimplementierungsklasse bereitstellen.Dies vereint die Vorteile von Schnittstellen und abstrakten Klassen.Skelettimplementierungen bieten Implementierungsunterstützung, ohne die strengen Einschränkungen aufzuerlegen, die abstrakte Klassen erzwingen, wenn sie als Typdefinitionen dienen.Zum Beispiel die Sammlungsrahmen Definiert den Typ mithilfe von Schnittstellen und stellt für jede eine Skelettimplementierung bereit.

Die Programmierung auf Schnittstellen bietet mehrere Vorteile:

  1. Erforderlich für Muster vom Typ GoF, z. B. das Besuchermuster

  2. Ermöglicht alternative Implementierungen.Beispielsweise können mehrere Datenzugriffsobjektimplementierungen für eine einzelne Schnittstelle vorhanden sein, die die verwendete Datenbank-Engine abstrahiert (AccountDaoMySQL und AccountDaoOracle können beide AccountDao implementieren).

  3. Eine Klasse kann mehrere Schnittstellen implementieren.Java erlaubt keine Mehrfachvererbung konkreter Klassen.

  4. Zusammenfassungen der Implementierungsdetails.Schnittstellen dürfen nur öffentliche API-Methoden enthalten und Implementierungsdetails verbergen.Zu den Vorteilen gehören eine sauber dokumentierte öffentliche API und gut dokumentierte Verträge.

  5. Wird häufig von modernen Dependency-Injection-Frameworks verwendet, z http://www.springframework.org/.

  6. In Java können Schnittstellen zum Erstellen dynamischer Proxys verwendet werden - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html.Dies kann sehr effektiv mit Frameworks wie Spring genutzt werden, um aspektorientierte Programmierung durchzuführen.Aspekte können Klassen sehr nützliche Funktionen hinzufügen, ohne diesen Klassen direkt Java-Code hinzuzufügen.Beispiele für diese Funktionalität sind Protokollierung, Prüfung, Leistungsüberwachung, Transaktionsabgrenzung usw. http://static.springframework.org/spring/docs/2.5.x/reference/aop.html.

  7. Scheinimplementierungen, Komponententests – Wenn abhängige Klassen Implementierungen von Schnittstellen sind, können Scheinklassen geschrieben werden, die diese Schnittstellen auch implementieren.Die Scheinklassen können verwendet werden, um Unit-Tests zu erleichtern.

Ich denke, einer der Gründe, warum abstrakte Klassen von Entwicklern weitgehend aufgegeben wurden, könnte ein Missverständnis sein.

Wenn das Gruppe von vier schrieb:

Programm zu einer Schnittstelle, keine Implementierung.

So etwas wie eine Java- oder C#-Schnittstelle gab es nicht.Sie sprachen über das objektorientierte Schnittstellenkonzept, das jede Klasse hat.Erich Gamma erwähnt es in dieses Interview.

Meiner Meinung nach führt die mechanische Befolgung aller Regeln und Prinzipien zu einer schwierig zu lesenden, zu navigierenden, zu verstehenden und zu verwaltenden Codebasis.Erinnern:Das Einfachste, was überhaupt funktionieren könnte.

Wie kommts?

Denn das steht in allen Büchern.Wie die GoF-Muster sehen viele Leute es als allgemein gut an und denken nie darüber nach, ob es wirklich das richtige Design ist oder nicht.

Woher kennen Sie alle Beziehungen zwischen Objekten, die innerhalb dieser Schnittstelle auftreten?

Das tun Sie nicht, und das ist ein Problem.

Wenn Du kennst diese Beziehungen bereits, Warum dann nicht einfach ein Abstract verlängern Klasse?

Gründe, eine abstrakte Klasse nicht zu erweitern:

  1. Sie haben völlig unterschiedliche Implementierungen und die Erstellung einer anständigen Basisklasse ist zu schwierig.
  2. Sie müssen Ihre einzige Basisklasse für etwas anderes verbrennen.

Wenn beides nicht zutrifft, fahren Sie fort und verwenden Sie eine abstrakte Klasse.Es wird Ihnen viel Zeit sparen.

Fragen, die Sie nicht gestellt haben:

Welche Nachteile hat die Verwendung einer Schnittstelle?

Sie können sie nicht ändern.Im Gegensatz zu einer abstrakten Klasse ist eine Schnittstelle in Stein gemeißelt.Sobald Sie eines verwenden, wird durch die Erweiterung der Code beschädigt, Punkt.

Brauche ich beides wirklich?

Meistens nein.Denken Sie gründlich nach, bevor Sie eine Objekthierarchie erstellen.Ein großes Problem bei Sprachen wie Java besteht darin, dass es viel zu einfach ist, riesige, komplizierte Objekthierarchien zu erstellen.

Betrachten Sie das klassische Beispiel, das LameDuck von Duck erbt.Klingt einfach, nicht wahr?

Nun, bis Sie angeben müssen, dass die Ente verletzt wurde und nun lahm ist.Oder zeigen Sie an, dass die lahme Ente geheilt ist und wieder laufen kann.In Java ist es nicht möglich, den Typ eines Objekts zu ändern. Daher funktioniert die Verwendung von Untertypen zur Anzeige von Lahmheit eigentlich nicht.

Programmieren auf eine Schnittstelle bedeutet, den "Vertrag" zu respektieren, der durch Verwenden dieser Schnittstelle

Dies ist das am meisten missverstandene Thema an Schnittstellen.

Es gibt keine Möglichkeit, einen solchen Vertrag mit Schnittstellen durchzusetzen.Schnittstellen können per Definition überhaupt kein Verhalten spezifizieren.Im Unterricht geschieht Verhalten.

Dieser Irrglaube ist so weit verbreitet, dass er von vielen Menschen als gängige Meinung angesehen wird.Es ist jedoch falsch.

Also diese Aussage im OP

Fast jedes Java-Buch, das ich gelesen habe, spricht von der Verwendung der Benutzeroberfläche als So geben Sie Zustand und Verhalten zwischen Objekten frei

ist einfach nicht möglich.Schnittstellen haben weder einen Zustand noch ein Verhalten.Sie können Eigenschaften definieren, die implementierende Klassen bereitstellen müssen, aber das ist das Beste, was ihnen möglich ist.Sie können Verhalten nicht über Schnittstellen teilen.

Sie können davon ausgehen, dass Menschen eine Schnittstelle implementieren, um das Verhalten bereitzustellen, das der Name ihrer Methoden impliziert, aber das ist nicht annähernd dasselbe.Und es gibt überhaupt keine Einschränkungen, wann solche Methoden aufgerufen werden (z. B. dass Start vor Stop aufgerufen werden sollte).

Diese Aussage

Erforderlich für Muster vom Typ GoF, z. B. das Besuchermuster

ist auch falsch.Das GoF-Buch verwendet genau keine Schnittstellen, da diese in den damals verwendeten Sprachen nicht vorhanden waren.Keines der Muster erfordert Schnittstellen, obwohl einige diese verwenden können.Meiner Meinung nach ist das Observer-Muster eines, bei dem Schnittstellen eine elegantere Rolle spielen können (obwohl das Muster heutzutage normalerweise mithilfe von Ereignissen implementiert wird).Im Besuchermuster ist fast immer eine Basis-Besucherklasse erforderlich, die das Standardverhalten für jeden Typ von besuchten Knoten implementiert, IME.

Persönlich denke ich, dass die Antwort auf die Frage dreifach ist:

  1. Schnittstellen werden von vielen als Allheilmittel betrachtet (diese Leute hegen meist dem „Vertrags“-Missverständnis oder denken, dass Schnittstellen ihren Code auf magische Weise entkoppeln).

  2. Java-Leute konzentrieren sich sehr auf die Verwendung von Frameworks, von denen viele (zu Recht) Klassen benötigen, um ihre Schnittstellen zu implementieren

  3. Bevor Generics und Annotationen (Attribute in C#) eingeführt wurden, waren Schnittstellen die beste Möglichkeit, einige Dinge zu erledigen.

Schnittstellen sind eine sehr nützliche Sprachfunktion, werden jedoch häufig missbraucht.Zu den Symptomen gehören:

  1. Eine Schnittstelle wird nur von einer Klasse implementiert

  2. Eine Klasse implementiert mehrere Schnittstellen.Oft als Vorteil von Schnittstellen angepriesen, bedeutet dies in der Regel, dass die betreffende Klasse gegen das Prinzip der Trennung von Belangen verstößt.

  3. Es gibt eine Vererbungshierarchie von Schnittstellen (häufig gespiegelt durch eine Hierarchie von Klassen).Dies ist die Situation, die Sie durch die Verwendung von Schnittstellen vermeiden möchten.Zu viel Vererbung ist eine schlechte Sache, sowohl für Klassen als auch für Schnittstellen.

All diese Dinge sind Code-Gerüche, meiner Meinung nach.

Es ist eine Möglichkeit, Loose zu fördern Kupplung.

Bei geringer Kopplung erfordert eine Änderung in einem Modul keine Änderung in der Implementierung eines anderen Moduls.

Eine gute Verwendung dieses Konzepts ist Abstraktes Fabrikmuster.Im Wikipedia-Beispiel erzeugt die GUIFactory-Schnittstelle eine Button-Schnittstelle.Die konkrete Fabrik kann WinFactory (die WinButton produziert) oder OSXFactory (die OSXButton produziert) sein.Stellen Sie sich vor, Sie schreiben eine GUI-Anwendung und müssen sich alle Instanzen davon ansehen OldButton Klasse und ändern Sie sie in WinButton.Dann müssen Sie nächstes Jahr hinzufügen OSXButton Ausführung.

Meiner Meinung nach sieht man das so oft, weil es eine sehr gute Praxis ist, die oft in den falschen Situationen angewendet wird.

Schnittstellen bieten im Vergleich zu abstrakten Klassen viele Vorteile:

  • Sie können Implementierungen wechseln, ohne Code neu zu erstellen, der von der Schnittstelle abhängt.Dies ist nützlich für:Proxy-Klassen, Abhängigkeitsinjektion, AOP usw.
  • Sie können die API von der Implementierung in Ihrem Code trennen.Das kann nützlich sein, weil es deutlich macht, wenn Sie Code ändern, der sich auf andere Module auswirkt.
  • Damit können Entwickler, die Code schreiben, der von Ihrem Code abhängt, Ihre API zu Testzwecken einfach nachahmen.

Den größten Nutzen ziehen Sie aus Schnittstellen, wenn Sie mit Codemodulen arbeiten.Allerdings gibt es keine einfache Regel, um zu bestimmen, wo Modulgrenzen liegen sollten.Daher kommt es leicht vor, dass diese bewährte Vorgehensweise übermäßig häufig verwendet wird, insbesondere wenn man zum ersten Mal Software entwickelt.

Ich würde (mit @eed3s9n) davon ausgehen, dass es die lose Kopplung fördern soll.Ohne Schnittstellen werden Unit-Tests außerdem viel schwieriger, da Sie Ihre Objekte nicht nachbilden können.

Warum sich ausdehnt, ist böse.Dieser Artikel ist so ziemlich eine direkte Antwort auf die gestellte Frage.Ich kann mir fast keinen Fall vorstellen, in dem Sie das tatsächlich tun würden brauchen eine abstrakte Klasse und viele Situationen, in denen es eine schlechte Idee ist.Das bedeutet nicht, dass Implementierungen, die abstrakte Klassen verwenden, schlecht sind, aber Sie müssen darauf achten, dass Sie den Schnittstellenvertrag nicht von Artefakten einer bestimmten Implementierung abhängig machen (Beispiel:die Stack-Klasse in Java).

Eine Sache noch:Es ist nicht notwendig oder empfehlenswert, überall Schnittstellen zu haben.Normalerweise sollten Sie feststellen, wann Sie eine Schnittstelle benötigen und wann nicht.In einer idealen Welt sollte der zweite Fall die meiste Zeit als letzte Klasse implementiert werden.

Hier gibt es einige hervorragende Antworten, aber wenn Sie nach einem konkreten Grund suchen, sind Sie bei Unit Testing genau richtig.

Bedenken Sie, dass Sie eine Methode in der Geschäftslogik testen möchten, die den aktuellen Steuersatz für die Region abruft, in der eine Transaktion stattfindet.Dazu muss die Business-Logik-Klasse über ein Repository mit der Datenbank kommunizieren:

interface IRepository<T> { T Get(string key); }

class TaxRateRepository : IRepository<TaxRate> {
    protected internal TaxRateRepository() {}
    public TaxRate Get(string key) {
    // retrieve an TaxRate (obj) from database
    return obj; }
}

Verwenden Sie im gesamten Code den Typ IRepository anstelle von TaxRateRepository.

Das Repository verfügt über einen nicht öffentlichen Konstruktor, der Benutzer (Entwickler) dazu ermutigt, die Factory zum Instanziieren des Repositorys zu verwenden:

public static class RepositoryFactory {

    public RepositoryFactory() {
        TaxRateRepository = new TaxRateRepository(); }

    public static IRepository TaxRateRepository { get; protected set; }
    public static void SetTaxRateRepository(IRepository rep) {
        TaxRateRepository = rep; }
}

Die Factory ist der einzige Ort, an dem direkt auf die TaxRateRepository-Klasse verwiesen wird.

Für dieses Beispiel benötigen Sie also einige unterstützende Klassen:

class TaxRate {
    public string Region { get; protected set; }
    decimal Rate { get; protected set; }
}

static class Business {
    static decimal GetRate(string region) { 
        var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
        return taxRate.Rate; }
}

Und es gibt noch eine weitere Implementierung von IRepository – das Mock-up:

class MockTaxRateRepository : IRepository<TaxRate> {
    public TaxRate ReturnValue { get; set; }
    public bool GetWasCalled { get; protected set; }
    public string KeyParamValue { get; protected set; }
    public TaxRate Get(string key) {
        GetWasCalled = true;
        KeyParamValue = key;
        return ReturnValue; }
}

Da der Live-Code (Business Class) eine Factory verwendet, um das Repository abzurufen, schließen Sie im Komponententest das MockRepository für das TaxRateRepository an.Sobald die Ersetzung erfolgt ist, können Sie den Rückgabewert fest codieren und die Datenbank überflüssig machen.

class MyUnitTestFixture { 
    var rep = new MockTaxRateRepository();

    [FixtureSetup]
    void ConfigureFixture() {
        RepositoryFactory.SetTaxRateRepository(rep); }

    [Test]
    void Test() {
        var region = "NY.NY.Manhattan";
        var rate = 8.5m;
        rep.ReturnValue = new TaxRate { Rate = rate };

        var r = Business.GetRate(region);
        Assert.IsNotNull(r);
        Assert.IsTrue(rep.GetWasCalled);
        Assert.AreEqual(region, rep.KeyParamValue);
        Assert.AreEqual(r.Rate, rate); }
}

Denken Sie daran, dass Sie nur die Geschäftslogikmethode testen möchten, nicht das Repository, die Datenbank, die Verbindungszeichenfolge usw.Für jeden davon gibt es unterschiedliche Tests.Auf diese Weise können Sie den Code, den Sie testen, vollständig isolieren.

Ein Nebeneffekt besteht darin, dass Sie den Unit-Test auch ohne Datenbankverbindung ausführen können, was ihn schneller und portabler macht (denken Sie an ein Team mit mehreren Entwicklern an entfernten Standorten).

Ein weiterer Nebeneffekt besteht darin, dass Sie den Test-Driven Development (TDD)-Prozess für die Implementierungsphase der Entwicklung nutzen können.Ich verwende nicht unbedingt TDD, sondern eine Mischung aus TDD und altmodischer Codierung.

In gewisser Weise denke ich, dass Ihre Frage einfach darauf hinausläuft: "Warum Schnittstellen und keine abstrakten Klassen verwenden?" Technisch gesehen können Sie mit beiden eine lose Kopplung erreichen – die zugrunde liegende Implementierung ist immer noch nicht für den aufrufenden Code verfügbar, und Sie können das abstrakte Factory-Muster verwenden, um eine zugrunde liegende Implementierung zurückzugeben (Schnittstellenimplementierung vs.abstrakte Klassenerweiterung), um die Flexibilität Ihres Designs zu erhöhen.Tatsächlich könnte man argumentieren, dass abstrakte Klassen Ihnen etwas mehr bieten, da sie es Ihnen ermöglichen, sowohl Implementierungen zu erfordern, um Ihren Code zu erfüllen („Sie MÜSSEN start() implementieren“), als auch Standardimplementierungen bereitzustellen („Ich habe eine Standard-Paint() für Sie“) kann überschreiben, wenn Sie möchten“) – bei Schnittstellen müssen Implementierungen bereitgestellt werden, was im Laufe der Zeit zu spröden Vererbungsproblemen durch Schnittstellenänderungen führen kann.

Grundsätzlich verwende ich Schnittstellen jedoch hauptsächlich aufgrund der Einzelvererbungsbeschränkung von Java.Wenn meine Implementierung von einer abstrakten Klasse erben MUSS, um vom Aufrufcode verwendet zu werden, bedeutet das, dass ich die Flexibilität verliere, von etwas anderem zu erben, auch wenn das möglicherweise sinnvoller ist (z. B.für Code-Wiederverwendung oder Objekthierarchie).

Ein Grund dafür ist, dass Schnittstellen Wachstum und Erweiterbarkeit ermöglichen.Angenommen, Sie haben eine Methode, die ein Objekt als Parameter akzeptiert.

öffentliches leeres Getränk (Kaffee etwasTrinken) {

}

Nehmen wir nun an, Sie möchten genau dieselbe Methode verwenden, aber ein hotTea-Objekt übergeben.Nun, das kannst du nicht.Sie haben die obige Methode gerade so fest codiert, dass nur Kaffeeobjekte verwendet werden.Vielleicht ist das gut, vielleicht ist das schlecht.Der Nachteil des oben Gesagten besteht darin, dass Sie strikt an einen Objekttyp gebunden sind, wenn Sie alle möglichen verwandten Objekte übergeben möchten.

Durch die Verwendung einer Schnittstelle, sagen wir IHotDrink,

Schnittstelle IHotDrink { }

und Ihre obige Methode umschreiben, um die Schnittstelle anstelle des Objekts zu verwenden,

öffentliches leeres Getränk (IHotDrink etwasDrink) {

}

Jetzt können Sie alle Objekte übergeben, die die IHotDrink-Schnittstelle implementieren.Sicher, Sie können genau dieselbe Methode schreiben, die genau dasselbe mit einem anderen Objektparameter tut, aber warum?Sie pflegen plötzlich aufgeblähten Code.

Es geht ums Entwerfen vor dem Codieren.

Wenn Sie nicht alle Beziehungen zwischen zwei Objekten kennen, nachdem Sie die Schnittstelle angegeben haben, haben Sie bei der Definition der Schnittstelle schlechte Arbeit geleistet – was relativ einfach zu beheben ist.

Wenn Sie sich direkt mit dem Codieren beschäftigt haben und auf halbem Weg gemerkt haben, dass Ihnen etwas fehlt, ist es viel schwieriger, es zu beheben.

Sie könnten dies aus einer Perl/Python/Ruby-Perspektive sehen:

  • Wenn Sie ein Objekt als Parameter an eine Methode übergeben, übergeben Sie nicht seinen Typ, sondern wissen nur, dass es auf einige Methoden reagieren muss

Ich denke, die Betrachtung von Java-Schnittstellen als Analogie dazu würde dies am besten erklären.Sie übergeben nicht wirklich einen Typ, sondern nur etwas, das auf eine Methode reagiert (ein Merkmal, wenn Sie so wollen).

Ich denke, der Hauptgrund für die Verwendung von Schnittstellen in Java ist die Beschränkung auf Einzelvererbung.In vielen Fällen führt dies zu unnötigen Komplikationen und Codeduplizierungen.Schauen Sie sich Traits in Scala an: http://www.scala-lang.org/node/126 Merkmale sind eine besondere Art abstrakter Klassen, aber eine Klasse kann viele davon erweitern.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top