Terminologie: Ich beziehe mich auf das Sprachkonstrukt interface
als 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 sort
Routine 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 sort
Abstraktion 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.LinkedList
unterstützt sowohl die List
Schnittstelle, bei der es um die Abstraktion "geordnete, indexierbare Sammlung" geht, als auch die Queue
Schnittstelle, 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:
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.
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 Comparable
Schnittstelle:
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 cmp
die 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 Comparable
Schnittstelle 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 -2
und 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.