Entwerfen mit Schnittstellen

Eine der grundlegenden Aktivitäten eines Software-Systemdesigns besteht darin, die Schnittstellen zwischen den Komponenten des Systems zu definieren. Da Sie mit dem Java-Schnittstellenkonstrukt eine abstrakte Schnittstelle definieren können, ohne eine Implementierung anzugeben, besteht eine Hauptaktivität eines Java-Programmdesigns darin, "herauszufinden, was die Schnittstellen sind". Dieser Artikel befasst sich mit der Motivation hinter der Java-Oberfläche und enthält Richtlinien, wie Sie diesen wichtigen Teil von Java optimal nutzen können.

Schnittstelle entschlüsseln

Vor fast zwei Jahren schrieb ich ein Kapitel über die Java-Oberfläche und bat einige Freunde, die C ++ kennen, es zu überprüfen. In diesem Kapitel, das jetzt Teil meines Java-Kurslesers Inner Java ist (siehe Ressourcen), habe ich Schnittstellen hauptsächlich als eine spezielle Art der Mehrfachvererbung vorgestellt: Mehrfachvererbung der Schnittstelle (das objektorientierte Konzept) ohne Mehrfachvererbung der Implementierung. Eine Rezensentin sagte mir, dass sie, obwohl sie die Mechanik der Java-Oberfläche nach dem Lesen meines Kapitels verstanden habe, sie nicht wirklich verstanden habe. Wie genau, fragte sie mich, waren Javas Schnittstellen eine Verbesserung gegenüber dem Mehrfachvererbungsmechanismus von C ++? Zu der Zeit konnte ich ihre Frage nicht zu ihrer Zufriedenheit beantworten, vor allem, weil ich es damals nicht getan hatte.Ich habe den Punkt der Schnittstellen selbst nicht ganz verstanden.

Obwohl ich eine ganze Weile mit Java arbeiten musste, bevor ich das Gefühl hatte, die Bedeutung der Schnittstelle erklären zu können, bemerkte ich sofort einen Unterschied zwischen der Java-Schnittstelle und der Mehrfachvererbung von C ++. Vor dem Aufkommen von Java habe ich fünf Jahre lang in C ++ programmiert, und in all dieser Zeit hatte ich noch nie eine Mehrfachvererbung verwendet. Mehrfachvererbung war nicht genau gegen meine Religion, ich bin einfach nie auf eine C ++ - Design-Situation gestoßen, in der ich es für sinnvoll hielt. Als ich anfing, mit Java zu arbeiten, fiel mir zuerst auf, wie oft sie für Schnittstellen nützlich waren. Im Gegensatz zur Mehrfachvererbung in C ++, die ich in fünf Jahren nie verwendet habe, habe ich ständig die Java-Schnittstellen verwendet.

Angesichts der Häufigkeit, mit der ich Schnittstellen nützlich fand, als ich anfing, mit Java zu arbeiten, wusste ich, dass etwas los war. Aber was genau? Könnte die Java-Schnittstelle ein inhärentes Problem der traditionellen Mehrfachvererbung lösen? War die Mehrfachvererbung der Schnittstelle an sich irgendwie besser als die einfache, alte Mehrfachvererbung?

Schnittstellen und das "Diamantproblem"

Eine Rechtfertigung für Schnittstellen, die ich schon früh gehört hatte, war, dass sie das "Diamantproblem" der traditionellen Mehrfachvererbung lösten. Das Diamantproblem ist eine Mehrdeutigkeit, die auftreten kann, wenn eine Klassenmultiplikation von zwei Klassen erbt, die beide von einer gemeinsamen Oberklasse abstammen. Zum Beispiel in Michael Crichtons Roman Jurassic Park,Wissenschaftler kombinieren Dinosaurier-DNA mit DNA von modernen Fröschen, um ein Tier zu erhalten, das einem Dinosaurier ähnelt, sich aber in gewisser Weise wie ein Frosch verhält. Am Ende des Romans stolpern die Helden der Geschichte über Dinosaurier-Eier. Die Dinosaurier, die alle weiblich geschaffen wurden, um eine Verbrüderung in freier Wildbahn zu verhindern, vermehrten sich. Chrichton schrieb dieses Wunder der Liebe den Ausschnitten der Frosch-DNA zu, mit denen die Wissenschaftler fehlende Teile der Dinosaurier-DNA ausgefüllt hatten. In Froschpopulationen, die von einem Geschlecht dominiert werden, könnten einige Frösche des dominanten Geschlechts spontan ihr Geschlecht ändern, sagt Chrichton. (Obwohl dies eine gute Sache für das Überleben der Froschart zu sein scheint, muss es für die einzelnen beteiligten Frösche furchtbar verwirrend sein.) Die Dinosaurier im Jurassic Park hatten dieses spontane Geschlechtsumwandlungsverhalten versehentlich von ihrer Froschherkunft geerbt.mit tragischen Folgen.

Dieses Jurassic Park-Szenario könnte möglicherweise durch die folgende Vererbungshierarchie dargestellt werden:

Das Diamantproblem kann in Vererbungshierarchien wie der in Abbildung 1 gezeigten auftreten. Tatsächlich hat das Diamantproblem seinen Namen von der Diamantform einer solchen Vererbungshierarchie. Eine Möglichkeit, wie das Diamantproblem in der Jurassic Park- Hierarchie auftreten kann, besteht darin, dass beide Dinosaurund Frog, aber nicht Frogosaur, eine in deklarierte Methode überschreiben Animal. So könnte der Code aussehen, wenn Java die traditionelle Mehrfachvererbung unterstützt:

abstrakte Klasse Tier {

abstraktes void talk (); }}

Klasse Frosch erweitert Tier {

void talk () {

System.out.println ("Ribit, ribit."); }}

Klasse Dinosaurier erweitert Tier {

void talk () {System.out.println ("Oh, ich bin ein Dinosaurier und mir geht es gut ..."); }}

// (Dies wird natürlich nicht kompiliert, da Java // nur eine einzelne Vererbung unterstützt.) Klasse Frogosaur erweitert Frog, Dinosaur {}

Das Diamantproblem zeigt seinen hässlichen Kopf, wenn jemand versucht, talk()ein FrogosaurObjekt aus einer AnimalReferenz aufzurufen , wie in:

Tier Tier = neuer Frogosaurier (); animal.talk ();

Aufgrund der durch das Diamond-Problem verursachten Mehrdeutigkeit ist nicht klar, ob das Laufzeitsystem die Implementierung von Frogoder aufrufen soll . Wird ein Krächzen oder singen ?Dinosaurtalk()Frogosaur"Ribbit, Ribbit.""Oh, I'm a dinosaur and I'm okay..."

Das Diamantproblem würde auch entstehen, wenn Animaleine öffentliche Instanzvariable deklariert worden Frogosaurwäre , die dann von beiden Dinosaurund geerbt hätte Frog. Wenn Sie in einem FrogosaurObjekt auf diese Variable verweisen , welche Kopie der Variablen - Frog's oder Dinosaur' s - wird ausgewählt? Oder würde es vielleicht nur eine Kopie der Variablen in einem FrogosaurObjekt geben?

In Java lösen Schnittstellen all diese Unklarheiten, die durch das Diamantproblem verursacht werden. Über Schnittstellen ermöglicht Java die mehrfache Vererbung der Schnittstelle, jedoch nicht der Implementierung. Die Implementierung, die Instanzvariablen und Methodenimplementierungen enthält, wird immer einzeln vererbt. Infolgedessen wird es in Java niemals zu Verwirrung darüber kommen, welche geerbte Instanzvariable oder Methodenimplementierung verwendet werden soll.

Schnittstellen und Polymorphismus

In meinem Bestreben, die Benutzeroberfläche zu verstehen, ergab die Erklärung des Diamantproblems für mich einen Sinn, aber sie befriedigte mich nicht wirklich. Sicher, die Schnittstelle stellte Javas Art dar, mit dem Diamantproblem umzugehen, aber war das der Schlüsseleinblick in die Schnittstelle? Und wie hat mir diese Erklärung geholfen, die Verwendung von Schnittstellen in meinen Programmen und Designs zu verstehen?

Mit der Zeit begann ich zu glauben, dass es bei der Schlüsseleinsicht in die Schnittstelle weniger um Mehrfachvererbung als vielmehr um Polymorphismus ging (siehe die Erklärung dieses Begriffs unten). Über die Benutzeroberfläche können Sie den Polymorphismus in Ihren Designs besser nutzen, wodurch Sie Ihre Software flexibler gestalten können.

Letztendlich entschied ich, dass der "Punkt" der Schnittstelle war:

Die Java-Oberfläche bietet Ihnen mehr Polymorphismus als mit einfach vererbten Klassenfamilien, ohne die "Last" der Mehrfachvererbung der Implementierung.

Eine Auffrischung zum Polymorphismus

Dieser Abschnitt enthält eine kurze Auffrischung der Bedeutung von Polymorphismus. Wenn Sie mit diesem ausgefallenen Wort bereits vertraut sind, können Sie mit dem nächsten Abschnitt "Mehr Polymorphismus" fortfahren.

Polymorphismus bedeutet, eine Superklassenvariable zu verwenden, um auf ein Unterklassenobjekt zu verweisen. Betrachten Sie beispielsweise diese einfache Vererbungshierarchie und den Code:

abstrakte Klasse Tier {

abstraktes void talk (); }}

Klasse Hund erweitert Tier {

void talk () {System.out.println ("Woof!"); }}

Klasse Katze erweitert Tier {

void talk () {System.out.println ("Meow."); }}

In Anbetracht dieser Vererbungshierarchie können Sie mit Polymorphismus einen Verweis auf ein DogObjekt in einer Variablen vom Typ halten Animal, wie in:

Tier Tier = neuer Hund (); 

Das Wort Polymorphismus basiert auf griechischen Wurzeln, die "viele Formen" bedeuten. Hier hat eine Klasse viele Formen: die der Klasse und einer ihrer Unterklassen. Ein Animalkann beispielsweise wie eine Dogoder eine Catoder eine andere Unterklasse von aussehen Animal.

Polymorphismus in Java wird durch dynamische Bindung ermöglicht, den Mechanismus, mit dem die Java Virtual Machine (JVM) eine aufzurufende Methodenimplementierung basierend auf dem Methodendeskriptor (Name der Methode sowie Anzahl und Typ ihrer Argumente) und der Klasse der auswählt Objekt, für das die Methode aufgerufen wurde. Die makeItTalk()unten gezeigte Methode akzeptiert beispielsweise eine AnimalReferenz als Parameter und ruft talk()diese Referenz auf:

Klasse Interrogator {

statische Leere makeItTalk (Tier Betreff) {subject.talk (); }}

Zur Kompilierungszeit weiß der Compiler nicht genau, an welche Objektklasse zur makeItTalk()Laufzeit übergeben wird. Es ist nur bekannt, dass das Objekt eine Unterklasse von sein wird Animal. Außerdem weiß der Compiler nicht genau, welche Implementierung von talk()zur Laufzeit aufgerufen werden soll.

Wie oben erwähnt, bedeutet dynamische Bindung, dass die JVM zur Laufzeit entscheidet, welche Methode basierend auf der Klasse des Objekts aufgerufen werden soll. Wenn das Objekt a ist Dog, ruft die JVM die DogImplementierung der Methode auf "Woof!". Wenn das Objekt a ist Cat, ruft die JVM die CatImplementierung der Methode auf "Meow!". Dynamische Bindung ist der Mechanismus, der Polymorphismus, die "Subsitutierbarkeit" einer Unterklasse für eine Oberklasse, ermöglicht.

Polymorphismus hilft dabei, Programme flexibler zu gestalten, da Sie zu einem späteren Zeitpunkt der AnimalFamilie eine weitere Unterklasse hinzufügen können und die makeItTalk()Methode weiterhin funktioniert. Wenn Sie beispielsweise später eine BirdKlasse hinzufügen :

Klasse Vogel erweitert Tier {

void talk () {

System.out.println ("Tweet, Tweet!"); }}

Sie können ein BirdObjekt an die unveränderte makeItTalk()Methode übergeben "Tweet, tweet!".

Mehr Polymorphismus bekommen

Schnittstellen bieten mehr Polymorphismus als einfach vererbte Klassenfamilien, da bei Schnittstellen nicht alles in eine Klassenfamilie passen muss. Zum Beispiel:

Schnittstelle Talkative {

void talk (); }}

abstrakte Klasse Tier implementiert Talkative {

abstraktes öffentliches Leergespräch (); }}

Klasse Hund erweitert Tier {

public void talk () {System.out.println ("Woof!"); }}

Klasse Katze erweitert Tier {

public void talk () {System.out.println ("Meow."); }}

Klasse Interrogator {

statische Leere makeItTalk (gesprächiges Thema) {subject.talk (); }}

Angesichts dieser Klassen und Schnittstellen können Sie später einer völlig anderen Klassenfamilie eine neue Klasse hinzufügen und dennoch Instanzen der neuen Klasse an übergeben makeItTalk(). Stellen Sie sich zum Beispiel vor, Sie fügen CuckooClockeiner bereits vorhandenen ClockFamilie eine neue Klasse hinzu :

Klassenuhr {}

Klasse CuckooClock implementiert Talkative {

public void talk () {System.out.println ("Kuckuck, Kuckuck!"); }}

Da CuckooClockdie TalkativeSchnittstelle implementiert wird, können Sie ein CuckooClockObjekt an die makeItTalk()Methode übergeben:

Klasse Beispiel4 {

public static void main(String[] args) { CuckooClock cc = new CuckooClock(); Interrogator.makeItTalk(cc); } }

With single inheritance only, you'd either have to somehow fit CuckooClock into the Animal family, or not use polymorphism. With interfaces, any class in any family can implement Talkative and be passed to makeItTalk(). This is why I say interfaces give you more polymorphism than you can get with singly inherited families of classes.

The 'burden' of implementation inheritance

Okay, my "more polymorphism" claim above is fairly straightforward and was probably obvious to many readers, but what do I mean by, "without the burden of multiple inheritance of implementation?" In particular, exactly how is multiple inheritance of implementation a burden?

As I see it, the burden of multiple inheritance of implementation is basically inflexibility. And this inflexibility maps directly to the inflexibility of inheritance as compared to composition.

By composition, I simply mean using instance variables that are references to other objects. For example, in the following code, class Apple is related to class Fruit by composition, because Apple has an instance variable that holds a reference to a Fruit object:

class Fruit {

//... }

class Apple {

private Fruit fruit = new Fruit(); //... }

In this example, Apple is what I call the front-end class and Fruit is what I call the back-end class. In a composition relationship, the front-end class holds a reference in one of its instance variables to a back-end class.

In last month's edition of my Design Techniques column, I compared composition with inheritance. My conclusion was that composition -- at a potential cost in some performance efficiency -- usually yielded more flexible code. I identified the following flexibility advantages for composition:

  • It's easier to change classes involved in a composition relationship than it is to change classes involved in an inheritance relationship.

  • Mit Composition können Sie die Erstellung von Back-End-Objekten verzögern, bis sie benötigt werden (und es sei denn). Außerdem können Sie die Back-End-Objekte während der gesamten Lebensdauer des Front-End-Objekts dynamisch ändern. Mit der Vererbung erhalten Sie das Bild der Oberklasse in Ihrem Unterklassenobjektbild, sobald die Unterklasse erstellt wurde, und es bleibt während der gesamten Lebensdauer der Unterklasse Teil des Unterklassenobjekts.

Der einzige Flexibilitätsvorteil, den ich für die Vererbung identifiziert habe, war:

  • Das Hinzufügen neuer Unterklassen (Vererbung) ist einfacher als das Hinzufügen neuer Front-End-Klassen (Komposition), da die Vererbung mit Polymorphismus einhergeht. Wenn Sie ein Stück Code haben, das nur auf einer Superklassenschnittstelle basiert, kann dieser Code ohne Änderung mit einer neuen Unterklasse arbeiten. Dies gilt nicht für Kompositionen, es sei denn, Sie verwenden Kompositionen mit Schnittstellen.