 Ja, herzlich willkommen zurück zur Veranstaltung Programmierparadigmen. Wir sind hier im Modul Datenabstraktion und Objektorientierung und das ist der zweite von fünf Teilen, in dem es um Vererbung gehen soll. Also wir sind jetzt hier in der, innerhalb dieses Moduls und werden uns ein bisschen genauer anschauen, wie denn nun eigentlich diese Datentypen oder Klassen, die wir gerade in dem ersten Teil gesehen haben, miteinander verbunden werden können, indem eine Klasse von einer anderen Klasse erbt. Zu Beginn versuche ich erstmal ein bisschen zu definieren, was Vererbung in dem Zusammenhang denn überhaupt bedeutet. Also die Idee von Vererbung ist Code Reuse, also die Verwendung von einer existierenden Implementierung, indem man eine neue Abstraktion, also zum Beispiel die Unterklasse, definiert als eine Erweiterung oder Verfeinerung einer existierenden Datenabstraktion, also zum Beispiel dann der Oberklasse. Also das Ziel von Vererbung ist eigentlich immer die Wiederverwendung von existierendem Code, sodass man den Code eben nicht kopieren muss oder in irgendeiner Form wiederverwendet, sondern diese Kapselung, die wir mit Klassen ja haben, einfach weiter nutzt und diese Abstraktion dann erweitert oder verfeinert. Das funktioniert dann so, dass die Unterklasse, die Members, also die Felder oder Methoden der Oberklasse erbt, was in der Regel bedeutet, dass die Unterklasse all diese Felder auch benutzen kann, gibt es ein paar Ausnahmen, wer mal sehen, und zusätzlich aber auch noch Dinge hinzufügen kann, also zum Beispiel weitere Felder oder Methoden hinzufügt oder auch die existierenden Felder oder Methoden in bestimmten Grenzen zumindest verändert und entsprechend anpasst. Wenn man über Vererbung spricht, gibt es oft ein bisschen Verwirrung bei zwei Konzepten, die im Prinzip nicht dasselbe sind, aber dann in vielen Sprachen in der Praxis doch ähnlich oder gleich behandelt werden, nämlich die beiden Konzepte subclasses und subtypes, also einmal die Unterklassen oder dann die Untertypen. Also die Frage ist, sind denn alle Unterklassen auch automatisch untertypen ihrer Oberklasse und die Antwort ist im Prinzip nein, denn das sind eigentlich zwei verschiedene Paar Schuhe und zwei verschiedene Paar Ideen. Also subclassing, das worum es hier hauptsächlich gehen soll, hat als Grundziel, dass wir den Code, der in der einer existierenden Klasse geschrieben ist, wiederverwenden wollen ohne den zu kopieren. Subtyping hat auch als Ziel, Code wiederzuverwenden, allerdings nicht innerhalb der Klasse, sondern auf der Seite der Clients der Klasse, also das Code ist das, der die Klasse benutzt, denn wenn Clientcode für eine Oberklasse geschrieben wurde, dann sollte dieser kleinen Code eben auch für all die Unterklassen funktionieren, das heißt, ich muss den kleinen Code nicht kopieren, wenn ich eine Unterklasse erzeuge und dann für die Unterklassen anpassen, sondern ich kann den einfach wiederverwenden und der funktioniert ganz automatisch. Also beides subclassing und subtyping hat was mit Code reuse zu tun, aber eben einmal innerhalb der Klasse und der Implementierung dieser Oberklasse und der Unterklasse, im Falle von subclassing und auf der anderen Seite auf der kleinen Seite und dafür ist subtyping da. In der Praxis sieht es so aus, dass die meisten Programmiersprachen diese beiden Konzepte einfach in einen Konzept zusammenführen und deswegen eben auch immer ein bisschen diese Verwirrung entsteht, was jetzt eigentlich genau der Unterschied ist. Also in den meisten Klassen, in den meisten Sprachen ist eine Unterklasse dann automatisch auch ein Untertyp der Oberklasse, weil eben diese beiden Konzepte da miteinander vereint werden. Wenn diese beiden Konzepte miteinander vereint werden, dann will man eine Eigenschaft gern haben und die wurde von Barbara Liskov vor einigen Jahren aufgeschrieben und heißt deswegen das Liskovsche Substitutability-Prinzip und was das Ganze bedeutet ist, vereinfacht gesagt, dass jeder Subtyp sich so verhalten soll wie der Supertyp, wenn ein Instanz dieses Typs durch das Interface des Supertyps verwendet wird. Das heißt also, wenn ich zwei Klassen in der Sprache habe, diese beiden Konzepte miteinander vermischt und die Unterklasse jetzt vielleicht das Verhalten der Oberklasse in irgendeiner Form verfeinert oder ändert, dann sollte das für den Client transparent sein, denn wenn ein Client ein Objekt dieser Klasse durch das Interface der Oberklasse benutzt, dann sollte er nicht sehen, ob das jetzt eine bestimmte Instanz von einer bestimmten Unterklasse oder vielleicht eine Instanz der Oberklasse ist, denn beide sollten sich eigentlich gleich verhalten, denn der Client weiß ja nicht, was für ein Objekt er da eigentlich vor sich hat und soll das eigentlich auch gar nicht wissen müssen. Also konkretes Beispiel, wenn ich eine Klasse B oder ein Typ B hat, der ein Subtyp von Typ A ist, dann heißt das ja, dass jedes Objekt vom Typ A durch irgendein Objekt von Typ B ersetzt werden kann. Und der Client sieht das nicht, denn der Client weiß nur, da ist etwas, was sich benutzen kann wie ein A und geht natürlich dann auch davon aus, dass das Verhalten dementsprechend ist. Der Vorteil, wenn Programme diesem Liskoption Substitutability Principle folgen, ist, dass man eben als Client nicht wissen muss, ob ich jetzt ein A oder ein B Formi habe, so lange ich das Ganze durch das Interface von A benutzen kann, dann sollte das Ganze eben auch mit Objekten vom Typ B funktionieren. Um das Ganze ein bisschen zu illustrieren, schauen wir uns mal ein Beispiel an, und zwar in Java, wo wir zwei Klassen haben, die eben genau dieses Liskoption Substitutability Principle nicht beachten und wie wir sehen werden, ergibt sich daraus dann Verhalten, was man so vielleicht nicht unbedingt erwartet. Also wir haben hier oben zwei Klassen A und dann eine Klasse B, die diese Klasse A erweitert. Die sind hier alles Static, aber das hat im Prinzip nur damit zu tun, dass ich das Ganze hier alles in eine Klasse namens Liskof packen will, aber das hat mit dem, was ich zeigen will, eigentlich nichts zu tun. Was diese Klasse A anbietet, ist eine Methode namens Add1, die ein Integer nimmt und dann dieses gegebenen Integer-Wert plus 1 zurückgibt. Die Klasse B erweitert A und überschreibt auch diese Methode Add1. Was sie hier allerdings macht, ist zusätzlich zu dem, was vorher gemacht wurde, noch zu überprüfen, ob der Werte übergeben ist, denn auch null oder positiv ist. Und falls der Werte negativ ist, gibt diese Unterklasse B eine illegal Argument-Exception zurück, bzw. wirft diese Exception und nur wenn diese Exception nicht geworfen wird, ruft B dann das Verhalten von A auf, indem mithilfe dieses super Keywords die Methode von A aufgerufen wird. Das Ganze verletzt dieses Liskof-Substitutability-Prinzipel, was wir gerade gesehen haben, und zwar aus dem Grund, dass das Verhalten einer B-Instanz, jetzt eben nicht mehr dem Verhalten einer A-Instanz entspricht. Und ein Beispiel, wo man sieht, dass es dann vielleicht zu Überraschungen führen kann, ist hier unten, was wir hier machen ist, wir rufen eine Methode GetA auf, die uns ein Objekt vom Typ A zurückgibt und rufen auf diesem Objekt dann Add1 auf, was, ja, und übergeben minus 3, also einen negativen Wert. Und wenn GetA uns jetzt eine Instanz von A zurückgeben würde, dann würde das Ganze funktionieren und wir müssten hier minus 2, also minus 3 plus 1, ausgegeben bekommen. Was jetzt aber passiert hier ist, dass GetA, wie wir hier oben sehen, eben keine Instanz von A zurückgibt, sondern eine Instanz von B und in dem Fall wird der Kot jetzt was anderes machen, was wir vielleicht einfach mal kurz anschauen und sich kompilieren und dann füllen wir es aus, denn was hier jetzt passiert ist, dass wir eben diese illegal Argument-Exception bekommen, die aber eben nur deshalb bekommt, weil die Klasse B des Verhalten der Klasse A so anpasst, dass diese beiden Verhalten eben nicht mehr miteinander kompatibel sind. Also das ist eine Verletzung dieses Liskowsen Substitutability-Prinzips, was dann in dem Fall zu überraschenden Verhalten führt. Bloß um das zu erklären, also B darf durchaus das Verhalten von A erweitern und auch verändern, aber eben nur auf eine Art und Weise, die kompatibel ist mit dem Verhalten der Oberklasse. Also wenn jetzt zum Beispiel B zusätzlich zu dem, was A hier macht, sagen wir mal noch, die Berechnung irgendwo hinschreiben würde in der Log-Datei oder so, das wäre vollkommen in Ordnung, weil das sieht aus sich des Kleins dann zumindest gleich aus und der Kleint kann eine Instanz von A genauso behandeln wie eine Instanz von B ohne zu wissen, was der Unterschied ist. Wenn das Verhalten aber so verändert wird wie hier, also dass es eben nicht mehr kompatibel ist mit dem von A, dann verletzt das das Substitutability-Prinzip. Da das Überschreiben von geerbten Feldern und Methoden manchmal zu Problemen führen kann, wie wir gerade in dem Beispiel gesehen haben, gibt es Sprachen, die das Ganze auch einschränken und gar nicht allen Unterklassen erlauben, alle Members zu verändern. In Java ist es so, wie wir gerade gesehen haben, dass die Unterklasse jede Methode oder jedes Feld überschreiben kann. Das heißt, der Programmierer der Unterklasse hat der volle Freiheit, muss aber dann eben zum Beispiel aufpassen, dass bestimmte Eigenschaften wie das Substitutability-Prinzip beachtet werden. In C++ und auch manchen anderen Sprachen ist es ein bisschen anders, denn hier kann die Oberklasse explizit bestimmen, ob die Unterklasse Members überschreiben kann, und zwar in der Form, dass Methoden als Virtual deklariert werden müssen und nur dann ist die Unterklasse überhaupt in der Lage, diese Methode zu überschreiben. Das heißt, der Programmierer der Oberklasse hat eine gewisse Einfluss und kann quasi sagen, dass manche Methoden dieser Oberklasse einfach nicht verändert und nicht überschreiben werden dürfen und macht explizit wann das der Fall ist, indem die Methoden, die überschrieben werden dürfen, als Virtual deklariert werden. Schauen wir uns mal ein konkretes Beispiel für dieses Virtual Keywork beziehungsweise das fehlende von an diesem Beispiel, was wir hier sehen an. Also das ist eine C++-Code, wo wir zwei Klassen haben, A und B. A ist die Oberklasse und B erweitert diese Oberklasse mit Hilfe dieser Doppelpunkt-Syntax. Die Oberklasse A bietet eine Methode namens FU an, die A.FU ausgibt. Und die Unterklasse bietet auch eine Methode namens FU an, die dann B.FU ausgibt. Was hier wichtig ist, ist, dass die Oberklasse diese Methode FU eben nicht als Virtual deklariert hat, also dieses, was hier stehen könnte, steht eben nicht da. Und die Folge davon ist, ist, dass diese Unterklasse zwar auch eine Methode namens FU anbietet, die zufällig denselben Namen hat, diese Methode überschreibt aber nicht die Methode der Superklasse. Und das hat dann gewisse Folgen in der Form, dass das verändert, welche Methode schlussendlich aufgerufen wird, wie das ganz genau zusammenhängt. Sehen wir in dem späteren Teil dieses Modules noch. Ich würde bloß hier einfach schon mal kurz zeigen, was jetzt passiert, wenn wir diesen Code laufen lassen. Und vorher erkläre ich noch kurz, was hier in der Main-Methode überhaupt passiert. Also hier haben wir eine Variable, die eine Referenz auf ein Objekt von Typ A speichert. Erstellen dann ein Objekt von Typ B. Also dieses Deklarieren von B ruft implizit den Konstruktor von B auf. Schreiben die Adresse dieses Objektes dann in unserer Variable A und rufen auf A dann die Methode FU auf. Und was jetzt hier passiert, wenn wir das Ganze ausführen, ist, dass A Punkt FU aufgerufen wird. Also die Methode der Oberklasse. Und der Grund, warum jetzt nicht diese vermeintlich überschriebene Methode aufgerufen wird, ist, weil die eben nicht überschrieben wurde. A hat diese Methode nicht mit Hilfe von Virtual als Überschreiber deklariert, sondern B hat einfach eine andere Methode namens FU hinzugefügt, die dann hier nicht ausgeführt wird. Also es ist wichtig zu verstehen, dass wenn man Methode überschreiben will in C++, die von der Oberklasse dann tatsächlich auch als Virtual deklariert sein müssen, weil man eben sonst einfach nur eine andere Methode hinzufügt, die dann aber nicht unbedingt das erwartete Verhalten bringt. Eine andere interessante Frage neben der Frage, ob Methoden überschrieben werden können, ist inwiefern Unterklassen die Sichtbarkeit der Methoden oder auch der Felder beeinflussen können. Wie so oft hängt die Antwort hier natürlich wieder von der Programmiersprache ab. Es gibt einige Sprachen wie zum Beispiel Java oder C-Sharp, wo die Unterklasse die Sichtbarkeit der Members nicht verändern können. Also man kann Dinge weder sichtbarer noch weniger sichtbar machen, sondern die Sichtbarkeiten bleiben einfach genau gleich. Was den großen Vorteil hat, dass es eben wieder mit dem Liskoption Substitutability-Prinzip kompatibel ist, denn dann kann kein Kleint überrascht sein, weil plötzlich eine Methode doch nicht sichtbar ist oder von irgendwelchen plötzlich sichtbaren Sachen von wird werden. In anderen Sprachen zum Beispiel Eiffel gibt es da eine andere Antwort. Da können Unterklassen die Sichtbarkeit sowohl verringern als auch erweitern. Also ich kann zum Beispiel von der Klasse erben, in der der Methode private ist und kann die dann aber in der Unterklasse public machen und sozusagen die Sichtbarkeit erweitern und natürlich auch andersrum. Also ich kann auch von der Public-Methode erben und die dann private machen, sodass Instanzen der Subklasse diese Methode plötzlich nicht mehr haben. In C++ ist die Antwort noch ein bisschen komplexer und zwar kann hier die gesamte Vererbung, also die Art und Weise, wie sich die Sichtbarkeit in der Unterklasse verändert, für die komplette Unterklasse festgelegt werden. Also nicht für jeden Member einzeln, sondern man kann explizit angeben, ob die komplette Vererbung public-protected oder private ist. Und was das Ganze bedeutet, ist, dass dann alle geerbten Members maximal public-protected oder private sind. Das heißt, ich reduziere sozusagen die Sichtbarkeit für alle geerbten Members mit einem Mal, indem ich das komplett für die Vererbung festlege. Wenn ich zum Beispiel jetzt Members habe, vielleicht auch manche, die public sind und meine Inheritance ist jetzt private, also ich habe eine Unterklasse, die privately von der Oberklasse erbt. Dann bedeutet das, dass alle Members, auch die, die in der Oberklasse public waren, jetzt nur noch private sind. Weil das mit dem Substitutability Principle nicht wirklich kompatibel ist, impliziert es dann auch keine Subtype-Relationship. Das heißt, wenn Private Inheritance in C++ benutzt wird, dann ist diese Unterklasse kein Subtyp der Oberklasse, denn die Unterklasse bietet ja auch weniger Members an oder zumindest diesen weniger sichtbar als in der Oberklasse und die Typen sind dann auch gar nicht in der Subtype-Relationship, denn der Instanzen vom Untertyp sind ja nicht voll kompatibel mit Instanzen vom Obertyp. Das heißt, an der Stelle gilt quasi nicht mehr das, was ich vorhin gesagt habe, dass in den meisten Sprachen, in den meisten Fällen Subtyping und Subclassing in einem Konzept vereint sind, sondern hier haben wir dann Subclassing, was nicht zu Subtyping führt, sondern der Zweck vom Subclassing ist hier wirklich nur Code-Reuse innerhalb der vererbenden Klasse. Schauen wir uns dazu erst mal wieder ein kleines Beispiel an und zwar wieder in der Form von 2C++-Klassen. Eine Klasse A, das wird die Oberklasse sein, die eine Public-Methode namens FU anbietet und dann eine Klasse B, die von A erbt, wieder mit dieser Doppelpunktnotation. Und was wir hier jetzt machen, ist, wir erstellen einfach eine Instanz von B und rufen dann die Methode FU darauf auf. In einer Sprache wie Java, zum Beispiel, würde das jetzt ohne Probleme funktionieren. Schauen wir mal, was in dem Fall in C++ passiert. Wenn ich das kompiliere, siehe da, bekomme ich ein Fehler, der mir sagt, dass diese Methode FU, die ja in A verfügbar ist, hier nicht als Public-Methode verfügbar ist. Und der Grund ist ganz einfach, dass standardmäßig diese Vererbung eben Private ist. Das heißt, alle Members von A sind in dem Fall Private inklusive dieser Methode FU und ich kann die sozusagen dann nicht von außen wie hier aufrufen. Wenn ich das Ganze jetzt so haben möchte, dass FU Public in B ist, müsste ich diese komplette Vererbung auch Public machen, indem ich hier dieses Public-Keyword hinter den Doppelpunkt schreibe. Und wenn ich das dann kompiliere, dann sollte es auch funktionieren und ich kann den Code auch ausführen und FU wird dann entsprechend aufgerufen. Neben dieser Einregel, die wir gerade in C++ gesehen haben, die es erlaubt, für die komplette Vererbung festzulegen, ob die Private Protected oder Public ist, gibt es noch eine Reihe von anderen Regeln, die in C++ gelten. Eine davon ist, dass die Unterklasse die Sichtbarkeit der Members der Oberklasse niemals erweitern kann. Also ich kann nicht eine Private-Methode erben und die dann Protected oder Public machen. Ich kann die Sichtbarkeit immer nur verringern und zum Beispiel eine Public-Methode Protected oder Private machen. Die andere Regel, die ich hier kurz noch erwähnen möchte, ist, dass die Unterklasse die Super-Klassen-Methode nicht nur verändern kann, sondern sie auch komplett löschen kann. Also ich kann quasi sagen, ich möchte von dieser Klasse erben und deren Code wieder verwenden. Allerdings brauche ich diese und diese Methode nicht, die vielleicht vorher sogar Public war und von nach außen nach außensichtbar war, sondern ich lösche diese Methode und Instanzen der Unterklasse haben dann diese Methode ganz einfach nicht. Und aufgrund dieser ganzen Regeln gilt eben in C++ nicht, dass die Unterklassen unter Typen sind, denn in der Unterklasse fehlen ja dann unter Umständen einige der Members oder sind zumindest weniger sichtbar als in der Oberklasse. Jetzt haben wir ganz viel über Vererbung als ein Mechanismus zum Wiederverwenden von Code geredet. Ich möchte vielleicht kurz noch erwähnen, dass natürlich auch Alternativen gibt. Also nicht immer, wenn man Code wiederverwenden möchte, muss man gleich die Klasse durch Vererbung erweitern, sondern es gibt auch andere Möglichkeiten, das zu machen. Was Vererbung im Prinzip hier macht, ist eine Is-A-Relation zu erstellen, denn die Instanzen der Unterklasse sind dann auch immer Instanzen der Oberklasse. Also das sollte man nur verwenden, wenn das sementisch auch irgendwie Sinn macht, sodass das, was die Unterklasse ausdrückt, auch ein Ding derart ist, wie das, was in der Oberklasse ausgedrückt wird. Oft will man, dass diese Is-A-Relation einer Has-A-Relation eigentlich haben. Also man sagt einfach nur, dass dieses eine Ding etwas hat, was durch das andere Ding repräsentiert wird. Und auch damit kann man den Code dieses anderen Dings oft wiederverwenden. Also zum Beispiel kann man nämlich einfach ein Feld haben, in dem die Klasse, die man wiederverwendet wird, dann gespeichert wird, beziehungsweise eine Instanz davon. Und entsprechend der Aufrufe dann an diese andere Klasse weiter reicht, also forwarded, sodass das Verhalten dann einfach von dieser anderen Klasse zur Verfügung gestellt wird, ohne dass wir jetzt gleich von der Klasse geerbt haben. Wenn ich jetzt zum Beispiel eine Klasse List habe, die einfach ja eine Liste repräsentiert und habe jetzt meine eigene Klasse namens Registrations, in der ich irgendwelche Registrierungen verarbeiten und speichern möchte, dann gibt es da zwei Varianten. Ich kann eine Is-A-Relation herstellen, indem ich von List erbe. Das heißt, ich sage dann, dass Registrations eine Liste ist oder ich kann auch einfach nur ein Feld vom Typ Liste innerhalb meiner Klasse Registrations haben und sage dann, dass meine Registrations eine Liste haben und wenn ich dann zum Beispiel eine neue Registrierung hinzufügen möchte, dann würde ich einfach die entsprechende Add oder so Methode von der Listenklasse aufrufen, ohne dass das jetzt gleich zu einer Verärbung führt. So, zum Abschluss dieses Teils, zum Thema Verärbung noch ein kleines Quiz und zwar in der Form eines Stückes C++-Code, wo die Frage ist, was hier falsch ist. Also irgendwo in diesem Code gibt es einen Compilation Error und die Frage für sie ist, wo genau tritt er auf und natürlich dann auch warum. Ich würde Sie bitten, das Video an der Stelle wieder anzuhalten, im Ilias abzustimmen, wo der Fehler auftritt und anschließend dann erst die Lösung anzuschauen. So, schauen wir die Lösung mal an. Also der Fehler tritt hier in Zeile 17 auf und zwar aus folgendem Grund. Also diese Klasse A bietet hier eine Methode an, namens BA, die Public ist und die wird hier unten dann versucht, über ein Instanz von B aufzurufen. Nun erbt aber B von A im Protected Modus. Das heißt, alles, was B von A erbt, ist maximal protected, aber nicht sichtbar, also insbesondere nicht public und insbesondere heißt das auch, dass BA da nur noch protected ist und wir können es aber hier dann nicht aufrufen, denn ein Client von B kann eben keine Protected Methoden aufrufen. Ja, und das war es auch schon wieder für diesen zweiten von fünf Teilen im Modul Datenabstattion und Objektorientierung. Ich hoffe, Sie wissen jetzt ein bisschen mehr über Vererbung und welche Feinheiten es da in verschiedenen Sprachen gibt. Und ja, können das hoffentlich dann auch mal irgendwo in der Praxis anwenden. Vielen Dank fürs Zuhören und bis zum nächsten Mal.