Warum sind Schnittstellen für das Erreichen einer losen Kopplung hilfreicher als Superklassen?

15

( Im Sinne dieser Frage meine ichinterface , wenn ich 'Schnittstelle' sage, das Sprachkonstrukt und nicht eine 'Schnittstelle' im anderen Sinne des Wortes, dh die öffentlichen Methoden, die eine Klasse der Außenwelt bietet, um mit und zu kommunizieren manipuliere es. )

Eine lose Kopplung kann erreicht werden, indem ein Objekt von einer Abstraktion anstelle eines konkreten Typs abhängt.

Dies ermöglicht eine lose Kopplung aus zwei Hauptgründen: 1) Abstraktionen ändern sich weniger wahrscheinlich als konkrete Typen, was bedeutet, dass der abhängige Code weniger wahrscheinlich beschädigt wird. Zur Laufzeit können 2 verschiedene konkrete Typen verwendet werden, da sie alle zur Abstraktion passen. Neue Betontypen können auch später hinzugefügt werden, ohne dass der vorhandene abhängige Code geändert werden muss.

Betrachten Sie beispielsweise eine Klasse Carund zwei Unterklassen Volvound Mazda.

Wenn Ihr Code von a abhängt Car, kann er zur Laufzeit entweder a Volvooder a verwenden Mazda. Auch später könnten zusätzliche Unterklassen hinzugefügt werden, ohne dass der abhängige Code geändert werden muss.

Außerdem Carist es weniger wahrscheinlich , dass sich - was eine Abstraktion ist - etwas ändert als Volvooder Mazda. Autos sind im Allgemeinen seit geraumer Zeit gleich, aber Volvos und Mazdas ändern sich mit größerer Wahrscheinlichkeit. Dh Abstraktionen sind stabiler als konkrete Typen.

All dies sollte zeigen, dass ich verstehe, was lose Kopplung ist und wie sie erreicht wird, wenn man von Abstraktionen und nicht von Konkretionen abhängt. (Wenn ich etwas falsches geschrieben habe, sagen Sie es bitte).

Was ich nicht verstehe, ist Folgendes:

Abstraktionen können Superklassen oder Interfaces sein.

Wenn ja, warum werden Schnittstellen speziell für ihre Fähigkeit gelobt, lose Kopplungen zuzulassen? Ich verstehe nicht, wie es sich von der Verwendung einer Superklasse unterscheidet.

Die einzigen Unterschiede, die ich sehe, sind: 1- Schnittstellen sind nicht durch einzelne Vererbung beschränkt, aber das hat nicht viel mit dem Thema der losen Kopplung zu tun. 2- Schnittstellen sind abstrakter, da sie überhaupt keine Implementierungslogik haben. Trotzdem verstehe ich nicht, warum das einen so großen Unterschied macht.

Erklären Sie mir bitte, warum Schnittstellen für das Ermöglichen einer losen Kopplung großartig sind, einfache Superklassen dagegen nicht.

Aviv Cohn
quelle
3
Die meisten Sprachen (z. B. Java, C #) mit "Schnittstellen" unterstützen nur die einfache Vererbung. Da jede Klasse nur eine unmittelbare Superklasse haben kann, sind (abstrakte) Superklassen zu begrenzt, damit ein Objekt mehrere Abstraktionen unterstützt. Suchen Sie nach Merkmalen (z. B. Scala oder Perl's Roles ) für eine moderne Alternative, die auch das „ Diamantproblem “ bei Mehrfachvererbung vermeidet .
Amon
@amon Sie sagen also, dass der Vorteil von Schnittstellen gegenüber abstrakten Klassen beim Versuch, eine lose Kopplung zu erreichen, darin besteht, dass sie nicht durch eine einzelne Vererbung beschränkt sind?
Aviv Cohn
Nein, ich meinte teuer in Bezug auf den Compiler hat mehr zu tun , wenn es eine abstrakte Klasse behandelt , aber dies könnte wahrscheinlich vernachlässigt werden.
pastöser
2
Es sieht aus wie @amon auf dem richtigen Weg ist, fand ich diesen Beitrag , wo gesagt wird , dass: interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class(die mich auf den Vergleich mit C ++ leeds, wo Schnittstellen nur Klassen mit rein virtuellen Funktionen sind).
pastöser
Sagen Sie bitte, wer sagt, dass Superklassen schlecht sind.
Tulains Córdova

Antworten:

11

Terminologie: Ich beziehe mich auf das Sprachkonstrukt interfaceals Schnittstelle und auf die Schnittstelle eines Typs oder Objekts als Oberfläche (mangels eines besseren Begriffs).

Eine lose Kopplung kann erreicht werden, indem ein Objekt von einer Abstraktion anstelle eines konkreten Typs abhängt.

Richtig.

Dies ermöglicht eine lose Kopplung aus zwei Hauptgründen: 1 - Abstraktionen ändern sich mit geringerer Wahrscheinlichkeit als konkrete Typen, was bedeutet, dass der abhängige Code mit geringerer Wahrscheinlichkeit beschädigt wird. 2 - Zur Laufzeit können verschiedene Betontypen verwendet werden, da sie alle zur Abstraktion passen. Neue Betontypen können auch später hinzugefügt werden, ohne dass der vorhandene abhängige Code geändert werden muss.

Nicht ganz richtig. Gegenwärtige Sprachen erwarten im Allgemeinen nicht, dass sich eine Abstraktion ändert (obwohl es einige Entwurfsmuster gibt, die damit umgehen). Das Trennen von Besonderheiten von allgemeinen Dingen ist Abstraktion. Dies geschieht normalerweise durch eine Abstraktionsebene . Diese Ebene kann in einige andere Details geändert werden, ohne den Code zu beschädigen, der auf dieser Abstraktion aufbaut - es wird eine lose Kopplung erreicht. Non-OOP-Beispiel: Eine sortRoutine wird möglicherweise von Quicksort in Version 1 in Tim Sort in Version 2 geändert. Code, der nur vom zu sortierenden Ergebnis abhängt (dh auf der sortAbstraktion aufbaut ), wird daher von der eigentlichen Sortierungsimplementierung abgekoppelt.

Was ich oben als Oberfläche bezeichnet habe, ist der allgemeine Teil einer Abstraktion. In OOP kommt es jetzt vor, dass ein Objekt manchmal mehrere Abstraktionen unterstützen muss. Ein nicht ganz optimales Beispiel: Java java.util.LinkedListunterstützt sowohl die ListSchnittstelle, bei der es um die Abstraktion "geordnete, indexierbare Sammlung" geht, als auch die QueueSchnittstelle, bei der es sich (grob gesagt) um die Abstraktion "FIFO" handelt.

Wie kann ein Objekt mehrere Abstraktionen unterstützen?

C ++ hat keine Schnittstellen, aber mehrere Vererbungen, virtuelle Methoden und abstrakte Klassen. Eine Abstraktion kann dann als eine abstrakte Klasse (dh eine Klasse, die nicht sofort instanziiert werden kann) definiert werden, die virtuelle Methoden deklariert, aber nicht definiert. Klassen, die die Besonderheiten einer Abstraktion implementieren, können dann von dieser abstrakten Klasse erben und die erforderlichen virtuellen Methoden implementieren.

Das Problem hierbei ist, dass Mehrfachvererbung zum Diamantproblem führen kann , wobei die Reihenfolge, in der Klassen nach einer Methodenimplementierung durchsucht werden (MRO: method resolution order), zu „Widersprüchen“ führen kann. Darauf gibt es zwei Antworten:

  1. Definieren Sie eine vernünftige Reihenfolge und lehnen Sie Bestellungen ab, die nicht sinnvoll linearisiert werden können. Der C3 MRO ist ziemlich vernünftig und funktioniert gut. Es wurde 1996 veröffentlicht.

  2. Nehmen Sie den einfachen Weg und lehnen Sie die Mehrfachvererbung ab.

Java entschied sich für die letztere Option und entschied sich für eine einzelne Verhaltensvererbung. Wir benötigen jedoch weiterhin die Fähigkeit eines Objekts, mehrere Abstraktionen zu unterstützen. Daher müssen Schnittstellen verwendet werden, die keine Methodendefinitionen, sondern nur Deklarationen unterstützen.

Das Ergebnis ist, dass die MRO offensichtlich ist (sehen Sie sich jede Superklasse in der richtigen Reihenfolge an) und dass unser Objekt mehrere Oberflächen für eine beliebige Anzahl von Abstraktionen haben kann.

Dies stellt sich als eher unbefriedigend heraus, da ziemlich oft ein bisschen Verhalten Teil der Oberfläche ist. Betrachten Sie eine ComparableSchnittstelle:

interface Comparable<T> {
    public int cmp(T that);
    public boolean lt(T that);  // less than
    public boolean le(T that);  // less than or equal
    public boolean eq(T that);  // equal
    public boolean ne(T that);  // not equal
    public boolean ge(T that);  // greater than or equal
    public boolean gt(T that);  // greater than
}

Dies ist sehr benutzerfreundlich (eine nette API mit vielen praktischen Methoden), aber mühsam zu implementieren. Wir möchten, dass die Schnittstelle nur cmpdie anderen Methoden in Bezug auf diese eine erforderliche Methode einschließt und automatisch implementiert. Mixins , aber vor allem Traits [ 1 ], [ 2 ] lösen dieses Problem, ohne in die Fallen der Mehrfachvererbung zu geraten.

Dies erfolgt durch die Definition einer Merkmalszusammensetzung, sodass die Merkmale nicht tatsächlich am MRO teilnehmen, sondern die definierten Methoden in der implementierenden Klasse zusammengefasst werden.

Die ComparableSchnittstelle könnte in Scala als ausgedrückt werden

trait Comparable[T] {
    def cmp(that: T): Int
    def lt(that: T): Boolean = this.cmp(that) <  0
    def le(that: T): Boolean = this.cmp(that) <= 0
    ...
}

Wenn eine Klasse dieses Merkmal verwendet, werden die folgenden Methoden zur Klassendefinition hinzugefügt:

// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
    override def cmp(that: Inty) = this.x - that.x
    // lt etc. get added automatically
}

So Inty(4) cmp Inty(6)wäre -2und Inty(4) lt Inty(6)wäre true.

In vielen Sprachen werden bestimmte Merkmale unterstützt, und in jeder Sprache, die über ein „Metaobject Protocol (MOP)“ verfügt, können Merkmale hinzugefügt werden. Das jüngste Java 8-Update fügte Standardmethoden hinzu, die den Merkmalen ähneln (Methoden in Schnittstellen können Fallback-Implementierungen aufweisen, sodass die Implementierung dieser Methoden durch Klassen optional ist).

Leider handelt es sich bei Merkmalen um eine relativ neue Erfindung (2002), die in den größeren Hauptsprachen daher eher selten vorkommt.

amon
quelle
Gute Antwort, aber ich möchte hinzufügen, dass Sprachen mit einfacher Vererbung die Mehrfachvererbung unter Verwendung von Schnittstellen mit Komposition manipulieren können .
4

Was ich nicht verstehe, ist Folgendes:

Abstraktionen können Superklassen oder Interfaces sein.

Wenn ja, warum werden Schnittstellen speziell für ihre Fähigkeit gelobt, lose Kopplungen zuzulassen? Ich verstehe nicht, wie es sich von der Verwendung einer Superklasse unterscheidet.

Erstens sind Subtypisierung und Abstraktion zwei verschiedene Dinge. Subtypisierung bedeutet lediglich, dass ich Werte eines Typs durch Werte eines anderen Typs ersetzen kann - keiner der Typen muss abstrakt sein.

Vor allem haben Unterklassen eine direkte Abhängigkeit von den Implementierungsdetails ihrer Oberklasse. Das ist die stärkste Kopplung, die es gibt. Wenn die Basisklasse nicht unter Berücksichtigung der Vererbung entworfen wurde, können Änderungen an der Basisklasse, die ihr Verhalten nicht ändern, Unterklassen dennoch beschädigen, und es gibt keine Möglichkeit, a priori zu ermitteln, ob eine Beschädigung auftritt. Dies ist als das fragile Basisklassenproblem bekannt .

Das Implementieren einer Schnittstelle koppelt Sie an nichts anderes als an die Schnittstelle selbst, die kein Verhalten enthält.

Doval
quelle
Danke für die Antwort. Um zu sehen, ob ich verstehe: Wenn Sie möchten, dass ein Objekt mit dem Namen A von einer Abstraktion mit dem Namen B abhängt, anstatt von einer konkreten Implementierung dieser Abstraktion mit dem Namen C, ist es häufig besser, wenn B eine von C implementierte Schnittstelle ist, als eine durch erweiterte Superklasse C. Dies liegt daran, dass: C die Unterklasse B fest mit B verbindet. Wenn sich B ändert, ändert sich C. C, das B implementiert (B ist eine Schnittstelle), koppelt B jedoch nicht mit C: B ist nur eine Liste von Methoden, die C implementieren muss, also keine enge Kopplung. In Bezug auf Objekt A (das abhängige Objekt) spielt es jedoch keine Rolle, ob B eine Klasse oder eine Schnittstelle ist.
Aviv Cohn
Richtig? ..... ....
Aviv Cohn
Warum sollte eine Schnittstelle an irgendetwas gekoppelt sein?
Michael Shaw
Ich denke, diese Antwort trifft es auf den Kopf. Ich benutze C ++ ziemlich oft, und wie in einer der anderen Antworten angegeben, hat C ++ keine Schnittstellen, aber Sie fälschen es, indem Sie Superklassen mit allen Methoden verwenden, die als "rein virtuell" verbleiben (dh von Kindern implementiert werden). Der Punkt ist, dass es einfach ist, Basisklassen zu erstellen, die neben der delegierten Funktionalität auch etwas bewirken. In vielen, vielen Fällen stellen ich und meine Mitarbeiter fest, dass auf diese Weise ein neuer Anwendungsfall entsteht, der die gemeinsame Funktionalität ungültig macht. Wenn gemeinsame Funktionen benötigt werden, ist es einfach genug, eine Hilfsklasse zu erstellen.
J Trana
@Prog Ihre Überlegungen sind größtenteils richtig, aber auch hier sind Abstraktion und Subtypisierung zwei getrennte Dinge. Wenn Sie sagen you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named C, dass Sie annehmen, dass Klassen nicht abstrakt sind. Eine Abstraktion ist alles, was Implementierungsdetails verbirgt. Eine Klasse mit privaten Feldern ist also genauso abstrakt wie eine Schnittstelle mit denselben öffentlichen Methoden.
Doval
1

Es gibt eine Kopplung zwischen Eltern- und Kindklassen, da das Kind vom Elternteil abhängt.

Nehmen wir an, wir haben eine Klasse A und Klasse B erbt davon. Wenn wir in die Klasse A gehen und Dinge ändern, wird auch die Klasse B geändert.

Nehmen wir an, wir haben eine Schnittstelle I und Klasse B implementiert sie. Wenn wir die Schnittstelle I ändern, bleibt die Klasse B unverändert, obwohl Klasse B sie möglicherweise nicht mehr implementiert.

Michael Shaw
quelle
Ich bin gespannt, ob die Downvoter einen Grund hatten oder nur einen schlechten Tag hatten.
Michael Shaw
1
Ich habe nicht abgelehnt, aber ich denke, dass es mit dem ersten Satz zu tun haben könnte. Untergeordnete Klassen sind an übergeordnete Klassen gekoppelt, nicht umgekehrt. Der Elternteil braucht nichts über das Kind zu wissen, aber das Kind braucht vertrautes Wissen über den Elternteil.
@ JohnGaughan: Danke für das Feedback. Zur Verdeutlichung bearbeitet.
Michael Shaw