Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
1 Einleitung
2 Die Basis der Objektorientierung
3 Die Prinzipien des objektorientierten Entwurfs
4 Die Struktur objektorientierter Software
5 Vererbung und Polymorphie
6 Persistenz
7 Abläufe in einem objektorientierten System
8 Module und Architektur
9 Aspekte und Objektorientierung
10 Objektorientierung am Beispiel: Eine Web-Applikation mit PHP 5 und Ajax
A Verwendete Programmiersprachen
B Literaturverzeichnis
Stichwort
Ihre Meinung?

Spacer
 <<   zurück
Objektorientierte Programmierung von Bernhard Lahres, Gregor Rayman
Das umfassende Handbuch
Buch: Objektorientierte Programmierung

Objektorientierte Programmierung
2., aktualisierte und erweiterte Auflage, geb.
656 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1401-8
Pfeil 5 Vererbung und Polymorphie
  Pfeil 5.1 Die Vererbung der Spezifikation
    Pfeil 5.1.1 Hierarchien von Klassen und Unterklassen
    Pfeil 5.1.2 Unterklassen erben die Spezifikation von Oberklassen
    Pfeil 5.1.3 Das Prinzip der Ersetzbarkeit
    Pfeil 5.1.4 Abstrakte Klassen, konkrete Klassen und Schnittstellen-Klassen
    Pfeil 5.1.5 Vererbung der Spezifikation und das Typsystem
    Pfeil 5.1.6 Sichtbarkeit im Rahmen der Vererbung
  Pfeil 5.2 Polymorphie und ihre Anwendungen
    Pfeil 5.2.1 Dynamische Polymorphie am Beispiel
    Pfeil 5.2.2 Methoden als Implementierung von Operationen
    Pfeil 5.2.3 Anonyme Klassen
    Pfeil 5.2.4 Single und Multiple Dispatch
    Pfeil 5.2.5 Die Tabelle für virtuelle Methoden
  Pfeil 5.3 Die Vererbung der Implementierung
    Pfeil 5.3.1 Überschreiben von Methoden
    Pfeil 5.3.2 Das Problem der instabilen Basisklassen
    Pfeil 5.3.3 Problem der Gleichheitsprüfung bei geerbter Implementierung
  Pfeil 5.4 Mehrfachvererbung
    Pfeil 5.4.1 Mehrfachvererbung: Möglichkeiten und Probleme
    Pfeil 5.4.2 Delegation statt Mehrfachvererbung
    Pfeil 5.4.3 Mixin-Module statt Mehrfachvererbung
    Pfeil 5.4.4 Die Problemstellungen der Mehrfachvererbung
  Pfeil 5.5 Statische und dynamische Klassifizierung
    Pfeil 5.5.1 Dynamische Änderung der Klassenzugehörigkeit
    Pfeil 5.5.2 Entwurfsmuster »Strategie« statt dynamischer Klassifizierung

In diesem Kapitel erfahren Sie, wie Sie die Vererbung der Spezifikation im Zusammenspiel mit der Fähigkeit der dynamischen Polymorphie nutzen können, um Ihre Programme flexibler zu gestalten. Wir stellen zunächst Vererbung und Polymorphie vor, um dann an konkreten Aufgabenstellungen deren Möglichkeiten und Grenzen aufzuzeigen.

5 Vererbung und Polymorphie

In der objektorientierten Programmierung gibt es zwei unterschiedliche Konzepte, die beide als Vererbung bezeichnet werden.

Vererbung der Spezifikation

In diesem Abschnitt werden wir zunächst die Vererbung der Spezifikation betrachten. Diese ist eine der wichtigsten Techniken der Objektorientierung. Im Zusammenspiel mit der dynamischen Polymorphie, die Thema von Abschnitt 5.2 ist, stellt die Vererbung der Spezifikation das zentrale Modellierungsmittel der Objektorientierung dar.

Vererbung der Implementierung

In Abschnitt 5.3 stellen wir dann die Vererbung der Implementierung vor. Die Vererbung der Spezifikation und die Vererbung der Implementierung werden häufig pauschal unter dem Begriff der Vererbung zusammengefasst. Dabei sind die zugrunde liegenden Konzepte klar verschieden. Die Vererbung der Implementierung ist ein Mittel zur Vermeidung von Redundanzen, das aber wesentlich mehr konzeptuelle und praktische Probleme mit sich bringt als die Vererbung der Spezifikation.


Rheinwerk Computing - Zum Seitenanfang

5.1 Die Vererbung der Spezifikation  Zur nächsten ÜberschriftZur vorigen Überschrift

Bevor wir auf die Vererbung der Spezifikation zu sprechen kommen, beginnen wir zunächst mit der Einführung von Beziehungen zwischen Klassen.


Rheinwerk Computing - Zum Seitenanfang

5.1.1 Hierarchien von Klassen und Unterklassen  Zur nächsten ÜberschriftZur vorigen Überschrift

Bisher haben wir Klassen meist als Gruppierungen von gleichartigen Objekten betrachtet. Das ist für sich eine durchaus nützliche und praktikable Sicht.

Die zentralen Mechanismen der Objektorientierung lassen sich jedoch erst nutzen, wenn auch Beziehungen zwischen Klassen möglich sind. Die wichtigste Beziehung, die zwischen zwei Klassen bestehen kann, ist, dass eine Klasse als Unterklasse einer anderen Klasse eingestuft wird.


Icon Hinweis Unterklassen und Oberklassen

Eine Klasse SpezielleKlasse ist dann eine Unterklasse der Klasse AllgemeineKlasse, wenn SpezielleKlasse die Spezifikation von AllgemeineKlasse erfüllt, umgekehrt aber AllgemeineKlasse nicht die Spezifikation von SpezielleKlasse. Die Klasse AllgemeineKlasse ist dann eine Oberklasse von SpezielleKlasse.

Die Beziehung zwischen AllgemeineKlasse und SpezielleKlasse wird Spezialisierung genannt. Die umgekehrte Beziehung zwischen SpezielleKlasse und AllgemeineKlasse heißt Generalisierung.


In Abbildung 5.1 ist die UML-Darstellung dieser Beziehungen zwischen zwei Klassen aufgeführt.

Abbildung 5.1    Darstellung von Unterklasse und Oberklasse in UML

Diese Beziehungen können auch in komplexeren Hierarchien organisiert sein. So kann eine Klasse durchaus die Spezifikation von mehreren anderen Klassen erfüllen, also Unterklasse von mehreren Oberklassen sein. Auf der anderen Seite kann eine Oberklasse auch mehrere Unterklassen haben. In Abbildung 5.2 ist als Beispiel die Klasse Dokument dargestellt.

Die Klasse Dokument ist Unterklasse der beiden Klassen Darstellbar und Druckbar. Sie ist aber auch Oberklasse der drei Klassen Arbeitsvertrag, Kreditvertrag und AGB.

Abbildung 5.2    Mehrere Unter- und Oberklassen


Rheinwerk Computing - Zum Seitenanfang

5.1.2 Unterklassen erben die Spezifikation von Oberklassen  Zur nächsten ÜberschriftZur vorigen Überschrift

In Abschnitt 4.2.2, »Kontrakte: die Spezifikation einer Klasse«, haben wir erläutert, dass sich die Spezifikation einer Klasse aus den Vor- und Nachbedingungen für alle ihre Operationen zusammensetzt, ergänzt durch die für alle Exemplare der Klasse geltenden Invarianten. Eine Unterklasse erbt die Spezifikation ihrer Oberklasse. Damit gilt die Spezifikation der Oberklasse auch für die Unterklasse. Die Unterklasse ist an diese Spezifikation gebunden und kann sie nur auf klar definierte Weise modifizieren. Die Regeln für die Anpassung dieser Spezifikation stellen wir im nachfolgenden Abschnitt 5.1.3 genauer vor.

Unterklasse zu sein ist also mit einer Menge Verantwortung verbunden. Eine Unterklasse muss immer für die Verpflichtungen ihrer Oberklasse einstehen können.


Icon Hinweis Vererbung der Spezifikation (Vererbung von Schnittstellen, engl. Interface Inheritance)

Eine Unterklasse erbt grundsätzlich die Spezifikation ihrer Oberklasse. Die Unterklasse übernimmt damit alle Verpflichtungen und Zusicherungen der Oberklasse. Alternativ wird auch der Begriff Vererbung von Schnittstellen benutzt. Verbung der Spezifikation drückt aber besser aus, dass eine Unterklasse die Verpflichtungen mit übernimmt, die sich aus der Spezifikation der Oberklasse ergeben. Es handelt sich eben nicht darum, einfach die Syntax einer Schnittstelle zu erben.


Beispiel zur Vererbung der Spezifikation

Verdeutlichen wir diese doch recht abstrakte Definition an einem Beispiel. Werfen Sie dazu einen Blick auf die Benutzeroberfläche einer gewöhnlichen Anwendung. Sie besteht aus Fenstern, Dialogen, Menüs, dargestellten Tasten und anderen Steuerelementen, Bildern, Animationen, Geräuschen und einer ganzen Reihe von weiteren Elementen. Unsere Beispielaufgabe besteht darin, dem Benutzer zu ermöglichen, die Benutzeroberfläche an seine Bedürfnisse anzupassen. Er soll die Menüstruktur, die Tastenkürzel, die Symbolleisten und andere Steuerelemente an seinen Geschmack anpassen können. Er soll also die Steuerungsmöglichkeiten der Anwendung mit den Aktionen der Anwendung beliebig verknüpfen können. In Abbildung 5.3 ist eine Reihe von möglichen Steuerelementen exemplarisch dargestellt.

Abbildung 5.3    Steuerelemente einer Anwendung

In diesem kurzen Abschnitt lassen sich bereits zwei Klassen finden: die Klasse der Aktionen und die Klasse der Steuerelemente. Darüber hinaus gibt es in der Anwendung noch viele andere Klassen, mit denen Sie sich jetzt aber nicht zu beschäftigen brauchen. Schauen Sie sich stattdessen die Klasse der Steuerelemente genauer an. Zu dieser Klasse gehören die Menüs, die Symbolleistentasten, die Tastenkürzel, die Mausgesten und sogar die Befehle der Sprachsteuerung.

Gemeinsamkeiten von Steuerelementen

Alle diese Objekte – Exemplare der Klasse Steuerelement – haben folgende Gemeinsamkeiten:

  • Ihnen kann eine Aktion zugeordnet werden.
  • Diese Aktion wird ausgelöst, wenn das Steuerelement aktiviert wird.
  • Sie haben einen darstellbaren Namen, der dem Benutzer bei der Konfiguration der Oberfläche angezeigt wird.

Unterschiede zwischen Steuerelementen

Doch sie haben nicht nur Gemeinsamkeiten, es bestehen auch ziemlich große Unterschiede zwischen den Steuerelementen. Den Menüs und den Symbolleistentasten kann man einen Namen, ein Symbol, einen erklärenden Text zuordnen, der dann in der Statuszeile erscheint, wenn man mit der Maus das Steuerelement berührt. Die Tastenkürzel, die Mausgesten und die Sprachsteuerungsbefehle haben diese Fähigkeiten nicht, dafür haben sie andere Fähigkeiten, welche die Menüs und die Symbolleistentasten nicht haben.

Sie können die Klasse Steuerelement also weiter unterteilen, und Sie können damit deren Exemplare weiter klassifizieren. Sie werden in diesem Beispiel feststellen, dass die Menüeinträge und die Symbolleistentasten sich nur durch die vom Benutzer zugewiesene Position unterscheiden. Deshalb ordnen Sie diese alle der Klasse Menü zu, die anderen Steuerelemente ordnen Sie den Klassen Tastenkürzel, Mausgeste und Sprachbefehl zu. In Abbildung 5.4 sind die Beziehungen zwischen den einzelnen Klassen dargestellt.

Abbildung 5.4    Steuerelemente und Aktionen

Unterklassen und Oberklassen

Die Klassen Menü, Tastenkürzel, Mausgeste und Sprachbefehl sind Unterklassen der Klasse Steuerelement, die ihre Oberklasse ist. Die Klasse Steuerelement beschreibt allgemein, was alle Steuerelemente der Anwendung gemeinsam haben. Die Klasse Steuerelement einerseits und die Klassen Menü, Tastenkürzel, Mausgeste und Sprachbefehl andererseits stehen zueinander in einer Beziehung: Die Oberklasse Steuerelement ist die Generalisierung der Unterklassen, die ihrerseits Spezialisierungen der Oberklasse sind.

Konformität

Jedes Exemplar einer der genannten Unterklassen ist gleichzeitig ein Exemplar der Oberklasse. Die Spezifikation der Klasse Steuerelement gilt also für alle Exemplare jeder der genannten Unterklassen – sie sind also konform mit der Spezifikation der Klasse Steuerelement.

Vererbung der Spezifikation

Diese Konformität bezeichnen wir als Vererbung der Spezifikation. Die Unterklassen erben die Spezifikation ihrer Oberklassen. Damit hat die Unterklasse wie bereits gesehen zum einen eine große Verantwortung übernommen: Eine Unterklasse geht alle Verpflichtungen der Oberklasse ein. Aber wir haben auch viel gewonnen: Die Unterklasse kann nun an allen Stellen eingesetzt werden, an denen auch die Oberklasse verwendet werden kann.

Diskussion: Arbeitsverweigerung durch Unterklasse

Gregor: Dass wir eine Unterklasse immer anstelle ihrer Oberklasse einsetzen können, ist nicht ganz richtig. Das kann ich dir mit ein paar Zeilen Java-Code skizzieren:
class A { 
    // macht etwas Sinnvolles 
    void machwas() {System.out.println("ich mach ja schon");} 
} 
class B extends A{ 
    // macht nix Sinnvolles 
    void machwas() { throw new RuntimeException("nix mach ich");} 
}
Bernhard: Es ist richtig, dass wir in deinem Beispiel ein Exemplar der Klasse B nicht anstelle eines Exemplars der Klasse A einsetzen können, da das Exemplar von B Arbeitsverweigerung betreibt. Aber das heißt einfach, dass B eben keine Unterklasse von A ist, auch wenn es durch die Modellierung in der Programmiersprache suggeriert wird. Nur weil du das Schlüsselwort extends hingeschrieben hast, muss das noch lange nicht heißen, dass wir wirklich eine Unterklasse vorliegen haben. Du hast einfach ein fehlerhaftes Design gebaut. Ich weiß natürlich, dass du ein solches Design nie wirklich verwenden würdest. Aber leider ist es ja nicht immer so offensichtlich, dass eine Klasse B eben keine Unterklasse einer Klasse A ist, sondern A nur technisch erweitert.

Anhand der kleinen Diskussion sehen Sie schon, dass die Frage, ob eine Klasse eine Unterklasse einer anderen Klasse ist, ganz zentral beim Design von objektorientierten Anwendungen ist. Gutes Design wird in echten Unterklassen resultieren, die auf jeden Fall dem Prinzip der Ersetzbarkeit genügen, das wir im folgenden Abschnitt näher betrachten.


Rheinwerk Computing - Zum Seitenanfang

5.1.3 Das Prinzip der Ersetzbarkeit   Zur nächsten ÜberschriftZur vorigen Überschrift

Das Prinzip der Ersetzbarkeit besagt, dass jedes Exemplar einer Klasse deren Spezifikation erfüllen muss. Das gilt auch dann, wenn das Objekt ein Exemplar einer Unterklasse der spezifizierten Klasse ist.

Exemplare von Unterklassen

Überall dort, wo in unserer Anwendung ein Exemplar der Klasse Steuerelement erwartet wird, kann man Exemplare der Unterklassen verwenden, denn die Unterklassen erben alle Eigenschaften, die Funktionalität, die Beziehungen und die Verantwortlichkeiten der Oberklasse.

Trennung Schnittstelle von Implementierung

Damit sind die Exemplare der Unterklassen gleichzeitig Exemplare der Oberklasse in Bezug auf die der Oberklasse zugrunde liegende Spezifikation. Im Kontext funktioniert das natürlich nur, wenn Sie das Prinzip der Trennung der Schnittstelle von der Implementierung auch von Seiten eines nutzenden Moduls einhalten. Ein nutzendes Modul darf sich nie auf Implementierungen des genutzten Moduls verlassen, sondern immer nur auf dessen Spezifikation.

In Abbildung 5.5 ist dargestellt, dass das Objekt steuerung (ein Exemplar von Aktionssteuerung) mit einem Exemplar der Klasse Steuerelement arbeitet.

Da die Klassen Menü, Tastenkürzel, Mausgeste und Sprachbefehl alle Steuerelemente sind und die Spezifikation der Klasse Steuerelement erfüllen, können deren Exemplare auch beliebig für das von steuerung verwendete Steuerelement ersetzt werden.

Gemeinsame Funktionalität von Steuerelementen

Die Funktionen, die ein Steuerelement verwenden möchten, interessiert es nicht, ob es sich um das Menü »Neue Datei«, das Tastenkürzel Taste Alt + Taste N oder die Mausgeste »Links-Rechts« handelt, sie verwenden nur die allen Steuerelementen gemeinsame Funktionalität: einen darstellbaren Namen zu haben und einer auszulösenden Aktion zugeordnet sein zu können. Dabei kann die konkrete Umsetzung dieser Funktionalität für verschiedene Steuerelemente natürlich unterschiedlich sein, und sie können unterschiedliche, über die Basisfunktionalität hinausgehende Zusatzfunktionalität besitzen. Zum Beispiel kann der Benutzer den Namen der Menüs selbst bestimmen, der Name der Tastenkürzel ist aber fest und nicht änderbar.

Abbildung 5.5    Menüs, Tastenkürzel, Mausgesten und Sprachbefehle sind echte Steuerelemente.

Sie haben hier ein fundamentales Prinzip der objektorientierten Systeme vorliegen: An jeder Stelle, an der ein Exemplar einer Oberklasse erwartet wird, kann auch ein Exemplar einer ihrer Unterklassen verwendet werden. Dieses Prinzip wird Prinzip der Ersetzbarkeit genannt, auch als das Liskov´sche Substitutionsprinzip bekannt.


Icon Hinweis Prinzip der Ersetzbarkeit

Wenn die Klasse B eine Unterklasse der Klasse A ist, dann können in einem Programm alle Exemplare der Klasse A durch Exemplare der Klasse B ersetzt worden sein, und es gelten trotzdem weiterhin alle zugesicherten Eigenschaften der Klasse A.


Der für die Oberklasse geschlossene Kontrakt mit Bezug auf Vorbedingungen, Nachbedingungen und Invarianten gilt also auch dann weiter, wenn Exemplare der Oberklasse durch Exemplare der Unterklasse ersetzt werden.

Diskussion: Prinzip der Ersetzbarkeit

Bernhard: Das mit dem Prinzip der Ersetzbarkeit hört sich zwar ganz gut an, in der Praxis funktioniert es aber nicht immer.

Gregor: Was meinst Du? Natürlich funktioniert es immer. Ein Exemplar einer Unterklasse ist doch gleichzeitig ein Exemplar der Oberklasse, also kann es überall dort verwendet werden, wo ein Exemplar der Oberklasse erwartet wird.

Bernhard: Eben nicht. Nehmen wir eine Umsetzung deines Beispiels in Java. Nehmen wir an, die Variable element1 hält eine Referenz auf ein direktes Exemplar der Klasse Steuerelement. Wenn ich element1.getClass(). getSimpleName() aufrufe, bekomme ich die Zeichenkette »Steuerelement« zurück. Würde aber element1 ein Exemplar der Klasse Sprachbefehl enthalten, liefert der gleiche Aufruf die Zeichenkette »Sprachbefehl«. Für bestimmte Operationen kann man sich also auf das Prinzip der Ersetzbarkeit nicht verlassen.

Gregor: Ich sehe, was du meinst. Aber trotzdem gilt das Prinzip der Ersetzbarkeit. Das Problem besteht in der Vermischung der fachlichen Konzepte mit deren technischer Umsetzung. In der fachlichen Definition der Klasse Steuerelement haben wir nicht beschrieben, dass deren Exemplare auf den Aufruf getClass().getSimpleName() mit der Zeichenkette »Steuerelement« antworten. Diese Eigenschaften gehören nicht zu der »Steuerelementigkeit« der Objekte, genauso wenig wie zum Beispiel die benötigte Speichergröße für ein Exemplar der Klasse. Die Funktionen, die Objekte der Klasse Steuerelement als solche verwenden und sich dabei auf das Prinzip der Ersetzbarkeit verlassen möchten, dürfen sich natürlich nur auf die Funktionalität der Klasse Steuerelement beziehen, die ihnen diese Klasse fachlich anbietet. Der Aufruf getClass().getSimpleName() gehört zum Beispiel nicht zu dieser fachlichen Funktionalität.

Bernhard: Na gut, aber was ist, wenn ich diese technischen Eigenschaften einer Klasse, wie du sie nennst, in die Beschreibung der Klasse aufnehme? Wird dann das Prinzip der Ersetzbarkeit nicht verletzt?

Gregor: Nein, das Prinzip sagt etwas anderes. Wenn ich als eine Eigenschaft der Exemplare der Klasse Steuerelement deklariere, dass der Aufruf getClass().getSimpleName() immer die Zeichenkette »Steuerelement« liefert, dann gehören die Exemplare der Klasse Sprachbefehl einfach nicht zu der Klasse Steuerelement. Die Klasse Sprachbefehl ist also in diesem Fall fachlich keine Unterklasse der Klasse Steuerelement, auch wenn die Sprache Java das anders sieht. Dadurch, dass du das Schlüsselwort extends in Java verwendest, hast du noch lange keine echte Unterklasse vorliegen. Eine Programmiersprache ist eben nur ein technisches Mittel, mit dem man bestimmte Konzepte umsetzen kann. Die Konzepte der Objektorientierung kann man in objektorientierten Sprachen wie Java sehr gut umsetzen, es heißt aber nicht, dass alle Konzepte immer direkt in die entsprechenden Konstrukte der Programmiersprache umgewandelt werden.

Bernhard: Na gut, ich sehe ein, dass technische Eigenschaften wie der Klassenname oder die benötigte Speichergröße nichts mit der fachlichen Funktionalität der Klasse Steuerelement zu tun haben. Trotzdem sind diese wichtig für andere Aspekte der Anwendung, zum Beispiel das Speichern einer Konfiguration.

Gregor: Selbstverständlich. Nur handelt es sich hier nicht um die Verwendung der Objekte in deren Eigenschaft, ein Steuerelement zu sein. Also kann man nicht von einer Verletzung des Prinzips der Ersetzbarkeit sprechen.

Unterklassen dürfen modifizieren

Eine Konsequenz aus dem Prinzip der Ersetzbarkeit ist es, dass Unterklassen die Spezifikation, die sie von ihren Oberklassen erben, durchaus modifizieren können. Sie dürfen von einem Aufrufer aber nicht mehr fordern oder ihm weniger zusichern, als es die Oberklasse tut.

Wenn das Prinzip der Ersetzbarkeit also nicht verletzt werden soll, dann kann für Exemplare von Unterklassen nur gelten, dass sie zwar mehr anbieten, aber nicht mehr verlangen als Exemplare ihrer Oberklassen. Dies gilt auch für den Kontrakt, den diese Exemplare von Unterklassen eingehen. Damit ergeben sich drei Konsequenzen für die Vorbedingungen, Nachbedingungen und Invarianten, die für eine Unterklasse gelten.

Die Vorbedingungen


Erste Konsequenz des Prinzips der Ersetzbarkeit für Unterklassen

Eine Unterklasse kann die Vorbedingungen für eine Operation, die durch die Oberklasse definiert wird, einhalten oder abschwächen. Sie darf die Vorbedingungen aber nicht verschärfen.


Falls eine Unterklasse die Vorbedingungen verschärfen würde, würde damit ohne Absprache mit den Partnern von diesen mehr verlangt als vorher.

Die Nachbedingungen


Zweite Konsequenz des Prinzips der Ersetzbarkeit für Unterklassen

Eine Unterklasse kann die Nachbedingungen für eine Operation, die durch eine Oberklasse definiert werden, einhalten oder erweitern. Sie darf die Nachbedingungen aber nicht einschränken.


Falls eine Unterklasse die Nachbedingungen lockern würde, würde diesen damit wieder ohne Absprache mit den Partnern des Kontrakts weniger geboten als vorher.

Invarianten


Dritte Konsequenz des Prinzips der Ersetzbarkeit für Unterklassen

Eine Unterklasse muss dafür sorgen, dass die für die Oberklasse definierten Invarianten immer gelten.


Die Partner des Kontrakts müssen sich auf die zugesicherten Invarianten verlassen können.

Allerdings fordert das Prinzip nicht, dass durch die Ersetzung keine Verhaltensänderung eintritt. Natürlich wollen wir durch Einsatz von Unterklassen das Verhalten eines Programms ändern. Aber durch die Ersetzung bleiben die durch die Oberklasse zugesicherten Eigenschaften erhalten. Nach dem Aufruf von first() auf einem Iterator gilt die Bedingung, dass anschließend der Iterator auf dem ersten Element einer Liste steht. Alle Unterklassen des Iterators sind ebenfalls an diese Bedingung gebunden.

Verletzung des Prinzips

Nicht immer ist unmittelbar klar, dass das Prinzip der Ersetzbarkeit durch eine bestimmte Modellierung verletzt wird. Betrachten wir deshalb ein Beispiel, bei dem die Verletzung nicht auf den ersten Blick erkennbar ist. Die Klassenhierarchie in Abbildung 5.6 erscheint auf den ersten Blick plausibel.

Abbildung 5.6    Verletzung des Prinzips der Ersetzbarkeit

Prüfung: A ist ein B

Die Frage, ob ein Quadrat ein Rechteck ist, würden wir zunächst mit »Ja« beantworten. Klar, ein Quadrat ist ein Rechteck, und ein Kreis ist eine Ellipse, nur eben mit speziellen Bedingungen. Ein Quadrat ist eben ein spezielles Rechteck, und deshalb kann man es als Spezialisierung eines Rechtecks modellieren. [Robert C. Martin führt in einem Artikel, der ursprünglich im C++-Report erschienen ist, zum Prinzip der Ersetzbarkeit das Beispiel zu Rechteck und Quadrat an. Wir verwenden es hier ebenfalls, weil es die grundlegende Problemstellung sehr plakativ verdeutlicht. Robert C. Martins Artikel findet sich unter http://www.objectmentor. com/resources/articles/lsp.pdf. ] Aber diese Prüfung alleine reicht eben nicht aus. Das Prinzip der Ersetzbarkeit fordert mehr, nämlich dass ein Quadrat in allen Kontexten an die Stelle eines Rechtecks treten kann.

Erweiterte Prüfung: Ersetzbarkeit

Aber ist das in unserem Fall wirklich gegeben? Wir haben der Klasse Rechteck zwei Methoden skalierenX und skalierenY gegeben. Diese machen für ein Rechteck durchaus Sinn, denn sie skalieren jeweils das Rechteck in X- bzw. Y-Richtung. Einem Rechteck tut das auch überhaupt nicht weh. In der Abbildung sind auch die Vor- und Nachbedingungen für die Operation skalierenX der Klasse Rechteck angegeben.

Nach dem Prinzip der Ersetzbarkeit müssen diese beiden Operationen nun aber auch für ein Quadrat anwendbar sein. Aber was passiert, wenn Sie ein Quadrat lediglich in X-Richtung skalieren? Dann ist es aber ganz schnell vorbei mit der Quadrat-Eigenschaft, und Sie haben das Objekt in einen inkonsistenten Zustand gebracht. In diesem Fall gilt die in der Abbildung angegebene Invariante (laengeX = laengeY) nicht mehr: Die Seitenlängen des Quadrats sind nicht mehr alle gleich.

Gründe für Verletzung

Die Gründe, warum das Prinzip der Ersetzbarkeit von einer Unterklasse nicht eingehalten wird, können verschiedene sein. In der Regel ist es aber so, dass eine oder mehrere Operationen der Oberklasse nicht mehr anwendbar sind, weil sie zu einem Fehler oder zu einem inkonsistenten Zustand führen. Auch wenn Sie eine Methode, die in einer Basisklasse umgesetzt ist, in einer abgeleiteten Klasse leer überschreiben, liegt der Verdacht auf eine Verletzung des Prinzips nahe. In der Regel haben Sie dann die Basisklasse schlecht gewählt, da offensichtlich nicht alle Operationen von allen Unterklassen sinnvoll umgesetzt werden können.

Sie sehen also, dass das Prinzip der Ersetzbarkeit klare Anforderungen an Klassenhierarchien stellt. Die Einhaltung des Prinzips ist eine zentrale Forderung, damit unsere Programme über den Einsatz von Unterklassen erweiterbar bleiben. In Abschnitt 7.5, »Kontrakte«, stellen wir weitere Beispiele dafür vor, wie das Prinzip der Ersetzbarkeit in der Praxis angewendet wird. Zunächst stellen wir aber die unterschiedlichen Arten vor, mit denen Klassen eine Schnittstelle und deren Umsetzung bereitstellen können.


Rheinwerk Computing - Zum Seitenanfang

5.1.4 Abstrakte Klassen, konkrete Klassen und Schnittstellen-Klassen  Zur nächsten ÜberschriftZur vorigen Überschrift

Wie wir bereits ausgeführt haben, sind Objekte in der Regel Exemplare von Klassen. Wir haben dabei nicht weiter eingeschränkt, von welchen Klassen es denn Exemplare geben kann. In diesem Abschnitt werden wir Klassen betrachten, von denen es gar keine Exemplare geben kann, und begründen, warum diese trotzdem sehr sinnvoll sein können.

Klassen mit Spezifikationen

Es gibt Klassen, deren hauptsächliche Aufgabe darin besteht, eine Spezifikation bereitzustellen, die von Unterklassen geerbt werden kann. Diese werden für alle oder einen Teil der durch sie spezifizierten Operationen keine Methoden für deren Implementierung bereitstellen.

Klassen können in dieser Hinsicht in drei Kategorien eingeteilt werden, abhängig davon, in welchem Umfang sie selbst für die von ihnen spezifizierte Schnittstelle auch Methoden bereitstellen:

  • Konkrete Klassen
  • Rein spezifizierende Klassen (Schnittstellen-Klassen)
  • Abstrakte Klassen

In den folgenden Abschnitten werden wir die drei Kategorien jeweils vorstellen. In Abbildung 5.7 sind im Überblick schon alle drei in UML-Notation zu sehen.

Die Klasse ElektrischeLeitung ist eine konkrete Klasse, Steuerelement ist eine abstrakte Klasse, und Aktion ist eine Schnittstellen-Klasse.

Konkrete Klassen


Icon Hinweis Konkrete Klassen

Konkrete Klassen stellen für alle von der Klasse spezifizierten Operationen auch Methoden bereit. Von konkreten Klassen können Exemplare erzeugt werden.


Wenn in einem Programm Objekte erstellt werden, müssen diese immer Exemplare einer konkreten Klasse sein. Die Klasse spezifiziert also – wie alle Klassen – selbst eine Schnittstelle, stellt aber auch für jede der darin enthaltenen Operationen konkrete Methoden zur Verfügung.

Abbildung 5.7    Konkrete Klasse, abstrakte Klasse und Schnittstellen-Klasse

Schnittstellen-Klassen


Icon Hinweis Schnittstellen-Klassen (engl. Interfaces)

Schnittstellen-Klassen dienen alleine der Spezifikation einer Menge von Operationen. Sie stellen für keine der durch die Klasse spezifizierten Operationen eine Methode bereit. Von Schnittstellen-Klassen können keine Exemplare erstellt werden.


Schnittstellen-Klassen sind also Klassen, die keine Methoden implementieren. Sie stellen eine Spezifikation bereit, die von anderen Klassen geerbt werden kann.

In UML wird eine Schnittstelle als eine Klasse des Stereotyps «interface» oder «Schnittstelle» dargestellt. Abbildung 5.8 zeigt ein einfaches Beispiel, das eine Schnittstelle ProtocolHandler definiert. Die Schnittstelle umfasst zwei Operationen canHandle und getContent.

Abbildung 5.8    Darstellung einer Schnittstelle in UML

Eine Klasse implementiert eine Schnittstelle.

Wenn eine Unterklasse einer Schnittstellen-Klasse Methoden für alle von der Schnittstelle spezifizierten Operationen bereitstellt, sprechen wir davon, dass diese Unterklasse die Schnittstelle implementiert.

Abbildung 5.9    Die Klasse »HttpHandler« implementiert die Schnittstelle »ProtocolHandler«.

In Abbildung 5.9 sehen Sie die UML-Darstellung einer solchen implementierenden Klasse. Dabei implementiert die Klasse HttpHandler die Schnittstelle ProtocolHandler, weil sie für alle in der Schnittstelle enthaltenen Operationen Umsetzungen bereitstellt.

Einsatz

In den statisch typisierten Programmiersprachen werden rein spezifizierende Klassen benötigt, um Variablen und Parameter mit dem Typ der Schnittstelle verwenden zu können. Auch stellen rein spezifizierende Klassen eine sinnvolle Sicht in der Entwurfsphase von Software dar.

In den dynamisch typisierten Programmiersprachen dagegen machen rein spezifizierende Klassen keinen Sinn. Eine Schnittstellen-Klasse wird in einem Programm benötigt, um als gemeinsamer Typ zu agieren für die Klassen, welche die Schnittstelle implementieren. In einer dynamisch typisierten Sprache werden diese Typen aber gar nicht deklariert, da es für eine rein spezifizierende Klasse keine Verwendung gibt.

Abstrakte Klassen

Betrachten wir in diesem Abschnitt nun die abstrakten Klassen. Diese bilden eine Zwischenstufe zwischen den konkreten Klassen und den Schnittstellen-Klassen: Sie stellen in der Regel für einen Teil ihrer Operationen auch Methoden zur Verfügung.


Icon Hinweis Abstrakte Klassen

Abstrakte Klassen stellen für mindestens eine der von der Klasse spezifizierten Operationen keine Methode bereit. Von einer abstrakten Klasse kann es keine direkten Exemplare geben. Alle Exemplare einer abstrakten Klasse müssen gleichzeitig Exemplare einer nicht abstrakten Unterklasse dieser Klasse sein.


Abstrakte Klassen können also für einen Teil der von ihnen spezifizierten Operationen eine Implementierung in Form von Methoden bereitstellen. Für die anderen Operationen werden die Methoden zwar auch deklariert, es wird jedoch keine Implementierung zur Verfügung gestellt. Diese Methoden werden auch abstrakte Methoden genannt.


Icon Hinweis Abstrakte Methoden

Abstrakte Methoden sind ein programmiersprachliches Konstrukt, das es erlaubt, eine Operation für eine Klasse zu definieren, ohne dafür eine Methodenimplementierung zur Verfügung zu stellen. Eine abstrakte Methode ist also eine Methode, die keine Implementierung hat. Sie dient nur zur Spezifikation einer Operation. Eine Implementierung für die durch die abstrakten Methoden deklarierten Operationen stellen dann die konkreten Unterklassen bereit.


Abstrakte Methoden sind damit eigentlich nichts anderes als Deklarationen von Operationen. Da sich diese in den Programmiersprachen nicht unbedingt von den Deklarationen von Methoden unterscheiden, sprechen wir in diesem Fall von abstrakten Methoden.

Beispiel für abstrakte Klasse

Betrachten wir ein Beispiel, bei dem eine Kombination von abstrakten und nichtabstrakten Methoden sinnvoll ist. Nehmen Sie dafür an, dass Sie eine Klasse von geometrischen Formen umsetzen wollen. Die Formen sollen alle auf dem Bildschirm darstellbar und es soll ein Verschieben der Formen möglich sein.

Eine Operation verschieben können Sie bereits in der Oberklasse GeometrischeForm implementieren, denn der Ablauf ist immer gleich:

1. Die geometrische Form verstecken
       
2. Die Koordinaten ändern
       
3. Die geometrische Form auf der neuen Position anzeigen
       

Doch wie können Sie die Operationen verstecken und anzeigen implementieren, ohne den konkreten Typ der geometrischen Form zu kennen? Ein Kreis wird doch anders als ein Viereck dargestellt. Diese Methoden können also nur in den jeweiligen Unterklassen implementiert werden können. In Abbildung 5.10 sind die entsprechenden Klassen mit ihren Operationen dargestellt.

Abbildung 5.10    Realisierung von abstrakten Methoden

Statisch typisierte Sprachen

In statisch typisierten Programmiersprachen wie C++, C# oder Java müssen Sie jede Operation, die Sie aufrufen möchten, deklarieren. Sie müssen also deklarieren, dass alle Exemplare der Klasse GeometrischeForm die Operation verstecken und anzeigen unterstützen, ohne sie in der Klasse GeometrischeForm zu implementieren. Dies geschieht darüber, dass die Klasse GeometrischeForm die Methoden verstecken und anzeigen als abstrakte Methoden deklariert.

Der Java-Quelltext für die Klasse GeometrischeForm ist in Listing 5.1 aufgeführt.

public abstract class GeometrischeForm { 
    protected int x; 
    protected int y; 
    protected Graphics2D graphics; 
 
    ... // Teile des Quelltextes weggelassen 
 
    public abstract void verstecken(); 
    public abstract void anzeigen(); 
 
    public void verschieben(int xNeu, int yNeu) { 
        verstecken(); 
        x = xNeu; 
        y = yNeu; 
        anzeigen(); 
    } 
}

Listing 5.1    Umsetzung der abstrakten Klasse »GeometrischeForm« in Java

In unserem Beispiel deklarieren Sie also zwei abstrakte Methoden verstecken und anzeigen in der Klasse GeometrischeForm, die dann in der Methode verschieben verwendet werden können. Durch die Deklaration der Methoden verspricht die Klasse GeometrischeForm, dass alle ihre Exemplare diese zwei Operationen unterstützen. Die Klasse GeometrischeForm erfüllt dieses Versprechen jedoch nicht selbst, dafür müssen ihre Unterklassen geradestehen.

Da die Klasse GeometrischeForm aber keine eigene Implementierung der zwei abstrakten Methoden bereitstellt, kann es keine Exemplare direkt von dieser Klasse geben. Alle Exemplare müssen zu Unterklassen gehören, die jeweils eine Implementierung für die beiden abstrakten Methoden bereitstellen. Dies macht die Klasse GeometrischeForm zu einer abstrakten Klasse.

In unserem Beispiel realisiert zum Beispiel die Klasse Kreis beide abstrakten Methoden. Damit ist die Klasse Kreis eine konkrete Klasse, wir können Exemplare von ihr erzeugen.

In Listing 5.2 ist die Umsetzung der konkreten Unterklasse Kreis ausgeführt.

class Kreis extends GeometrischeForm { 
 
    protected int radius; 
 
    ... 
 
    public void verstecken() { 
        graphics.hideOval(x, y, radius, radius); 
    } 
    public void anzeigen() { 
        graphics.drawOval(x, y, radius, radius); 
    } 
}

Listing 5.2    Klasse »Kreis« realisiert abstrakte Methoden (Java-Beispiel).

Abstrakte Klasse ohne abstrakte Methoden

Eine Klasse, die abstrakte Methoden deklariert, ist immer selbst abstrakt. Doch es kann auch sinnvoll sein, eine Klasse als abstrakt zu deklarieren, auch wenn sie keine abstrakten Methoden deklariert. Dies ist dann der Fall, wenn es fachlich gefordert ist, dass die Exemplare einer Oberklasse immer zu einer ihrer Unterklassen gehören müssen.

Gregor: Der Unterschied zwischen abstrakten Klassen und Schnittstellen-Klassen erscheint mir etwas künstlich. Eigentlich sind doch auch Schnittstellen-Klassen einfach nur abstrakte Klassen, nur dass bei ihnen eben alle Methoden abstrakt sind. Konzeptuell ist hier doch kein großer Unterschied.

Bernhard: Da hast du Recht. Allerdings gibt es in den Programmiersprachen einige Randbedingungen, welche die Unterscheidung notwendig machen. Eine Klasse, die überhaupt keine Methoden implementiert, also eine reine Schnittstellen-Klasse ist, kann oft anders eingesetzt werden als eine andere abstrakte Klasse.

Gregor: Und was sind das für geheimnisvolle Randbedingungen?

Bernhard: Da muss ich einen kleinen Vorgriff machen, wir haben ja bisher noch nicht von der Vererbung der Implementierung und von Mehrfachvererbung gesprochen. Aber eine Reihe von Programmiersprachen lassen Mehrfachvererbung nur für die Spezifikation zu. Damit kann eine Klasse von mehreren Schnittstellen-Klassen erben, sie kann aber nicht von mehreren Klassen erben, die Methodenimplementierungen bereitstellen. Und damit wird die Unterscheidung für die Programmiersprache relevant.

Schnittstellen-Klassen und abstrakte Klassen in den Programmiersprachen

Grundsätzlich kann eine Schnittstellen-Klasse in einer Programmiersprache immer als eine Klasse umgesetzt werden, die ausschließlich abstrakte Methoden deklariert. Betrachten wir im Folgenden kurz die Umsetzung von abstrakten Klassen und Schnittstellen-Klassen in den statisch typisierten Sprachen C++, Java und C#.

Schnittstellen und abstrakte Klassen in C++

In C++ werden Klassen nicht explizit als abstrakt markiert. Eine Klasse gilt genau dann als abstrakt, wenn sie mindestens eine abstrakte Methode aufweist. Eine Schnittstelle in C++ ist einfach eine Klasse, die keine objektbezogenen Datenelemente deklariert und bei der alle Methoden abstrakt sind. In C++ werden die abstrakten Methoden auch als rein virtuelle Methoden bezeichnet (engl. pure virtual) und durch eine Deklaration mit dem Zusatz = 0 gekennzeichnet. Listing 5.3 zeigt ein Beispiel für eine Schnittstellen-Klasse in C++.

class generalbehavior { 
    virtual void dothis() = 0; 
    virtual void dothat() = 0; 
    virtual void checkresult() = 0; 
}

Listing 5.3    Schnittstellen-Klasse in C++

Eine Besonderheit in C++ ist es, dass eine abstrakte Methode eine Implementierung haben kann. Dies scheint zunächst ein Widerspruch zu sein und der Definition einer abstrakten Methode entgegenzustehen. Allerdings kann diese Implementierung nur unter expliziter Angabe des Klassennamens aufgerufen werden. Sie wird damit nie beim Aufruf einer Operation auf einem Objekt ausgeführt werden und gilt damit auch nicht als Implementierung der Operation. [Eine Ausnahme bilden hier die Destruktoren, die immer implizit beim Löschen eines Objekts aufgerufen werden. ]

In C++ können Sie eine Klasse nicht explizit als abstrakt deklarieren, sondern sie ist implizit genau dann abstrakt, wenn sie mindestens eine abstrakte Methode besitzt. Werden in einer abstrakten Klasse fachlich keine abstrakten Methoden benötigt, können Sie den Destruktor als abstrakt deklarieren und mit diesem Trick die Klasse selbst abstrakt machen.

class A { 
  virtual ~A() = NULL  { 
    // Implementierung 
    // des Destruktors 
  } 
};

Schnittstellen und abstrakte Klassen in Java und C#

C# hat sich die Behandlung von Schnittstellen-Klassen und abstrakten Klassen sehr genau bei Java abgeschaut. Deshalb stellen wir im Folgenden zwar die Variante von Java vor, alle Ausführungen gelten aber genauso für C#.

Java bietet ein spezielles Sprachkonstrukt interface für Schnittstellen-Klassen, um diese von den Klassen, die Methoden implementieren können, zu unterscheiden. Dabei kann eine Klasse mehrere dieser interfaces implementieren. Im unten stehenden Beispiel ist die Deklaration einer reinen Schnittstellen-Klasse in Java dargestellt.

interface GeneralBehavior { 
    void doThis(); 
    void doThat(); 
    void checkResult(); 
}

In Java können Methoden und Klassen mit dem Schlüsselwort abstract explizit als abstrakt markiert werden. Abstrakte Methoden in Java dürfen keine Implementierung haben. Randbedingung ist, dass eine Klasse mit abstrakten Methoden auch selbst als abstrakt deklariert werden muss.

In Java ist es natürlich auch möglich, Schnittstellen-Klassen einfach als abstrakte Klassen ohne Daten und ohne Methodenimplementierungen umzusetzen. In der Regel sollten Sie das aber nicht tun. In Java kann eine Klasse zwar von mehreren Schnittstellen-Klassen erben, nicht aber von mehreren anderen Klassen, auch wenn diese abstrakt sind und keinerlei Methoden implementieren.

Diskussion: Abstrakte Klassen vs. Interfaces in Java

Gregor: Im obenstehenden Text gibt es eine kleine Einschränkung: In der Regel soll es also keine gute Idee sein, Schnittstellen-Klassen als komplett abstrakte Klassen umzusetzen. Was wäre denn eine Ausnahme von der Regel?

Bernhard: Die Ausnahme hängt leider mit technischen Restriktionen zusammen. Es nützen ja die schönsten Konzepte nichts, wenn in der Praxis dadurch technische Schwierigkeiten entstehen.

Gregor: Wie meinst du das?

Bernhard: Durch die technische Umsetzung in Java führt eine Änderung an Interfaces dazu, dass bereits ausgelieferte Klassen, die von dem Interface erben, nicht mehr lauffähig sind. C# weist übrigens das gleiche Problem auf. Man spricht auch davon, dass so eine Änderung nicht binär kompatibel durchzuführen ist. Das Problem ist ein Teil dessen, was als Fragile Binary Interface Problem bekannt ist. Erweitern wir ein Interface um eine neue Operation, werden alle darauf basierenden Anwendungen nicht mehr arbeitsfähig sein, außer sie werden noch einmal komplett durch den Compiler gejagt. Bei einem Projekt wie zum Beispiel der Entwicklungsplattform Eclipse, bei der Tausende von Erweiterungen auf Schnittstellen basieren, kann das fatal sein.

Gregor: Und bei abstrakten Klassen ist das besser?

Bernhard: Bei abstrakten Klassen ist es zumindest möglich, neue Operationen und Methoden hinzuzufügen, ohne dass abgeleitete Klassen dadurch betroffen sind. Das erlaubt mehr Flexibilität. Diese Überlegungen sollten aber nur dann eine Rolle spielen, wenn es für ein Projekt sehr wichtig ist, dass Änderungen binär kompatibel erfolgen, und wenn Änderungen an Schnittstellen als wahrscheinlich eingeschätzt werden. [Unter http://www.artima.com/lejava/articles/designprinciples.html erläutert Erich Gamma im Gespräch mit Bill Venners, warum in vielen Fällen bei der Entwicklung der freien IDE Eclipse rein abstrakte Klassen anstelle von Interfaces eingesetzt wurden. ]
Rheinwerk Computing - Zum Seitenanfang

5.1.5 Vererbung der Spezifikation und das Typsystem  Zur nächsten ÜberschriftZur vorigen Überschrift

In Abschnitt 4.2.3, »Klassen sind Datentypen«, haben wir vorgestellt, wie Klassen als Datentypen agieren. Dabei haben wir das statische und dynamische Typsystem der Programmiersprachen vorgestellt.

Konsequenz von Änderungen an Oberklassen

In diesem Abschnitt wollen wir nun darauf eingehen, welche Möglichkeiten diese beiden Typsysteme jeweils bieten, wenn Klassenhierarchien angepasst werden sollen. Wir werden am Beispiel einer einfachen Klassenhierarchie zeigen, welche Konsequenzen Änderungen an Oberklassen haben können und wie in den beiden Typsystemen darauf reagiert werden kann.

Icon Beispiel Lesen aus Dateien

Betrachten wir zur Illustration eine Operation findLines einer Klasse StreamHandler, die bestimmte Zeilen aus einer Textdatei (einem Exemplar der Klasse File) heraussucht. In Abbildung 5.11 sind die beiden Klassen dargestellt.

Abbildung 5.11    Operation »findLines« arbeitet auf einer Datei.

Die Operation findLines wurde in StreamHandler mit der Prämisse umgesetzt, dass sie mit einem Exemplar von File als Parameter aufgerufen wird. Exemplare der Klasse File besitzen Operationen zum Lesen und Schreiben von Daten (read und write), zum Springen an eine beliebige Stelle in der Datei (seek) und zum Sperren bestimmter Bereiche der Datei (lock). Die beschriebene Funktion findLines verwendet aber nur die Fähigkeit eines Dateiobjekts, Daten sequenziell über die Methode read auszulesen.

Nehmen wir an, Sie möchten die Operation findLines nun auch für andere Datenquellen verwenden. Sie möchten, dass die Eingabedaten statt aus einer Datei auch aus Datenbanken oder Internetseiten stammen können.

Eine denkbare Lösung wäre es nun, einfach für die neuen Datenquellen neue, völlig unabhängige Klassen wie HttpReader und DBSource umzusetzen.

Diese mögliche Lösung ist in Abbildung 5.12 dargestellt. Diese Lösung führt jedoch zu Code-Redundanzen und damit zur Verletzung unseres Prinzips Wiederholungen vermeiden. Die drei Methoden werden sich inhaltlich nicht unterscheiden, da alle die Operation read aufrufen. Der einzige Unterschied liegt im Typ ihres Parameters.

Abbildung 5.12    (UML-Diagramm) Nicht empfehlenswerte Lösung durch Code-Redundanz

In einem statischen Typsystem haben Sie aber zwei Möglichkeiten, eine solche Erweiterung auch ohne Code-Redundanzen vorzunehmen.

Problematische Lösung: Neue Unterklassen

  • Möglichkeit 1 Sie setzen neue Unterklassen der Klasse File für die beschriebenen Datenquellen um. Allerdings müssten Sie bei diesen dann wiederum einige Fähigkeiten abklemmen, da die neuen Klassen keine Möglichkeit zum Springen an eine bestimmte Stelle oder zum Sperren einer Datei haben. Das könnten Sie umsetzen, indem Sie für die entsprechenden Methoden einfach eine Leerimplementierung oder eine Fehlermeldung einsetzen. In Abbildung 5.13 ist diese nicht empfehlenswerte Lösung dargestellt. Diese Variante verletzt das Prinzip der Ersetzbarkeit, da die Unterklassen den Kontrakt der Basisklasse nicht einhalten.

Abbildung 5.13    Verletzung des Prinzips der Ersetzbarkeit durch Einschränkung

Bessere Möglichkeit: Neue Oberklasse

  • Möglichkeit 2 Alternativ können Sie eine neue Superklasse Source zum Typ File deklarieren, die nur die benötigten Gemeinsamkeiten der Dateien und der anderen Datenströme enthält. Diese Lösung ist in Abbildung 5.14 dargestellt. Die Klassen HttpReader und DBSource sind dann Unterklassen von Source. Zudem passen Sie die Deklaration der Methode findLines so an, dass sie statt eines Dateiobjekts ein Objekt dieses neuen generalisierten Typs Datenstrom als Parameter erwartet.

Abbildung 5.14    Lösung durch neue Oberklasse

Diese zweite Alternative ist die bessere, weil hier das Prinzip der Ersetzbarkeit nicht verletzt wird, [Manchmal haben Sie diese Möglichkeit eben nicht. Zum Beispiel dann, wenn Sie die Quelltexte der Klasse File oder der Funktion, die Sie verwenden möchten, nicht ändern können. ] doch sie ist mit relativ viel Aufwand durch die Anpassung der Klassenhierarchie verbunden.

Dynamisches Typsystem erleichtert Anpassung.

In einer Sprache mit dynamischem Typsystem ist der Aufwand geringer. Sie implementieren einfach Klassen, welche die benötigte Funktionalität zum sequenziellen Lesen der Daten aus den verschiedenen Datenströmen bereitstellen, und rufen die Operation mit Exemplaren dieser neuen Klassen auf. In Abbildung 5.15 ist zu sehen, dass die Operation findLines gar nicht angepasst werden musste.

Abbildung 5.15    Anpassung bei dynamischem Typsystem

Da die Klassenzugehörigkeit der Parameter nicht formal festgelegt worden ist, genügt es völlig, dass die übergebenen Objekte die benötigte Operation read unterstützen.

Die Tatsache, dass Dateien und unsere neuen Klassen alle zu einer neuen Klasse Source gehören und dass die Operation findLines mit dieser neuen Klasse als Parameterwert umgehen kann, müssen Sie zur Dokumentation der Anwendung hinzufügen.

Um das Prinzip der Trennung der Schnittstelle von der Implementierung einzuhalten, müssen Sie auch die geänderten Vorbedingungen der Operation in Ihre Dokumentation aufnehmen.

Diskussion: Lösung für statisches Typsystem

Bernhard: Nun sind wir ja in der Praxis nicht zu häufig mit dynamisch typisierten Sprachen zugange. Ein relevanter Teil von uns arbeitet ja dann doch mit Java oder C++ und kann diesen Vorteil der dynamisch typisierten Sprachen nicht nutzen. Und ehrlich gesagt: Es kann auch ganz schön anstrengend sein, erst zur Laufzeit darauf zu stoßen, dass eine Operation von einem Objekt nun gerade nicht unterstützt wird.

Gregor: Ich möchte mich ganz bewusst aus dem Streit heraushalten, welches Typsystem besser sei, das statische oder das dynamische. Beide haben ihre Vorteile. Die große Flexibilität des dynamischen Typsystem kann durchaus die Produktivität steigern. Das erkennt jeder, der wegen einer kleinen Typänderung in C++ ein großes Projekt neu kompilieren muss. Andererseits kann die statische Typisierung mit geeigneten Werkzeugen bereits zur Entwicklungszeit helfen, wenn die Tools zum Beispiel zeigen können, welche Methoden auf einer Variablen aufgerufen werden können oder an welchen Stellen eine Methode verwendet wird. Mit welchem Typsystem man schneller und bequemer fährt, ist letzten Endes eine Frage der Entwicklungswerkzeuge, die man verwendet.

Downcasts können auf Modellierungsfehler deuten

In den statisch typisierten Sprachen gibt es in der Regel die Möglichkeit, den Typ eines Objekts zum Typ einer Unterklasse zu konvertieren. Diese Möglichkeit wird auch Downcast genannt.


Icon Hinweis Downcast

In einer statisch typisierten Programmiersprache können Sie ein Objekt, das bisher über eine Variable des Typs einer Oberklasse referenziert wurde, auch einer Variablen mit dem Typ einer Unterklasse zuweisen. Dazu müssen Sie das Objekt explizit in den Typ der Unterklasse konvertieren. Diese Konvertierung wird Downcast genannt, da die Konvertierung aus Sicht der Klassenhierarchie nach unten, also hin zu einer spezielleren Klasse erfolgt. Diese Konvertierung kann natürlich nur klappen, wenn das Objekt zur Laufzeit tatsächlich auch ein Exemplar der Unterklasse ist. Die meisten Programmiersprachen bieten Operationen an, die eine Konvertierung versuchen und einen Fehler signalisieren, wenn es sich bei dem Objekt nicht um ein Exemplar der angegebenen Unterklasse handelt. Beispiele für solche Operationen sind die Cast-Operatoren in Java oder der Operator dynamic_cast in C++.


Downcasts deuten auf Probleme.

Grundsätzlich sollten Sie in den meisten Programmen ohne Downcasts auskommen. Das Prinzip der Trennung der Schnittstelle von der Implementierung legt nahe, dass Sie mit den zur Verfügung stehenden Schnittstellen einer Klasse arbeiten. Wenn Sie aber explizite Downcasts verwenden, kompromittieren Sie die Schnittstelle und legen deren Implementierungen bloß. Sie machen Annahmen darüber, welche Klassen diese Schnittstelle implementieren.

Wenn Sie sich real existierenden Code anschauen, werden Sie allerdings doch recht häufig auf Downcasts stoßen. Es gibt zwar durchaus sinnvolle Verwendungen für Downcasts in technischen Komponenten wie Frameworks. Downcasts in reinen Anwendungsmodulen sollten aber misstrauisch machen.

Beispiel für Probleme mit Downcasts

Betrachten Sie im Folgenden ein Beispiel für Downcasts, das zu Problemen führt. In Abbildung 5.16 ist eine Klasse GeschaeftspartnerAnzeige dargestellt, welche die Klasse Geschaeftspartner als Schnittstelle verwendet.

Abbildung 5.16    Anzeige für Geschäftspartner verwendet Downcast.

Trotz der Verwendung der Schnittstelle differenziert die Methode anrede() aber intern nach Person und Organisation durch Verwendung von Downcasts und schafft dadurch zusätzliche Abhängigkeiten. In Listing 5.4 sehen Sie die zugehörige Umsetzung in der Sprache Java.

class GeschaeftspartnerAnzeige { 
    // ... 
  String anrede(Geschaeftspartner p) {   
    if (p instanceof Person)  
 
      return ((Person) p).vorname() 
           + " " + ((Person) p).nachname(); 
    if (p instanceof Organisation)  
      return ((Organisation) o).bezeichnung();  
   // und was ist mit anderen Geschäftspartnertypen? 
   return "Unbekannter Geschäftspartnertyp"; 
  } 
  // .. 
}

Listing 5.4    Typbestimmung zur Laufzeit

In diesem Beispiel haben Sie in Zeile eine Methode anrede() vorliegen, welche die Anschriften eines Geschäftspartners ausgibt. Anhand der Typzugehörigkeit des Geschäftspartners entscheiden Sie, ob eine Bezeichnung der Organisation oder der Vor- und Nachname einer Person gedruckt werden sollen. Handelt es sich um eine Person (Zeile ), werden Vorname und Nachname verwendet. Dazu erfolgt in Zeile ein Downcast auf die Unterklasse Person. Handelt es sich um eine Organisation (Zeile ), wird die Bezeichnung der Organisation verwendet. Dazu erfolgt dann in Zeile ein Downcast auf die Unterklasse Organisation. Die Motivation für dieses Vorgehen ist es, dass die Methode die Anschriften aller möglichen Geschäftspartner ausgeben soll.

Es entsteht durch dieses Vorgehen aber ein Problem: Wenn Sie später eine neue Art von Geschäftspartner implementieren, müssen Sie in diese Methode eingreifen, um auch diesen ausgeben zu können. Aber werden Sie diese Stelle dann überhaupt finden? Und was ist mit den ganzen anderen Stellen im Code, an denen möglicherweise genau die gleiche Unterscheidung stattfindet? Die Verwendung des Downcasts deutet darauf hin, dass Sie das Prinzip der Trennung der Schnittstelle von der Implementierung verletzen.

Modellierung ohne Downcast

Durch eine Anpassung der Modellierung können Sie die Notwendigkeit des Downcasts beseitigen. Es gehört in diesem Beispiel zu den gemeinsamen Eigenschaften aller Geschäftspartner, dass sich für sie eine Anrede formulieren lässt. Da es aber eine gemeinsame Eigenschaft aller Geschäftspartner ist, gehört es auch zur Schnittstelle der Geschäftspartner. In Abbildung 5.17 ist die korrigierte Version dargestellt.

Abbildung 5.17    Korrigierte Version ohne Downcast

Diese vermeidet die beschriebenen Probleme. Wenn Sie jetzt eine neue Art von Geschäftspartner implementieren, werden Sie dort eine Methode anrede() umsetzen. Die Notwendigkeit der Downcasts entfällt, und die Methode in der Klasse GeschaeftspartnerAnzeige kann angepasst werden:

String anrede(Geschaeftspartner p) { 
    return p.anrede(); 
}

Immer wenn es möglich ist, sollten Sie also auf Downcasts verzichten. Meistens ist es besser, dynamische Polymorphie und das Prinzip der Ersetzbarkeit zu nutzen. Downcasts werden in der Praxis meist dann eingesetzt, wenn eine Modellierung eigentlich geändert werden müsste, also eine Anpassung der eigentlich verwendeten Schnittstelle benötigt würde, dies aber aus praktischen Gründen nicht möglich ist, zum Beispiel weil diese Schnittstelle gar nicht in Ihrer Verantwortung liegt. Wenn Sie in unserem Beispiel nur für die Klasse GeschaeftspartnerAnzeige zuständig wären und die Schnittstelle der Klasse Geschaeftspartner auf keinen Fall geändert werden kann, wäre ein Downcast die einzige Möglichkeit.

Aber in so einem Fall sollten Sie sich immer klar machen, warum Sie dieses Vorgehen wählen. In den Fällen, in denen Sie die Modellierung der Schnittstelle ändern können, sollten Sie es auch tun.


Rheinwerk Computing - Zum Seitenanfang

5.1.6 Sichtbarkeit im Rahmen der Vererbung  topZur vorigen Überschrift

In Abschnitt 4.2.5, »Sichtbarkeit von Daten und Methoden«, haben Sie bereits die verschiedenen Sichtbarkeitsstufen für Eigenschaften und Methoden eines Objekts im Überblick kennen gelernt. Die Sichtbarkeitsstufe »Geschützt« beschäftigt sich mit der Sichtbarkeit von Methoden, die nicht zur Schnittstelle gehören, aber innerhalb von Methodenimplementierungen von abgeleiteten Klassen aufgerufen werden können.

Sichtbarkeitsstufe »Geschützt« (protected)

Wie schon bei der Sichtbarkeitsstufe »Privat« müssen wir auch hier wieder zwischen der klassenbasierten und der objektbasierten Definition der Sichtbarkeit unterscheiden.


Sichtbarkeitsstufe »Geschützt«

Auf geschützte Daten und Methoden dürfen neben den Methoden, die in derselben Klasse implementiert sind, auch die Methoden, die in ihren Unterklassen implementiert sind, zugreifen.


Datenelemente können von Basisklasse geerbt werden.

Die Daten und Methoden eines Objekts können in der Klasse, zu der das Objekt direkt gehört, deklariert worden sein. Die Elemente können aber auch von einer ihrer Basisklassen geerbt worden sein.

Java, C# oder C++ folgen grundsätzlich diesem klassenbasierten Sichtbarkeitskonzept. Allerdings dürfen in Java auf geschützte Elemente nicht nur die Unterklassen zugreifen, sondern zusätzlich alle Klassen im gleichen Package.

C#

Hier ein Beispiel für den Zugriff auf geschützte Datenelemente in C#:

class B { 
  private int x; 
  protected int y; 
} 
 
class C: B { 
  public void test() { 
    x = 1; # Fehler. Privates x ist nicht in C deklariert 
    y = 2; # OK. Geschütztes y ist in der 
           # Oberklasse B deklariert 
  } 
}

Listing 5.5    Zugriff auf geschützte Datenelemente in C#

In Java, C# und C++ bezieht sich auch die Sichtbarkeitsstufe »Geschützt« nicht auf einzelne Objekte, sondern auf ganze Klassen.

class D: B { 
  public void test(B b, D d) { 
    d.y = 1; // OK. d gehört zur Klasse D 
    b.y = 2; // Fehler. 
    // b gehört nicht notwendigerweise zur Klasse D 
  } 
}

Listing 5.6    Sichtbarkeitsstufe »Geschützt« an einem Beispiel in C#

Sichtbarkeit der Vererbungsbeziehung

Die Verwendung von Sichtbarkeitsregeln ist nicht nur für Methoden und Datenelemente möglich. Konzeptuell sind Sichtbarkeitsregeln auch für die Beziehungen zwischen Klassen und abgeleiteten Klassen möglich.

Der Standardfall ist es, dass die Vererbungsbeziehung öffentlich (public) ist: Es ist öffentlich sichtbar, dass die Unterklasse die Schnittstelle der Oberklasse ebenfalls unterstützt. Das ist das Wesen der Vererbung der Spezifikation.

Es kann aber auch Fälle geben, in denen diese Vererbung nach außen nicht sichtbar sein soll. Die Vererbungsbeziehung selbst ist dann privat. Diese Art der Vererbung wird allerdings nur von wenigen Programmiersprachen unterstützt.


Icon Hinweis Private Vererbung

Wenn eine Klasse B von einer Klasse A privat erbt, so erbt sie nicht die Spezifikation dieser Klasse. Nach außen ist also nicht sichtbar, dass ein Exemplar von B auch ein Exemplar von A ist. Innerhalb der Methoden von B können aber alle Operationen von A genutzt werden.


Private Vererbung in C++

Lassen Sie uns die private Vererbung am Beispiel von Vererbungsbeziehungen in der Sprache C++ etwas genauer betrachten. Wenn Sie eine Unterklasse B einer Oberklasse A vorliegen haben, können Sie in C++ bestimmen, ob die Tatsache, dass die Klasse B eine Unterklasse von A ist, nach außen sichtbar sein soll.

Ist die Sichtbarkeit der Vererbung privat, so erbt die Klasse B zwar die Funktionalität der Klasse A, sie erbt jedoch nach außen keine Verpflichtungen der Klasse A – denn nach außen ist die Klasse B eben keine Unterklasse von A. Die Exemplare von B müssen also nicht mit der Spezifikation von A konform sein und sind nicht an das Prinzip der Ersetzbarkeit gebunden.

Private Vererbung vs. Delegation

Die private Vererbung lässt sich immer in eine Delegationsbeziehung umwandeln, so dass jedes Exemplar von B ein Exemplar von A oder einer geeigneten Unterklasse besitzt. Für eine solche Struktur braucht man zwar etwas mehr Quelltext, dafür ist sie aber etwas verständlicher und eindeutiger. Abbildung 5.18 zeigt ein Beispiel für eine private Vererbungsbeziehung.

Abbildung 5.18    Private Vererbung in C++

Umwandlung private Vererbung in Delegation

Die private Vererbung lässt sich in die Delegationsbeziehung umwandeln, die in Abbildung 5.19 dargestellt ist. Das äußere Verhalten in der Methode B::testB bleibt dabei dasselbe.

Abbildung 5.19    Private Vererbung durch Delegation ersetzt

Neben der öffentlichen und privaten Sichtbarkeit der Vererbung gibt es in C++ noch die geschützte Sichtbarkeit der Vererbung. Hier ist die Vererbung von außen nicht sichtbar, die Unterklassen der Klasse B sehen aber, dass A eine Oberklasse von B ist.

Diskussion: Was soll denn die private Vererbung?

Bernhard: Bei privater Vererbung können wir doch eigentlich gar nicht mehr von Vererbung sprechen. Es ist ja gerade Sinn der Vererbung, dass eine Unterklasse eben auf alle Pflichten der Oberklasse eingeht. Wenn dies nicht so ist, haben wir eine simple Delegationsbeziehung vorliegen und sollten diese auch explizit machen.

Gregor: Ich denke, diese Entscheidung kann man getrost einem Entwicklungsteam überlassen. Die private Vererbung kommt mit einem guten Stück weniger Code aus, da eine Reihe von Zugriffen nicht per Delegation weitergereicht werden müssen. Es sollte aber eine bewusste Designentscheidung in einem Team sein, für bestimmte Aufgaben private Vererbung zu nutzen. Private Vererbung kann außerdem als eine Art von Mixin genutzt werden. Wir mischen dabei intern verfügbare Funktionalität zu anderen Klassen hinzu.

Differenzierter Zugriff auf einzelne Klassen

Manchmal muss ein Objekt aus technischen Gründen anderen Objekten den Zugriff auf bestimmte Daten und Methoden ermöglichen, auch wenn diese nicht zu der spezifizierten Schnittstelle gehören. Die Sprache C++ hat für diese Beziehung die sympathische Beschreibung gewählt, dass Klassen, denen dieser Zugriff speziell erlaubt wird, befreundete Klassen sind.

Um eine sehr sinnvolle Verwendung dieses differenzierten Zugriffs zu erläutern, werden wir zunächst ein Beispiel vorstellen, bei dem eine Sammlung von Objekten Iteratoren bereitstellt, mit denen die Sammlung sukzessive durchlaufen werden kann.


Icon Hinweis Iteratoren

Ein Iterator ist ein Objekt, das es ermöglicht, eine Sammlung von anderen Objekten zu durchlaufen, ohne den Zustand der Sammlung selbst zu verändern. Iteratoren kapseln damit eine Sammlung und präsentieren diese nach außen so, als hätte diese einen Zustand, der ein aktuelles Element identifiziert.


Iteratoren bieten in der Regel Operationen, um auf das aktuelle Element zuzugreifen und um die Sammlung sukzessive zu durchlaufen.

Die Spezifikation der Schnittstelle einer Sammlung könnte in Java so aussehen: [Java hat eine umfassende Sammlungsbibliothek, hier beschreiben wir nur ein sehr vereinfachtes Beispiel, nicht die Standardklassen aus Java. ]

public interface Collection { 
  public abstract Iterator iterator(); 
  public abstract void add(Object element); 
} 
public interface Iterator { 
  public abstract boolean hasNext(); 
  public abstract Object next(); 
}

Listing 5.7    Schnittstellen-Klassen für Sammlungen und Iteratoren

Jede Sammlung (Collection) hat die Methode iterator(), die einen neuen Iterator zurückgibt. Die Methode next() liefert nach und nach immer das nächste Objekt der Sammlung, so lange, bis deren Methode hasNext() den Wert false zurückgibt. Auf diese Weise können Sie eine Sammlung mehrfach parallel abzählen.

Verlinkte Liste als Sammlung

Eine einfache Implementierung einer Sammlung ist die verlinkte Liste. Jeder Eintrag der verlinkten Liste referenziert ein in der Sammlung enthaltenes Objekt und den nächsten Eintrag in der Liste. In Abbildung 5.20 ist das Zusammenspiel der beteiligten Klassen aufgeführt.

Abbildung 5.20    Zusammenspiel zwischen Iteratoren und Sammlungen

Ein Iterator verwaltet in diesem Beispiel einen Eintrag currentEntry, der auf das aktuelle Listenelement zeigt. Der Iterator benötigt dabei Zugriffe auf die Daten des Listenelements, um das darin enthaltene Element zu erfragen oder um das aktuelle Element weiterzubewegen auf den Folgeeintrag in der Liste. Dafür stellt die Klasse LinkedListEntry die Operationen getItem() und getNextEntry() zur Verfügung.

Die Anwendung eines Iterators ist in Listing 5.8 dargestellt. Dort werden zunächst drei Elemente einer Liste hinzugefügt. Die entsprechenden Zeilen sind mit markiert.

String text1 = new String("erster Listeneintrag"); 
String text2 = new String("zweiter Listeneintrag"); 
String text3 = new String("dritter Listeneintrag"); 
 
LinkedList list = new LinkedList(); 
list.add(text1);    
list.add(text2);    
list.add(text3);    
 
Iterator iterator = list.iterator();   
while (iterator.hasNext()) {   
    String text = (String)iterator.next(); 
    System.out.println(text); 
}

Listing 5.8    Verwendung des Iterators

Anschließend wird die Liste in Zeile nach ihrem Iterator befragt und dieser verwendet, um in Zeile die Liste zu durchlaufen und jedes Element ausgeben zu lassen. [In Java ab der Version 5 lassen sich für die Umsetzung von Sammlungen und Iteratoren auch parametrisierte Klassen (Generics) verwenden. In unserem Beispiel haben wir darauf verzichtet, um die Darstellung einfacher zu halten. ]

In Listing 5.9 ist eine Implementierung aufgeführt, welche die in Abbildung 5.20 vorgestellten Klassen für Sammlungen und Iteratoren umsetzt.

public class LinkedList implements Collection { 
  private LinkedListEntry firstEntry; 
  public Iterator iterator() { 
    return new LinkedListIterator(firstEntry); 
  } 
  // .. Methode add() weggelassen 
} 
 
public class LinkedListEntry { 
  private Object item;    
  private LinkedListEntry nextEntry;   
  public Object getItem() {  
    return item; 
  } 
  public LinkedListEntry getNextEntry() {   
    return nextEntry; 
  } 
 
  ... // weitere Methoden, die Initialisierung zum Beispiel 
} 
 
public class LinkedListIterator implements Iterator { 
  private LinkedListEntry currentEntry; 
  public LinkedListIterator(LinkedListEntry firstEntry) { 
    currentEntry = firstEntry; 
  } 
  public boolean hasNext() { 
    return currentEntry != null; 
  } 
  public Object next() {    
    Object result = currentEntry.getItem(); 
    currentEntry = currentEntry.getNextEntry(); 
    return result; 
  } 
}

Listing 5.9    Zugriff auf Listenelemente in Java

Iterator braucht Zugriff auf Daten der Liste.

Die Klasse LinkedListEntry muss den Zugriff auf ihre Elemente item und nextEntry, die in den mit markierten Zeilen deklariert sind, zumindest lesend ermöglichen, sonst könnte der Iterator nicht darauf zugreifen. Deshalb wird in den mit markierten Zeilen der Zugriff über die Operationen getItem() und getNextEntry() erlaubt. So kann die Iteratorklasse selbst in der Methode next() in Zeile darauf zugreifen und so ein jeweils aktuelles Element verwalten.

Datenkapselung verletzt

Aber warum sollten auch andere Klassen einen Zugriff auf diese Elemente der Klasse LinkedListEntry erhalten? Schließlich handelt es sich hier nur um die Implementierungsdetails der verlinkten Liste, nicht um ein spezifiziertes Verhalten. Gleiches gilt auch für den Konstruktor der Klasse LinkedListIterator. Nur die Klasse LinkedList muss Exemplare von LinkedListIterator erstellen können, für beliebige andere Klassen sollte dies nicht möglich sein.

Implementierungseinheit

Die Klassen LinkedList, LinkedListEntry und LinkedListIterator bilden zusammen eine Implementierungseinheit. Für die Klassen dieser Einheit untereinander sollten lockerere Sichtbarkeitsregeln gelten als für Klassen von außerhalb.

Als ersten Schritt könnte man die Sichtbarkeit der Klassen LinkedListEntry und LinkedListIterator und ihrer Methoden getItem() und getNextEntry() beziehungsweise des Konstruktors von LinkedListIterator von Öffentlich (Schlüsselwort public) auf Geschützt innerhalb des Packages (ohne ein Schlüsselwort) reduzieren. Dies würde bewirken, dass nur Klassen aus demselben Package den Zugriff erhalten.

Dies ist aber meistens immer noch zu viel, denn schließlich werden in dem Package noch andere Implementierungen der Schnittstelle Sammlung liegen.

Eine gute Alternative bieten hier die geschachtelten Klassen. Dabei wird die Sichtbarkeit einer Klasse so eingeschränkt, dass sie nur innerhalb genau einer anderen Klasse sichtbar ist.


Icon Hinweis Geschachtelte Klassen (engl. Nested Classes oder Inner Classes)

Geschachtelte Klassen sind Klassen, die innerhalb einer anderen Klasse deklariert werden. Die geschachtelte Klasse erhält den Zugriff auf alle Elemente der äußeren Klasse, sogar auf die privaten Elemente. Die äußere Klasse erhält den vollen Zugriff auf die Elemente ihrer geschachtelten Klassen. Die geschachtelte Klasse ist dabei selbst ein Element der äußeren Klasse. Sie selbst kann für andere Klassen – wie andere Elemente der äußeren Klasse auch – sichtbar oder unsichtbar gemacht werden.


Icon Beispiel Geschachtelte Klassen in Java

Java bietet die Möglichkeit, geschachtelte Klassen zu nutzen. [Auch C# unterstützt geschachtelte Klassen. Dabei gelten ähnliche Sichtbarkeitsregeln wie in Java. ] Somit lässt sich das Beispiel aus Abbildung 5.20 auch über eine geschachtelte Klasse realisieren. In Listing 5.10 ist das modifizierte Beispiel zu sehen.

public class LinkedList implements Collection { 
 
  // ... 
  // innere Klasse, von außen nicht sichtbar 
  private static class LinkedListEntry {   
    Object item; 
    LinkedListEntry nextEntry; 
  } 
 
  // diese innere Klasse ist von außen auch nicht sichtbar 
  private static class LinkedListIterator  
  implements Iterator { 
 
    private LinkedListEntry currentEntry; 
    LinkedListIterator(LinkedListEntry firstEntry) { 
      currentEntry = firstEntry; 
    } 
    public boolean hasNext() { 
      return currentEntry != null; 
    } 
    public Object next() { 
      Object result = currentEntry.item; 
      currentEntry = currentEntry.nextEntry; 
      return result; 
    } 
  } 
}

Listing 5.10    5.10 Geschachtelte Klassen

In Listing 5.10 sind die Datenelemente item und nextEntry von außen nicht sichtbar, weil die geschachtelte Klasse LinkedListEntry in Zeile privat ist. Sie können hier also auf die Lesemethoden getItem() und getNextEntry() verzichten, weil die ebenfalls geschachtelte Klasse LinkedListIterator, die in Zeile zu sehen ist, direkt auf diese Dateneinträge zugreifen kann. Nach außen ist aber weder die Klasse LinkedListEntry noch die Klasse LinkedListIterator sichtbar. Alle Anwender der Klasse LinkedList werden alleine mit der Schnittstellen-Klasse Iterator arbeiten.

Geschachtelte Klassen in C# und C++

C++ unterstützt auch geschachtelte Klassen, allerdings gelten für die geschachtelten Klassen dieselben Sichtbarkeitsregeln wie für alle anderen Klassen. Damit sind diese nicht nutzbar, um wie in Java damit die Kapselung von Daten zu erreichen.

Wie bereits am Anfang dieses Kapitels erwähnt, bietet C++ einen anderen Mechanismus, um einer anderen Klasse den Zugriff auf Interna einer Klasse zu erlauben: Die Klasse deklariert, dass die privilegierte Klasse ihr »Freund« ist.

Im Beispiel aus Abbildung 5.20 in Java haben wir eine Sammlung von Objekten der Klasse Object implementiert, denn alle Klassen in Java sind direkte oder indirekte Unterklassen der Klasse Object. In C++ gibt es eine solche generelle Oberklasse nicht. Daher enthält unser C++-Beispiel parametrisierte Klassen (Templates) mit dem Typparameter T. Parametrisierte Klassen haben wir bereits in Abschnitt 4.2.3 vorgestellt.

Icon Beispiel Geschachtelte Klassen in C++

Hier das Beispiel der Klasse LinkedListEntry in C++:

template<typename T> class LinkedListEntry { 
private: 
    T* item; 
    LinkedListEntry* nextEntry; 
  friend class LinkedListIterator<T>;   
}; 
template<typename T> class LinkedListIterator { 
private: 
    LinkedListEntry<T>* currentEntry; 
    LinkedListIterator(LinkedListEntry<T>* firstEntry) { 
        currentEntry = firstEntry; 
    } 
public: 
    bool hasNext() { 
        return currentEntry != NULL; 
    } 
    T* next() {    
        T* result = currentEntry->item; 
        currentEntry = currentEntry->nextEntry; 
        return result; 
    } 
  friend class LinkedList<T>;  
};

Listing 5.11    Differenzierter Zugriff in C++

In Zeile erlaubt die Klasse LinkedListEntry der Klasse LinkedListIterator den Zugriff auf ihre internen Daten. [Durch die Angabe des Typparameters T wird diese Freigabe allerdings auf die Iteratoren eingeschränkt, die Exemplare der gleichen Klasse verwalten, wie das für LinkedListEntry der Fall ist. ] Was noch schöner ist: Die Freundschaft wird auch erwidert. Durch den Eintrag in Zeile erklärt auch die Klasse LinkedListIterator die Klasse LinkedListEntry zum Freund. Nun kann in Zeile innerhalb der Methode next() der Klasse LinkedListIterator auf private Datenelemente der Klasse LinkedListEntry zugegriffen werden. Diese braucht keine öffentlichen Operationen mehr dafür bereitzustellen.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

 <<   zurück
  Zum Rheinwerk-Shop
Neuauflage: Objektorientierte Programmierung






Neuauflage:
Objektorientierte Programmierung

Jetzt Buch bestellen


 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Rheinwerk-Shop: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Rheinwerk-Shop: C++ Handbuch






 C++ Handbuch


Zum Rheinwerk-Shop: Einstieg in Python






 Einstieg in Python


Zum Rheinwerk-Shop: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern