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


Rheinwerk Computing - Zum Seitenanfang

5.4 Mehrfachvererbung  Zur nächsten ÜberschriftZur vorigen Überschrift

Objekte können die Spezifikation von mehreren Klassen erfüllen. Ein solches Objekt ist in diesem Fall Exemplar von mehreren Klassen. Dies ist trivial, wenn die betroffenen Klassen in direkter oder indirekter Vererbungsbeziehung stehen.

Doch es gibt auch andere Fälle: Ein Objekt kann durchaus auch die Spezifikation von mehreren Klassen erfüllen, die zueinander nicht in Vererbungsbeziehung stehen. In diesen Fällen sprechen wir von Mehrfachvererbung.

Wie generell beim Thema Vererbung, so ist es auch bei der Mehrfachvererbung ein grundlegender Unterschied, ob mehrfach von Spezifikationen oder von Implementierungen geerbt wird. Im folgenden Abschnitt erläutern wir Anwendungen und Probleme dieser beiden Varianten.


Rheinwerk Computing - Zum Seitenanfang

5.4.1 Mehrfachvererbung: Möglichkeiten und Probleme  Zur nächsten ÜberschriftZur vorigen Überschrift

Wir gehen in diesem Abschnitt zunächst kurz auf die Mehrfachvererbung von Spezifikationen ein, um dann einen konkreten Anwendungsfall für Mehrfachvererbung der Implementierung zu betrachten.

Mehrfachvererbung der Spezifikation

Um die Mehrfachvererbung der Spezifikation zu erläutern, greifen wir unser Beispiel mit den Steuerelementen einer Benutzeroberfläche wieder auf. In Abbildung 5.52 ist eine Erweiterung dieses Beispiels dargestellt, in der auch die Klasse Darstellbar zum Einsatz kommt. Dabei erfüllt ein Exemplar der Klasse Menü sowohl die Spezifikation der Klasse Steuerelement als auch der Klasse Darstellbar.

Ein Exemplar der Klasse Menü ist also ein Exemplar der Klasse Steuerelement, doch gleichzeitig ist es auch ein Element der Klasse aller auf dem Bildschirm darstellbaren Objekte. Das Objekt gehört also auch zu der Klasse Darstellbar. Warum nun also nicht die Klassen Steuerelement und Darstellbar in eine Spezialisierungsbeziehung bringen?

Abbildung 5.52    Die Klasse »Menü« ist eine Unterklasse sowohl der Klasse »Steuerelement« als auch der Klasse »Darstellbar«.

Klassen »Steuerelement« und »Darstellbar« sind unabhängig.

In unserer Beispielanwendung sind nicht nur die Steuerelemente darstellbar. Unsere Anwendung kann auch Texte, Bilder, Animationen und Filme darstellen. Also ist die Klasse Steuerelement keine Oberklasse der Klasse Darstellbar. Manche Steuerelemente in unserer Anwendung, die Tastenkürzel und die Sprachbefehle, können nicht dargestellt werden. Also ist die Klasse Steuerelement keine Unterklasse der Klasse Darstellbar. Die beiden Klassen sind also völlig unabhängig und stehen in keiner Vererbungsbeziehung.

Damit ist die Klasse Menü eine Unterklasse sowohl der Klasse Steuerelement als auch der Klasse Darstellbar. Die Klasse Menü erbt die Spezifikation der beiden anderen Klassen.

Solange Sie die Klassen einfach im Rahmen der Vererbung der Spezifikation betrachten, ist auch die Mehrfachvererbung eine konzeptionell einfache Angelegenheit.

In unserem Beispiel gehört jedes Exemplar der Klasse Menü sowohl zu der Klasse Steuerelement als auch zu der Klasse Darstellbar. Es muss deshalb die Spezifikation beider Oberklassen erfüllen und für alle spezifizierten Operationen eine Methode bereitstellen. Die Mehrfachvererbung der Spezifikation bedeutet nur, dass sich die Exemplare gemeinsamer Unterklassen an alle Verpflichtungen aller vererbten Klassen halten. Damit ist die Mehrfachvererbung der Spezifikation konzeptuell nicht grundsätzlich verschieden von einfacher Vererbung.

Mehrfachvererbung der Implementierung

Die Mehrfachvererbung der Implementierung ist vor allem dann sinnvoll, wenn die Oberklassen Funktionalität aus unterschiedlichen, sich nicht überlappenden Bereichen bereitstellen. Wir erläutern das am besten an einem Beispiel, nämlich dem Entwurfsmuster des Beobachters. Dazu stellen wir zunächst das Entwurfsmuster selbst vor.


Icon Hinweis Entwurfsmuster »Beobachtetes–Beobachter« (engl. Observable-Observer)

Das Muster »Beobachtetes–Beobachter« (oder auch nur kurz »Beobachter«) wird verwendet, wenn mehrere Objekte – die Beobachter – über die Änderungen eines anderen Objekts – dem Beobachteten – benachrichtigt werden sollen. Bei der Anwendung dieses Musters verwaltet das beobachtete Objekt eine Sammlung von Referenzen auf seine Beobachter. Das beobachtete Objekt braucht dabei nicht die konkreten Implementierungen der Beobachter zu kennen, es reicht, dass es ihre Beobachter-Schnittstelle kennt. Bei jeder Änderung des beobachteten Objekts benachrichtigt dieses alle seine Beobachter, die dann ihren eigenen Zustand aktualisieren können.


Ein beliebtes Beispiel für den Einsatz des Musters »Beobachtetes-Beobachter« ist die Darstellung von sich ändernden Dokumenten. Zum Beispiel kann ein HTML-Dokument auf verschiedene Arten dargestellt werden: In einem Fenster kann man seinen Quelltext darstellen, in einem anderen Fenster kann man seine Vorschau sehen, und wieder ein anderes Fenster kann seine Struktur darstellen.

Wenn das Dokument nun geändert wird, sollten alle seine Darstellungen automatisch angepasst werden, indem alle Beobachter des Dokuments (die verschiedenen Sichten) über die Änderung benachrichtigt werden. [Im Abschnitt 8.2, »Die Präsentationsschicht: Model, View, Controller (MVC)«, werden wir das MVC-Muster vorstellen, durch das die Interaktion zwischen den dargestellten und den darstellenden Komponenten weiter strukturiert wird. ]

Eine möglicher Entwurf dafür könnte aussehen wie in Abbildung 5.53. Dabei stellt die Klasse BeobachtbaresHtmlDokument Methoden für zwei unterschiedliche Bereiche zur Verfügung. Die Methoden an Markierung bieten nicht-HTML-spezifische Funktionalität. Sie sind nur für das »beobachtbar sein« zuständig. Die Methoden hinter der Klammer mit der Markierung besitzen HTML-spezifische Funktionalität und könnten vielleicht auch in einer anderen Anwendung ohne Beobachter verwendet werden.

Problem: Zwei Gruppen von Methoden

Die gewählte Modellierung ist praktikabel, allerdings hat sie einen Nachteil. Die Klasse BeobachtbaresHtmlDokument enthält zwei Gruppen von Methoden: In einer Gruppe sind die HTML-relevanten Methoden, in der anderen die Methoden, welche die Beobachter des Dokuments verwalten. Getreu dem Prinzip einer einzigen Verantwortung sollten wir diese zwei Funktionalitätsgruppen in zwei Quelltextmodule aufteilen – in unserem Falle also zwei Klassen. Die eine Klasse sollte sich ausschließlich um die HTML-relevanten Belange kümmern. Die andere Klasse sollte die Verwaltung der Beobachter übernehmen. Der große Vorteil wäre, dass sie dann auch bei anderen beobachteten Objekten eingesetzt werden kann.

Abbildung 5.53    Beobachter eines beobachtbaren HTML-Dokuments

In Abbildung 5.54 ist eine Modellierung aufgeführt, die genau das leistet: Die Klasse BeobachtbaresHtmlDokument erbt ihre Funktionalität von zwei Klassen: HtmlDokument stellt die HTML-spezifische Funktionalität zur Verfügung (siehe Markierung ), und die Klasse Beobachtbares sorgt für die benötigte Funktionalität im Rahmen des Beobachtermusters (siehe Markierung ).

Umsetzung in Java und C#

Nun, leider ist diese Klassenstruktur nicht in jeder Programmiersprache so direkt umsetzbar. In Java oder in der aktuellen Version von C# kann eine Klasse die Implementierung maximal von einer Klasse erben. Unsere Klasse BeobachtbaresHtmlDokument müsste sich also entscheiden: Von welcher der Klassen Beobachtbares und HtmlDokument soll sie ihre Funktionalität erben, und welche soll sie über eine Komponente nutzen? Dies ist eine unangenehme Entscheidung, denn fachlich gesehen sind die Exemplare der Klasse BeobachtbaresHtmlDokument Exemplare sowohl der Klasse Beobachtbares als auch der Klasse HtmlDokument. Die Klasse BeobachtbaresHtmlDokument ist also fachlich gesehen eine Unterklasse der beiden Oberklassen – und wenn sie schon die Implementierung der Methoden beider Klassen nicht erben kann, so soll sie zumindest ihre Spezifikation und ihre Schnittstellen erben.

Abbildung 5.54    Ein HTML-Dokument mit geerbter Beobachtbarkeit

Im beschriebenen Beispiel liegt also eine sinnvolle Anwendung für die Mehrfachvererbung von Implementierungen vor. Allerdings lassen sich durch die Verwendung der Mehrfachvererbung auch grobe Schnitzer in ein Modell einbauen, wenn das Prinzip der Ersetzbarkeit nicht auch hier strikt beachtet wird. Im folgenden Abschnitt stellen wir ein Beispiel vor, bei dem Mehrfachvererbung zwar möglich ist, aber zu sehr fragwürdigen Effekten führt.

Missbrauch bei Mehrfachvererbung der Implementierung

Marsroboter

Stellen Sie sich als Beispiel vor, die NASA hat sich entschieden, ihre Marsmission zu überdenken, und ist zum den Schluss gekommen, dass es zu gefährlich ist, Menschen dort hinzuschicken, und die Kosten dafür viel zu hoch sind. Stattdessen lässt die NASA einen Roboter entwickeln, der die Astronauten ersetzen soll. Der Vertriebsabteilung Ihrer Firma ist es gelungen, die NASA-Entscheider davon zu überzeugen, dass Sie die Software günstiger entwickeln können als die Konkurrenz.

Jetzt schreiben Sie ein System, das diesen Roboter steuert. Eine seiner wichtigsten Fähigkeiten ist es, Schnappschüsse von der Marsoberfläche zu machen und sie zurück zur Erde zu schicken.

Methode shoot

Die Fähigkeit, Schnappschüsse zu schießen, deklarieren Sie als eine Operation shoot in der Klasse Camera. Ihr Marsroboter wird also eine Unterklasse der Klasse Camera in der Sie die Methode shoot implementieren.

Sie und Ihre Kollegen sind klug und fleißig, und Ihre Arbeit kommt gut voran. Doch die Vertriebsabteilung schläft auch nicht. Es ist ihr gelungen, die geplante Software auch an andere Kunden zu verkaufen, die einen ähnlichen Roboter für andere gefährliche Aufgaben nutzen möchten. Natürlich müssen Sie dazu die Funktionalität etwas erweitern. Eine neue Fähigkeit des Roboters ist es, mit einem Maschinengewehr agieren zu können.

Sie wählen eine Lösung, in der die Klasse Roboter auch als eine Unterklasse der Klasse Gun modelliert ist, die eine Operation zum Abfeuern einer Salve deklariert. Diese Operation haben Sie sinnigerweise auch shoot genannt.

Abbildung 5.55    Liebe Kinder: nicht zu Hause nachbauen. Gefährliches Design eines Roboters.

Gruppenfoto?

Wäre dies tatsächlich das von Ihnen entworfene Design (dargestellt auch in Abbildung 5.55), müssten Sie dem Vertrieb ein großes Kompliment machen. Das Gruppenfoto der Entwickler sollten Sie allerdings lieber nicht dem von Ihnen programmierten Roboter überlassen.

Obwohl das beschriebene Design also durch Mehrfachvererbung der Implementierung ermöglicht würde, ist es natürlich nicht sinnvoll. Die Kompositionsbeziehung aus Abbildung 5.56 bildet diesen Fall sicherlich besser ab.

Dabei wird der Roboter nicht über Vererbung, sondern über Komposition aus den Komponenten Camera und Gun zusammengesetzt.

Abbildung 5.56    Komposition ist in diesem Fall sicherer.

Diskussion: Ist Mehrfachvererbung sinnvoll?

Bernhard: Wir würden doch in der Praxis nicht wirklich einen Roboter aus seinen verschiedenen Bestandteilen zusammenerben. Hier haben wir auch gleich ein offensichtliches Beispiel für einen Missbrauch von Vererbungsbeziehungen, den uns Programmiersprachen nun mal ermöglichen.

Gregor: Da hast du allerdings Recht. Allerdings sind nicht alle Fehler bei der Nutzung von Mehrfachvererbung so einfach zu erkennen wie der aus unserem Beispiel.Aber das heißt auch noch lange nicht, dass eine sinnvoll genutzte Mehrfachvererbung auch von Implementierungen nicht zu besser strukturierten Programmen führen kann. Vor allem der Mechanismus der Klassenerweiterung, der normalerweise Mixin [Klassenerweiterungen (Mixins) erläutern wir in Abschnitt 5.4.3, »Mixin-Module statt Mehrfachvererbung«. ] genannt wird, ist oft ein sinnvolles Mittel, das es uns erlaubt, separate Anliegen auch sinnvoll zu trennen.

Bernhard: Warum gehen dann so populäre Sprachen wie Java und C# so restriktiv mit der Mehrfachvererbung um? Gerade bei Java wird diese Restriktion am Beispiel der Observer-Klasse eigentlich recht deutlich. Ich muss mich entscheiden, ob ich von Observer oder innerhalb einer fachlichen Klassenhierarchie erben möchte. Mit Mehrfachvererbung von implementierenden Klassen wäre das kein Problem. Das wäre ein klassischer Fall für eine Mixin-Klasse.

Gregor: Es hat natürlich relevante Vorteile, die Mehrfachvererbung von Implementierungen auszuklammern. Wir schließen damit auf einfache Weise eine ganze Reihe von möglichen Problemen aus und machen die Implementierung der Sprache einfacher. Aber bei C# wurde wohl bereits erkannt, dass ein komplettes Ausklammern der Mehrfachvererbung über das Ziel hinausschießt. Es gibt zumindest Überlegungen, Verfahren zur Klassenerweiterung in eine Folgeversion aufzunehmen.

Im Beispiel des Roboters ist also die Komposition sicher die bessere Lösung, auch wenn rein technisch eine Lösung über Mehrfachvererbung möglich wäre.

Am Beispiel der Anwendung des Musters »Beobachter« aus Abbildung 5.54 ist aber deutlich geworden, dass sich auch die Mehrfachvererbung der Implementierung sinnvoll einsetzen lässt. Trotzdem bieten viele Programmiersprachen, darunter so populäre wie Java und C#, diese Möglichkeit nicht an.

Der Grund liegt darin, dass bei der Mehrfachvererbung einige konzeptionelle Fragen auftauchen, die zusätzliche Komplexität in Ihre Programme einbringen.

Aber was bleibt dann? Wie können Sie die Aufgabenstellung, die wir anhand des Entwurfsmusters »Beobachter« illustriert haben, in solchen Programmiersprachen lösen? Es gibt mehrere Möglichkeiten einer Lösung.

Im folgenden Abschnitt werden wir den Mechanismus der Delegation als Alternative zur Mehrfachvererbung vorstellen. In Abschnitt 5.4.3 werden wir daran anschließend die sogenannten Mixin-Module vorstellen, mit deren Hilfe die Problemstellung ebenfalls gelöst werden kann. Im Anschluss an diese beiden Abschnitte geben wir dann ab 5.4.4 einen Überblick darüber, warum Mehrfachvererbung eigentlich konzeptionell schwierig ist und wie die verschiedenen Programmiersprachen damit umgehen.


Rheinwerk Computing - Zum Seitenanfang

5.4.2 Delegation statt Mehrfachvererbung  Zur nächsten ÜberschriftZur vorigen Überschrift

Java und C# machen es sich einfach.

Der Verzicht auf die Mehrfachvererbung der Implementierung, wie von Java und C# praktiziert, hat den Charme der Einfachheit. Allerdings bringt dieser Verzicht auch einen relevanten Nachteil mit sich. Die Vererbung ist ein wichtiges Mittel der Vermeidung der Wiederholung – wenn mehrere oder gar alle Unterklassen eine Operation auf die gleiche Art umsetzen, sollte die umsetzende Methode nur an einer Stelle definiert sein. Die Vererbung ermöglicht es, diese Methode in der Oberklasse zu implementieren, so dass sie von allen Unterklassen geerbt werden kann.

In Java und C# können Sie diesen Mechanismus nur entlang eines Stranges der Vererbungshierarchie nutzen. Was sollen Sie aber tun, wenn mehrere Klassen eine explizite Schnittstelle auf die gleiche Art implementieren sollen, diese in der Klassenhierarchie aber keine gemeinsame implementierende Basisklasse haben? Sie haben keine Oberklasse, von der sie die Implementierung erben können.

Redundanz im Quellcode

Eine offensichtliche und offensichtlich unschöne Möglichkeit ist es, Redundanzen in Quelltexte einzuführen. Jede der Klassen implementiert ihre eigene Methode zu jeder der deklarierten Operationen. Dies ist schnell mit Kopieren und Einfügen erledigt, man produziert mehr Quelltext und stellt sicher, dass es in dem Projekt auch in der Zukunft für Softwareentwicklung und Qualitätssicherung genug zu tun geben wird.

Eine bessere Alternative ist es, die benötigte zusätzliche Funktionalität dadurch sicherzustellen, dass der Aufruf von Operationen an ein anderes Objekt delegiert wird.


Icon Hinweis Delegation

Beim Verfahren der Delegation setzt ein Objekt eine Operation so um, dass der Aufruf der Operation an ein anderes Objekt delegiert wird. Dadurch kann die Verantwortung, eine Implementierung für eine bestimmte Schnittstelle bereitzustellen, an ein Exemplar einer anderen Klasse delegiert werden. Auf diese Weise kann auch ohne Mehrfachvererbung die Funktionalität von mehreren Klassen genutzt werden.


In Abbildung 5.57 ist eine Lösung für unser Problem des beobachtbaren HTML-Dokuments vorgestellt, die auf Delegation basiert. Bei diesem Design deklarieren wir die Klasse Beobachtbares als eine explizite Schnittstelle und lassen die Klasse BeobachtbaresHtmlDokument diese Schnittstelle implementieren, welche zudem die Funktionalität von HTMLDokument erbt.

Die wirkliche Umsetzung der Methoden der Klasse Beobachtbares, die ja nicht nur für die HTML-Dokumente gebraucht werden kann, stellt die Klasse BeobachtbaresImplementierung bereit. Jedes Exemplar der Klasse BeobachtbaresHtmlDokument besitzt ein Exemplar der Klasse BeobachtbaresImplementierung, auf das es alle Aufrufe der in der Schnittstelle Beobachtbares deklarierten Methoden weiterleitet – delegiert (siehe Markierung ). Dieses Hilfsobjekt übernimmt also die komplette Verwaltung und Benachrichtigung der Beobachter.

Abbildung 5.57    Beobachtbares HTML-Dokument unter Verwendung eines Delegaten

Die Vorgehensweise, die Implementierung bestimmter Methoden einem Delegaten zu überlassen, löst das Problem der fehlenden Vererbungsmöglichkeit und hat auch noch andere Vorteile. Eine Implementierung einer Methode einer Klasse ist in Java und C# nicht während der Laufzeit der Anwendung änderbar, alle direkten Exemplare einer implementierenden Klasse haben für eine Operation dieselbe Methode. Diese Einschränkung gilt nicht für die Delegation.

Austausch von Delegaten

Verschiedene Objekte, auch wenn sie direkt zu einer und derselben Klasse gehören, können die Aufrufe an unterschiedliche Hilfsobjekte delegieren und diese sogar zur Laufzeit ändern. Diese Tatsache macht man sich auch beim Entwurfsmuster »Strategie« zunutze, dem wir uns in Abschnitt 5.5.2 widmen werden.

Ein Nachteil der Delegation besteht darin, dass die Klassen, die sie verwenden, um bestimmte Schnittstellen zu implementieren, immer noch eine eigene Rumpfimplementierung der Schnittstelle besitzen müssen. Auch wenn jede der Methoden nur aus einem einzigen Aufruf der entsprechenden Methode des Delegaten besteht, sie muss vorhanden sein. Einige Programmiersprachen bieten hier allerdings weitergehende Unterstützung. In Ruby ist es zum Beispiel möglich, alle Aufrufe von Operationen an ein Hilfsobjekt weiterzuleiten, wenn diese von einem Objekt nicht selbst umgesetzt werden.

Sofern die von Ihnen verwendete Programmiersprache den Mechanismus von Mixins unterstützt, bieten diese eine Alternative, die in vielen Fällen mit weniger Quelltext umzusetzen ist.


Rheinwerk Computing - Zum Seitenanfang

5.4.3 Mixin-Module statt Mehrfachvererbung  Zur nächsten ÜberschriftZur vorigen Überschrift

Mixins

Ein Mixin ist ein Modul, das Definitionen von Datenelementen und Methodenimplementierungen enthält, die einer implementierenden Klasse hinzugefügt werden können. Die Mixins kann man als das Gegenstück zu den reinen Schnittstellen-Klassen (in Java z. B. die Interfaces) betrachten. Während die reinen Schnittstellen nur einen Typ der Exemplare spezifizieren und keine Implementierung definieren, enthalten die Mixins Implementierungen von Methoden, sie selbst spezifizieren jedoch keinen Typ. Durch das »Zumischen« eines Mixins in die Deklaration einer implementierenden Klasse enthält diese Klasse alle Methoden des Mixins.


Icon Hinweis Mixin

Mixins werden Module genannt, die existierende Klassen um Datenelemente und Methoden erweitern, ohne dass eine Subklasse dieser existierenden Klasse erstellt werden muss. Beispiele für Programmiersprachen, die eine solche Erweiterung zulassen, sind Ruby, Python oder CLOS (Common Lisp Object System). Allerdings werden Mixins von den verbreiteten objektorientierten Sprachen wie Java und C# zumindest in den aktuellen Versionen nicht direkt unterstützt. Wenn ein Mixin-Modul eine existierende Klasse erweitert, werden wir das in Ermangelung eines besseren deutschen Wortes als das Reinmischen des Moduls bezeichnen.


Mixins statt Vererbung

Bei dem Problem der fehlenden Vererbungsmöglichkeit können Ihnen die Mixins helfen. Statt der Delegation an eine Hilfsklasse können Sie unter Verwendung von Mixins ein Modul implementieren, das die Methoden für die Operationen der Schnittstelle implementiert.

Wenn Sie dieses Modul als ein Mixin zu den implementierenden Klassen hinzufügen, ist Ihre Delegationsabsicht klar und explizit formuliert. Und wenn sich die Schnittstelle ändern sollte, müssen Sie auch nur das Mixin-Modul anpassen.

In der Sprache Ruby gehört das Konzept der Mixins zu einem der Schlüsselfeatures der Programmiersprache. Wir stellen deshalb die Verwendung von Mixins anhand von Ruby vor. In Ruby kann ein Modul in eine Klasse mit dem Statement include eingefügt werden. Somit werden alle Routinen, die in diesem Modul definiert werden, zu Methoden der Klasse, in die wir das Modul reinmischen. Die reingemischten Methoden enthalten dabei den Zugriff auf alle anderen Methoden der Klasse, seien es Methoden, die in der Klasse direkt definiert worden sind, von der Oberklasse geerbt oder von anderen Modulen reingemischt wurden.

Mehrere Mixins mit gleichen Methoden

In Ruby erhält eine Klasse dabei pro Methodennamen immer nur eine Methode. Die zuletzt »reingemischte« Methode ersetzt die vorher reingemischte gleichnamige Methode. In Abbildung 5.58 ist dargestellt, wie Ruby auch in Mixin-Modulen nach Methoden sucht, wenn eine Operation auf einem Objekt aufgerufen wird.

Abbildung 5.58    Suchreihenfolge nach Mixin-Modulen in Ruby

In Ruby ist es kein Fehler, in einer Klasse mehrere Methoden mit dem gleichen Namen zu deklarieren. Die zuletzt deklarierte Methode ersetzt immer die zuvor deklarierte Methode des gleichen Namens. Mixins können aber existierende Methoden von Klassen nicht überschreiben, sondern lediglich neue Methoden hinzufügen. Klassen werden deshalb durch Mixin-Module erweitert. Dies unterscheidet den Mechanismus klar von der Vererbung.

Wenn in Ruby eine Klasse oder ein Modul erweitert wird, wird automatisch die Funktionalität aller ihrer Exemplare erweitert! Dies funktioniert auch mit Mixins. Wenn Sie also nachträglich einem Mixin-Modul eine neue Methode hinzufügen, erhalten alle Klassen, in die Sie dieses Mixin importieren, diese Methode ebenfalls. Somit kann die Methode sofort von allen ihren Exemplaren verwendet werden.

Aspektorientierte Fähigkeiten von Ruby

Verzeihen Sie den Enthusiasmus und die etwas angestrengt jugendliche Sprache an dieser Stelle, aber dieses Feature ist richtig cool. Nicht nur weil sich dynamisch selbst modifizierender Code nützlich sein kann, sondern weil die Fähigkeit, bereits vorhandene Klassen zu erweitern oder sie zu ändern, eine wichtige Fähigkeit der aspektorientierten Programmierung ist. Weitere Anwendungen dafür werden wir deshalb auch in Kapitel 9, »Aspekte und Objektorientierung«, vorstellen.

Mixins in C++

Ein den Mixins ähnlicher Mechanismus kann auch in der Sprache C++ umgesetzt werden.

Den Mixins kommt nämlich in C++ die private Ableitung von einer Oberklasse sehr nahe. Durch die private Vererbung erhält die Klasse alle öffentlichen und geschützten Methoden und Datenelemente der als Mixin agierenden Oberklasse, ohne jedoch »von außen betrachtet« ihre Unterklasse zu werden. Den Mechanismus der privaten Vererbung in C++ haben Sie bereits in Abschnitt 5.1.6, »Sichtbarkeit im Rahmen der Vererbung«, kennen gelernt.

Wenden wir uns nun im folgenden Abschnitt noch einem anderen Aspekt der Mehrfachvererbung zu.


Rheinwerk Computing - Zum Seitenanfang

5.4.4 Die Problemstellungen der Mehrfachvererbung  topZur vorigen Überschrift

Eine Programmiersprache, die Mehrfachvererbung unterstützt, muss drei Fragen dazu beantworten können. Es können sich verschiedene Situationen ergeben, in denen der Umgang mit Datenstrukturen, Operationen und Methoden nicht eindeutig geklärt ist. Eine Programmiersprache, die Mehrfachvererbung unterstützt, muss für diese Fragen eine Strategie vorliegen haben.

In Abbildung 5.59 sind die folgenden drei Fragen an einem Beispiel illustriert.

  • Frage 1: Wie werden Operationen mit gleicher Signatur behandelt, die in verschiedenen Oberklassen deklariert werden (siehe Markierung )?
  • Frage 2: Wenn mehrere Oberklassen dieselbe Operation mit unterschiedlichen Methoden umsetzen, welche Methode wird beim Aufruf der Operation an einem Exemplar der gemeinsamen Unterklasse aufgerufen (siehe Markierung )?
  • Frage 3: Wie werden Datenstrukturen kombiniert? An Markierung werden die geerbeten Operationen und Methoden dargestellt. Wenn mehrere Oberklassen die Datenstruktur der Exemplare beschreiben, wie sieht die kombinierte Datenstruktur der Exemplare der gemeinsamen Unterklasse aus (siehe Markierung )?

Abbildung 5.59    Problemstellungen bei Mehrfachvererbung

In Tabelle 1 ist in der Übersicht dargestellt, wie die als Beispiel gewählten Sprachen Java, C#, Python und C++ bei diesen Fragen vorgehen. Java und C# umgehen die letzten beiden Fragen dadurch, dass sie nur die Mehrfachvererbung der Spezifikation erlauben. Damit müssen die beiden Sprachen lediglich eine Antwort auf die erste Frage liefern.


Tabelle 5.1    Übersicht über die Behandlung der Problemstellungen durch Sprachen

Java C# Python C++

Frage 1

Verschmelzung von Operationen

Mehrere Operationen

Eine Operation

Verschmelzung von Operationen

Frage 2

Nicht relevant

Nicht relevant

Diamantenregel

Expliziter Klassenname

Frage 3

Nicht relevant

Nicht relevant

Nicht relevant

Wahlweise


In den folgenden Abschnitten schauen wir uns das Vorgehen der Sprachen bei der Beantwortung der Fragen etwas genauer an. Anhand von Beispielen werden wir dabei illustrieren, welche Probleme bei der Mehrfachvererbung entstehen können und welche Mittel uns die verschiedenen Sprachen bieten, diese zu lösen.

Java und C# unterstützen nur Mehrfachvererbung der Spezifikation

Die Mehrfachvererbung der Spezifikation ist sowohl konzeptionell als auch in der Umsetzung einfacher als die Mehrfachvererbung der Implementierung. Aus diesem Grund verzichten die Programmiersprachen Java und C# zum Beispiel komplett auf die Mehrfachvererbung der Implementierung.

Da Java und C# statisch typisiert sind, muss jede Klasse, auch wenn sie selbst keine Implementierung bereitstellt, deklariert werden. Aus diesem Grund unterscheidet man in Java und C# zwischen zwei Arten von Klassen:

  • Klassen, die eine Implementierung ihrer Spezifikation enthalten können, die sowohl in Java als auch in C# mit dem Schlüsselwort class deklariert und daher einfach nur »Klasse« genannt werden.
  • Klassen, die nur die Spezifikation ihrer Schnittstelle enthalten dürfen. Sie werden einfach nur »Schnittstellen« genannt und mit dem Schlüsselwort interface deklariert.

Implizite und explizite Schnittstellen

Auch wenn man in der Umgangssprache in Java und C# vereinfacht zwischen Klassen und Schnittstellen unterscheidet, sollten Sie sich immer der Tatsache bewusst sein, dass auch die explizit als solche deklarierten Schnittstellen aus der Sicht der Objektorientierung Klassen sind und die Klassen auch eine implizite Schnittstelle deklarieren, die aus allen ihren öffentlichen Operationen besteht.

In Java und C# ist die Mehrfachvererbung der expliziten Schnittstellen erlaubt, die Mehrfachvererbung der implementierenden Klassen jedoch nicht. Eine explizite Schnittstelle kann von mehreren anderen expliziten Schnittstellen erben, und eine implementierende Klasse kann mehrere Schnittstellen implementieren. Eine implementierende Klasse kann jedoch nur von einer implementierenden Klasse erben.

Operationen mit gleicher Signatur in Java

Mehrere Oberklassen deklarieren gleiche Operation.

Wenn mehrere Schnittstellen in Java eine Operation mit der gleichen Signatur deklarieren, gelten alle diese Deklarationen in der gemeinsamen Ableitung als eine Deklaration derselben Operation. In Java kann also eine Klasse nicht mehrere Methoden mit derselben Signatur implementieren. [Zumindest nicht in der Programmiersprache Java. In dem, vom Compiler generierten, Bytecode kann in der Tat eine Klasse mehrere Methoden mit derselben Signatur besitzen. Dies ist allerdings eher ein Implementierungsdetail der Generics in der Version 5 von Java als ein Feature der Programmiersprache. ]

Da der Rückgabetyp einer Operation nicht zu deren Signatur gehört, kann in Java eine Klasse nicht ohne weiteres zwei Schnittstellen implementieren, die eine Operation mit derselben Signatur aber unterschiedlichen Rückgabetypen deklarieren.

In den alten Java-Versionen bis 1.4 ist es immer ein Fehler, wenn eine Klasse zwei Schnittstellen mit einer Operation mit derselben Signatur, aber unterschiedlichen Rückgabetypen zu implementieren versucht. Denn bis zur Version 1.4 kann eine Methode den Rückgabewert der Methode, die sie überschreibt, beziehungsweise der Operation, die sie implementiert, nicht ändern.

Abbildung 5.60 zeigt ein Beispiel, in dem die Datenstrukturen nur von einer Klasse geerbt werden (siehe Markierung ). Die Implementierung von operationA wird nur von BasisklasseA geerbt (siehe Markierung ). Es existiert zudem nur eine operationY , ihr Rückgabetyp ist mit beiden geerbten Operationen kompatibel (kovariante Rückgabetypen werden ab Java 5 unterstützt). Die Klasse AbgeleiteteKlasse hat drei verschiedene inkompatible Rückgabewertspezifikationen für operationZ geerbt. Dies ist in Java nicht erlaubt.

Ab der Version 5 kann eine Methode einen Rückgabetyp deklarieren, der mit dem ursprünglichen Rückgabetyp kovariant ist.


Icon Hinweis Kovariante Typen

Der Typ T2 ist dem Typ T1 kovariant, wenn alle Exemplare von T2 gleichzeitig Exemplare von T1 sind. Einfacher gesagt, T2 muss entweder T1 oder sein Untertyp sein.


Prinzip der Ersetzbarkeit

Obwohl in unserem Beispiel die AbgeleiteteKlasse einen anderen Rückgabetyp der operationY deklariert als den, der in der SchnittstelleB beziehungsweise in der SchnittstelleC deklariert ist, handelt es sich nicht um eine Verletzung des Prinzips der Ersetzbarkeit. Denn die Deklaration der SchnittstelleB besagt, dass die operationY angewendet auf jedes Exemplar der SchnittstelleB ein Exemplar der Klasse/Schnittstelle SchnittstelleB oder null zurückgibt. Die Implementierung in der Klasse AbgeleiteteKlasse sorgt dafür, dass operationY angewendet auf jedes ihrer Exemplare ein Exemplar der Klasse AbgeleiteteKlasse zurückgibt. Da die AbgeleiteteKlasse aber eine Unterklasse der SchnittstelleB ist, ist das Prinzip der Ersetzbarkeit nicht verletzt.

Abbildung 5.60    Mehrfachvererbung in Java

Ab Java 5: kovariante Rückgabetypen

Da in unserem Beispiel die AbgeleiteteKlasse sowohl die SchnittstelleB als auch die SchnittstelleC implementiert, ist der Typ AbgeleiteteKlasse sowohl mit dem Typ SchnittstelleB als auch mit dem Typ SchnittstelleC kovariant.

Operationen mit gleicher Signatur in C#

Wenn in C# mehrere geerbte Schnittstellen eine Operation mit der gleichen Signatur deklarieren, enthält in C# die erbende Schnittstelle mehrere Operationen mit gleichem Namen und gleicher Signatur. Dies ist ein Unterschied zu Java.

Gleiche Signatur

Unabhängig davon ob diese Operationen gleiche oder unterschiedliche, kovariante oder nicht kovariante Rückgabewerttypen haben, es sind unterschiedliche Operationen.

Beim Aufruf meldet C# einen Mehrdeutigkeitsfehler, wenn die Signatur des Aufrufes die aufzurufende Methode nicht eindeutig bestimmt. Man kann diese Mehrdeutigkeit des Aufrufes auch durch explizite Typumwandlung des Objekts auf eine der Oberschnittstellen beseitigen.

Wenn eine Schnittstelle in C# eine Operation deklariert, welche die gleiche Signatur hat wie eine geerbte Operation, wird eine neue Operation deklariert, welche die geerbte Operation verdeckt. Eine solche verdeckende Deklaration sollte mit dem Schlüsselwort new gekennzeichnet werden.

Icon Beispiel Schnittstellen in C#

In Abbildung 5.61 wird die Implementierung von operationA() nur von BasisklasseA geerbt. Für die operationA aus der SchnittstelleB kann, muss aber nicht eine eigene Methode bereitgestellt werden (siehe Markierung ). Jeder der geerbten Operationen mit unterschiedlichen Rückgabetypen an Markierung muss in einer konkreten Klasse eine eigene Methode zugewiesen werden. Welche Methode aufgerufen wird, hängt vom Typ der Variablen ab, mit der auf das Objekt zugegriffen wird.

Abbildung 5.61    Mehrfachvererbung von Operationen in C#

Die Datenstrukturen (siehe Markierung ) in diesem Beispiel werden hingegen nur von einer Klasse geerbt.

new und override

Genauso wie die Schnittstellen mehrere Operationen mit derselben Signatur haben können, können Klassen mehrere Methoden mit derselben Signatur haben. Wie bereits oben beschrieben, kann eine Klasse in C# mit dem Schlüsselwort new durch eine neue Methode mit gleicher Signatur eine geerbte Methode verdecken, oder sie kann sie mit dem Schlüsselwort override überschreiben.

Wie sieht es aber mit der Implementierung der Schnittstellen aus, wenn diese mehrere Operationen mit der gleichen Signatur enthalten? Hier bietet C# zwei Möglichkeiten an:

Angabe der Schnittstelle

Gibt man vor dem Namen der Methode den Namen der Schnittstelle an, in der sie deklariert wurde, implementiert die Methode nur diese Operation. Zusätzlich können wir aber auch eine allgemeine Methode ohne Angabe der konkreten Schnittstelle umsetzen. [Diese Methode muss allerdings mit Sichtbarkeitsstufe public deklariert werden. ] Diese wird dann in allen Fällen aufgerufen, in denen es keine anwendbare schnittstellenspezifische Methode gibt.

Mehrfachvererbung von Operationen und Methoden in C++

Mehrere Klassen implementieren dieselbe Operation.

C++ verhält sich sehr pragmatisch bei der Frage, was bei Mehrdeutigkeiten in Bezug auf den Aufruf von Methoden zu tun ist. Entweder ist der Aufruf einer Methode eindeutig, oder es handelt sich um einen Uneindeutigkeitsfehler. Wenn eine Klasse von mehreren Oberklassen Methoden mit derselben Signatur erbt, muss man beim Aufruf einer der Methoden den gemeinten Typ des aufgerufenen Objekts explizit bestimmen.

Die aufzurufende Methode lässt sich also in den betrachteten Fällen immer bestimmen.

In der Abbildung 5.62 sind an Markierung nur neu hinzugefügte Datenelemente dargestellt. An Markierung sehen Sie, dass nur die überschriebenen und neuen Methoden dargestellt werden. In dem abgebildeten Beispiel enthält ein Exemplar der Klasse abgeleiteteKlasse alle geerbten Datenelemente, zwei davon heißen datumC (siehe Markierung und ). Bei einem Zugriff auf datumC muss eindeutig klar sein, welches Element gemeint ist, sonst ist es ein Fehler.

Werden zwei Operationen mit gleicher Signatur von zwei verschiedenen Klassen geerbt, so gelten ähnliche Regeln wie in Java. Es muss nur eine Methode für die Implementierung beider Operationen umgesetzt werden.

Rückgabetypen kovariant?

Ob die Rückgabetypen der Methoden gleich oder nur kovariant sein müssen, hängt hier von dem verwendeten Compiler ab. Der aktuelle C++-Standard erlaubt zwar kovariante Rückgabetypen, nicht alle Compiler halten sich allerdings an diese Vorgabe. Manche unterstützen sie überhaupt nicht, manche nur teilweise.

Abbildung 5.62    Mehrfachvererbung in C++

Mehrfachvererbung von Operationen und Methoden in Python

In Python wird eine Operation ausschließlich über ihren Namen identifiziert. Wenn eine Klasse von mehreren Klassen mehrere Methoden mit demselben Namen geerbt hat, unterstützt sie trotzdem nur eine Operation mit diesem Namen.

Die spannende Frage ist, welche der geerbten Methoden ausgeführt wird, wenn die Operation auf einem Exemplar der abgeleiteten Klasse aufgerufen wird.

Suchreihenfolge für Methoden

In den älteren Versionen von Python (bis zur Version 2.1) gilt für die Bestimmung der implementierenden Methode die Regel »Tiefensuche, von links nach rechts«. Das bedeutet, dass die Methode in den geerbten Oberklassen von links nach rechts gesucht wird, wobei bei jeder Klasse auch deren Oberklassen untersucht werden.

Im folgenden Bild illustrieren wir die Reihenfolge, in der eine Methode bei einem Aufruf einer Operation gesucht wird:

Abbildung 5.63    Mehrfachvererbung in Python

Prinzip der kleinstmöglichen Überraschung

Python verfolgt löblicherweise das Prinzip der kleinstmöglichen Überraschung. Wäre es also nicht angebracht, statt einer Tiefensuche eher die Breitensuche zu verwenden? Das heißt, zuerst alle direkt geerbten Klassen zu untersuchen, dann erst deren direkte Oberklassen und so weiter? Wäre die Suchreihenfolge AbgleiteteKlasse, BasisklasseB, BasisklasseE nicht weniger überraschend? Immerhin ist die Klasse BasisklasseE in der Vererbungshierarchie näher an der AbgleiteteKlasse als die Klasse BasisklasseA.

Doch wenn wir das Prinzip der Kapselung beachten, muss für uns unwichtig sein, ob die Klasse BasisklasseB eine Methode selbst implementiert oder ob sie die Methode von einer ihrer Oberklassen geerbt hat. Aus diesem Grund ist die von Python gewählte Suchstrategie verständlich.

In Python 2.2 wurde die Suchstrategie jedoch geändert.

Die Zulassung der Mehrfachvererbung führt automatisch dazu, dass eine Klasse in der Vererbungshierarchie mehrfach auftreten kann. Was passiert, wenn eine Klasse zwei Oberklassen hat, die von derselben Klasse eine Methode erben, und eine der Oberklasse diese Methode überschreibt?

Diamantenregel

Wenn eine Klasse in der Vererbungshierarchie mehrfach auftritt, kann man das Klassendiagramm in Form einer Raute (englisch auch Diamond) darstellen. Daher der Name der neuen Suchvorgehensweise in Python 2.2: die Diamantenregel. [Um die Kompatibilität mit älteren Versionen zu gewährleisten, unterscheidet man in Python ab der Version 2.2 zwischen zwei Arten von Klassen. Für die »alten« Klassen werden die geerbten Methoden auch in den neueren Python-Versionen mit der Tiefensuche gefunden. Die Diamantenregel gilt nur für die »neuen« Klassen, die explizit von der Klasse object erben müssen. ]

Nach der Diamantenregel wird die Suchreihenfolge genau wie in den alten Versionen bestimmt. Doch anstatt sofort mit der Suche anzufangen, werden aus der Suchreihenfolge zuerst die Duplikate entfernt. Erhalten bleibt immer nur der letzte Eintrag einer Klasse. Wird also eine geerbte Methode durch eine der Oberklassen überschrieben, wird diese überschriebene Variante gefunden, auch wenn die ursprüngliche Implementierung in einer Basisklasse »weiter links« in der Vererbungshierarchie zu finden wäre.

Abbildung 5.64    Mehrfachvererbung in Python; die Diamantenregel

Sollten Sie mit der Suchmethode von Python nicht zufrieden sein, haben Sie immer die Möglichkeit, die Methode in der abgeleiteten Klasse selbst zu implementieren und den Aufruf auf die gewünschte Implementierung umzuleiten.

Datenstrukturen bei Mehrfachvererbung in Python

Klassen erben von ihren Oberklassen nicht nur die Spezifikation der Operationen und die Implementierung der Methoden, sondern auch die Definition der Datenelemente der Oberklasse. Wie sieht die Datenstruktur einer solchen von mehreren Oberklassen abgeleiteten Klasse in Python aus?

In Python werden die Datenstrukturen der Exemplare einer Klasse nicht explizit durch die Klasse vorgegeben.

Zusammenführen der Datenelemente

Die Daten jedes Objekts werden in Python dem Objekt dynamisch zur Laufzeit zugeordnet, und unterschiedliche Exemplare derselben Klasse können durchaus unterschiedliche Attribute besitzen. Wenn mehrere Oberklassen ein Datenelement mit demselben Namen initialisieren, besitzen die Exemplare der abgeleiteten Klasse nur genau ein Datenelement mit diesem Namen. Das Datenelement enthält den letzten ihm zugewiesenen Wert. Da die Sprachen Python dynamisch typisiert ist und Variablen und Datenelemente keine deklarierten Typen haben, kann man jedem Datenelement jeden beliebigen Wert zuordnen.

Initialisierung von Daten

Wenn also die Klasse A in ihrer Initialisierungsroutine die Datenelemente x und y dem initialisierten Exemplar zuordnet, und die Klasse B die Datenelemente y und z, und wenn die Klasse C von A und B abgeleitet ist, können wir keine Aussage über die Attribute der initialisierten Exemplare von C treffen, ohne uns die Initialisierungsroutine der Klasse C selbst anzuschauen – denn hier gilt die einfache Regel – das Datenelement wird den ihm zuletzt zugewiesenen Wert haben.

Namenskonflikte

Um eventsuelle Namenskonflikte zu vermeiden, kann man in Python den Namen eines Datenexemplars mit einem doppelten Unterstrich anfangen lassen. Dies veranlasst Python, intern dem Namen des Datenattributes noch den Namen der deklarierenden Klasse hinzuzufügen. Damit wird ein Namenskonflikt zwar nicht ganz ausgeschlossen, aber die Gefahr wird erheblich reduziert.

Datenstrukturen bei Mehrfachvererbung in C++

Kombination von Datenstrukturen

Die Klassen in C++ enthalten die Deklaration aller ihrer Datenelemente und bestimmen so die für die Speicherung der Daten benötigten Datenstrukturen. Historisch betrachtet sind die Klassen in C++ eine Weiterentwicklung der Strukturen von C.

Ähnlich wie bei der einfachen Vererbung setzt sich die Datenstruktur der Exemplare der abgeleiteten Klasse aus den Datenstrukturen der Oberklassen und den Einträgen, welche die abgeleitete Klasse selbst deklariert, zusammen. Die Datenstrukturen der verschiedenen Klassen in der Vererbungshierarchie werden im Speicher einfach hintereinander gehängt.

Besitzen also mehrere Oberklassen einen Dateneintrag mit dem gleichen Namen, so werden die Exemplare von deren gemeinsamen Ableitungen mehrere Dateneinträge mit denselben Namen besitzen. Die Datentypen der Exemplare brauchen dabei nicht gleich zu sein.

Doch wenn die unterschiedlichen Dateneinträge denselben Namen haben, wie wird auf diese Dateneinträge zugegriffen? Welcher der Dateneinträge wird verwendet, wenn man den Namen benutzt?

Wenn es bei einem Zugriff nicht eindeutig ist, welcher Dateneintrag gemeint ist, meldet ein C++-Compiler einen Fehler. Als Programmierer haben wir aber die Möglichkeit, die Eindeutigkeit wieder herzustellen, indem explizit die Klasse angegeben wird, aus welcher der Eintrag verwendet werden soll.

Illustrieren wir diesen etwas trockenen Sachverhalt am besten an einem Beispiel. In Abbildung 5.65 wird der Dateneintrag x von zwei Basisklassen geerbt.

Abbildung 5.65    Mehrfachvererbung der Dateneinträge in C++

Icon Beispiel Zuordnung eines Datenelements

Dadurch hat jedes Exemplar der Klasse C drei Dateneinträge mit dem Namen x. Dennoch besteht hier keine Gefahr der Uneindeutigkeit, weil der Eintrag x in der Klasse C die geerbten Einträge verdeckt. Daher kann man in den Methoden der Klasse C einfach den Bezeichner x oder in Methoden anderer Klassen den Ausdruck pC->x verwenden. Die Elemente, die in den Klassen A und B deklariert sind, können über den Typ C gar nicht referenziert werden. Um sie verwenden zu können, müssen wir einen Zeiger vom Typ A* bzw. B* verwenden.

Icon Beispiel Uneindeutigkeit

Wenn die Klasse C aber keinen eigenen Eintrag mit dem Namen x hätte, wie in Abbildung 5.66, so wäre der Aufruf pC->x nicht eindeutig und somit nicht zulässig.

Abbildung 5.66    Mehrfachvererbung der Dateneinträge in C++. Ein Namenskonflikt.

Namenskonflikte werden also vom Compiler entdeckt und müssen vom Programmierer durch eine explizite Typkonvertierung aufgelöst werden.

Es gibt allerdings noch eine weitere Situation, in der die Entscheidung über ein Datenelement Fragen aufwirft: Wenn eine Oberklasse in der Vererbungshierarchie einer Unterklasse mehrfach vorkommt, enthalten die Exemplare der Unterklasse die Datenstruktur der Oberklasse mehrfach oder nur einmal? In Abbildung 5.67 ist diese Situation dargestellt: Die Klasse AbgeleiteteKlasse erbt den Dateneintrag datumX gleich zweimal von der Klasse BasisklasseA.

In diesem Beispiel ist jedes Exemplar der Klasse BasisklasseB gleichzeitig ein Exemplar der Klasse BasisklasseA und hat also die Daten eines Exemplars von der BasisklasseA. Das Gleiche gilt für die BasisklasseC. Da ein Exemplar der Klasse AbgeleiteteKlasse gleichzeitig ein Exemplar der BasisklasseB und der BasisklasseC ist, also deren Daten besitzt, besitzt es zwei Datensätze der Klasse BasisklasseA.

Abbildung 5.67    Diamantenvererbung in C++. Datenstrukturen sind mehrfach vorhanden.

Das Object4 ist ein Exemplar der Klasse AbgeleiteteKlasse. Da die AbgeleiteteKlasse indirekt eine Unterklasse der BasisklasseA ist, ist das Object4 gleichzeitig ein Exemplar der BasisklasseA. Doch welcher der zwei Datensätze datumX ist gemeint, wenn man das Objekt als ein Exemplar der BasisklasseA betrachten möchte?

Diese Frage ist nicht eindeutig zu beantworten, und aus diesem Grund würde der Compiler einen Mehrdeutigkeitsfehler melden, wenn wir das Object4 einer Variablen des Typs BasisklasseA zuweisen wollten.

Eindeutigkeit über static_cast

Um dem Compiler zu helfen, müssen wir bei der Typumwandlung von AbgeleiteteKlasse* zu BasisklasseA* immer den Weg in der Vererbungshierarchie eindeutig spezifizieren und den Typ des Zeigers immer zuerst explizit zu BasisklasseB* oder BasisklasseC* umwandeln. Über den Aufruf von static_cast können wir diese Zuordnung eindeutig herstellen.

Problematisch ist hier einzig die Tatsache, dass hier die Konzepte der Vererbung (der Ist-Beziehung) und der Komposition (der Besteht-aus-Beziehung) durcheinander gebracht sind. Doch was schert sich der Compiler um konzeptionelle Schwierigkeiten von Softwareentwicklern?


Ersetzung der Mehrfachvererbung durch Komposition

Eine alternative und unproblematische Umsetzung des Beispiels aus Abbildung 5.67 können Sie erstellen, indem Sie statt der Vererbung explizit die Komposition verwenden.

struct ZusammengesetzteKlasse { BasisklasseB b; BasisklasseC c; };

In diesem Beispiel ist ein Exemplar der Klasse ZusammengesetzteKlasse kein Exemplar der BasisklasseB oder der BasisklasseC, sondern ein Exemplar der Klasse ZusammengesetzteKlasse. Sie besteht aus einem Exemplar der BasisklasseB und einem Exemplar der BasisklasseC, die jeweils ein Exemplar der BasisklasseA sind.


Virtuelle Vererbung

Manchmal kann es für uns aber wichtig sein, dass jedes Exemplar der abgeleiteten Klasse sich eindeutig als ein Exemplar der in der Klassenhierarchie mehrfach vorkommenden Oberklasse betrachten lässt. Dies bietet in C++ die sogenannte virtuelle Vererbung.


Icon Hinweis Virtuelle Vererbung

Wenn eine BasisklasseB von der BasisklasseA virtuell abgeleitet ist, bedeutet das, dass die Exemplare der BasisklasseB zwar alle Dateneinträge der Oberklasse BasisklasseA besitzen, allerdings nicht exklusiv. Wenn auch die BasisklasseC virtuell von der BasisklasseA abgeleitet ist und die AbgeleiteteKlasse sowohl von der BasisklasseB als auch von der BasisklasseC abgeleitet ist, so enthalten die Datenstrukturen der Exemplare der Klasse AbgeleiteteKlasse nur einen Satz der Einträge der BasisklasseA. Die in der BasisklasseB und in der BasisklasseC implementierten Methoden teilen sich diesen Datensatz.


Abbildung 5.68 zeigt den Effekt von virtueller Vererbung in der Übersicht.

Exemplare der Klasse AbgeleiteteKlasse enthalten nun aufgrund der virtuellen Vererbung das Datenelement datumX nur einmal.

Dies entspricht grob dem Verhalten von Python: Zwar hat jede Klasse der Klassenhierarchie eigene Methoden, sie teilen aber eine gemeinsame Datenstruktur.

In diesem Beispiel enthält ein Exemplar der Klasse AbgeleiteteKlasse nur einen Datensatz der BasisklasseA, obwohl die Klasse BasisklasseA über zwei Vererbungspfade erreichbar ist. Daher kann das Object4 es eindeutig als ein Exemplar der BasisklasseA betrachtet werden, und man kann seinen Dateneintrag datumX direkt verwenden, weil der Name datumX diesen Dateneintrag eindeutig bestimmt.

Abbildung 5.68    Virtuelle Vererbung in C++

Diskussion: Virtuelle Vererbung

Bernhard: Hör mal, Du machst hier so trockene Beispiele mit ABC, kannst du dir nicht etwas Realistischeres ausdenken?

Gregor: O.k., ich versuche es. Stellen wir uns die Klasse der Transportmittel vor. Jedes Transportmittel wird eine Bezeichnung haben. Bei Schiffen wird es der Name des Schiffes sein, bei Autos das Kennzeichen, bei Fahrrädern die Seriennummer des Rahmens.Jetzt unterteilen wir die Klasse Transportmittel anhand des Kriteriums »Motorisierung« in zwei Unterklassen: Motorisierte Transportmittel (mit dem Dateneintrag »Leistung«) und unmotorisierte Transportmittel. Anhand des Kriteriums »Bereifung« unterteilen wir die Transportmittel in bereifte Transportmittel (mit dem Dateneintrag »Reifendruck«) und reifenlose Transportmittel. Schließlich leiten wir von den Oberklassen motorisierte Transportmittel und bereifte Transportmittel die Klasse Auto ab. Ich habe dir das Szenario übrigens in Abbildung 5.69 aufgezeichnet.Ein Auto hat also einen Dateneintrag über die Leistung des Motors, definiert in der Klasse motorisierte Transportmittel. Es hat auch einen Dateneintrag für den Reifendruck, definiert in der Klasse bereifte Transportmittel. Außerdem hat ein Auto, da es ein Transportmittel ist, auch einen Namen. Einen oder zwei? Da wir hier nur einen Namen brauchen, würden wir in diesem Falle in C++ die virtuelle Vererbung verwenden.

Abbildung 5.69    Exotisches Beispiel für virtuelle Vererbung

Bernhard: Hör mal, dieses Beispiel ist zwar nicht so trocken, aber es ist schon ziemlich … Wie soll ich es sagen, ohne Dich zu beleidigen …?

Gregor: Ja gut, du hast schon Recht. Das Beispiel ist nicht besonders realistisch. Ich muss zugeben, dass ich noch nie in der Praxis die virtuelle Vererbung in C++ gebraucht habe, weil die Klassenhierarchien nie tief und komplex genug waren und ich in vielen Fällen die Komposition gegenüber der Vererbung bevorzuge.Wenn wir überhaupt überlegen müssen, ob eine Klasse virtuell oder nicht virtuell erben soll, sollten wir stattdessen zunächst überlegen, ob die Klassenhierarchie nicht zu kompliziert ist.

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