Vererbung & Polymorphie anhand von Beispiel erklären



  • icarus2 schrieb:

    Nicht-Informatiker

    Wer genau ist die Zielgruppe? Vielleicht kann man dann aus diesem Bereich ein anschauliches Beispiel konstruieren.



  • Danke für die Tipps. Ich werde Shapes verwenden. Das scheint mir ein gutes Beispiel zu sein (auch wenn man aufpassen muss, z.B. mit Square is-a Rectangle). Habs jetzt mal dazu etwas vorbereitet.

    KasF schrieb:

    icarus2 schrieb:

    Nicht-Informatiker

    Wer genau ist die Zielgruppe? Vielleicht kann man dann aus diesem Bereich ein anschauliches Beispiel konstruieren.

    Bauingenieure und Umweltwissenschaftler

    Der Prof hat als Fallbeispiel für Vererbung und Polymorphie numerische Integration genommen (mit Simpson-Regel, Newton-Cotes, Monte-Carlo Intergration etc). Es sind Studenten im 2. Semester, die hatten (und werden vielleicht auch nie) Numerik haben...



  • Eventuell ist es auch hilfreich statt "is a" lieber "behaves like a" zu sagen, um diese Falle mit "aber ein Quadrat *ist* doch ein Rechteck" von Anfang an etwas aus der Schusslinie zu nehmen.



  • Kein Aufrufer will statt

    double f=integriere(&sin,0,3.14);
    

    schreiben

    Integrierer i;
    double f=i.berechne(&sin,0,3.14);
    

    Kurzum, die numerische Integration fühlt sich nicht wie eine Klasse an.

    Wurstbrot&Supermarkt bzw Papabär&Bär sind eher falsch.

    45 Minuten reichen nicht, um mehr zu machen, als ein einfaches Beispiel hinzuklatschen, wobei ich wenns mehr um die Filosofie dahinter geht, zum Vogelfischtier greifen würde und wenn ich praktische Implemetierung anstreben würde, Shapes bevorzugen würde.

    Shape of Nine schrieb:

    Ich würde sehr aufpassen, viele Beispiele sind bei Vererbung etwas tricky. zB Vogel und Fisch von Tier ableiten.

    Sehe darin kein Problem.

    Shape of Nine schrieb:

    Oder einem Vogel die Methode flieg() zu geben.

    Pah, der Strauß kriegt die maximaleFlughöhe=0 gesetzt und der Kunde wird nichtmal bemerken, daß innendrin der Strauß flieg() benutzt.
    "und zeigt warum man eben doch nicht die Natur abbildet mit OOP, sondern nur ein spezielles Modell."



  • icarus2 schrieb:

    Natürlich gibt es die klassichen Beispiele wie z.B. die Tierwelt aber irgendwie finde ich diese doch recht künstlich.

    Es gibt kein gutes Beispiel für Vererbung, weil Vererbung kein gutes Sprachkonzept ist. Es ergibt viel zu selten Sinn.

    Über Java-Schnittstellen kann man aber Sinnvolles sagen.



  • Jester schrieb:

    Eventuell ist es auch hilfreich statt "is a" lieber "behaves like a" zu sagen, um diese Falle mit "aber ein Quadrat *ist* doch ein Rechteck" von Anfang an etwas aus der Schusslinie zu nehmen.

    Der Fehler ergibt sich daraus, dass man über unveränderbare (immutable) Objekte nachdenkt, dann aber veränderbare (mutable) Objekte implementiert.

    Ein unveränderbares Quadrat ist eben ein unveränderbares Rechteck.
    Ein veränderbares Quadrat ist aber kein veränderbares Rechteck.

    Wo's dann etwas schwierig zu erklären wird, ist wenn man sagt dass ein veränderbares Quadrat ein unveränderbares Rechteck ist. Was in Programmcode ohne weiteres möglich ist, und evtl. sogar Sinn macht, aber in Worten einfach schwer zu erklären ist.

    Zumindest fallen mir keine geeigneten Begriffe ein um "kannst du nicht ändern" ("C++ const", .NET ReadOnlyCollection) und "kann sich nicht ändern" (.NET String, delegates) zu unterscheiden.



  • TyRoXx schrieb:

    Es gibt kein gutes Beispiel für Vererbung, weil Vererbung kein gutes Sprachkonzept ist. Es ergibt viel zu selten Sinn.

    Über Java-Schnittstellen kann man aber Sinnvolles sagen.

    Man kann Vererbung in C++ verwenden um Java-artige Schnittstellen umzusetzen.

    Und dabei noch ein Shortcoming von Java-artigen Schnittstellen beseitigen, nämlich dass man bei Java-artigen Schnittstellen keine Default-Implementierung für Schnittstellen-Member machen kann.

    Was auch der Grund ist warum viele Dinge in Java oder C# Klassen sind und keine Schnittstellen (z.B. Streams).



  • hustbaer schrieb:

    TyRoXx schrieb:

    Es gibt kein gutes Beispiel für Vererbung, weil Vererbung kein gutes Sprachkonzept ist. Es ergibt viel zu selten Sinn.

    Über Java-Schnittstellen kann man aber Sinnvolles sagen.

    Man kann Vererbung in C++ verwenden um Java-artige Schnittstellen umzusetzen.

    * In Java, denn darum geht es hier gerade.

    hustbaer schrieb:

    Und dabei noch ein Shortcoming von Java-artigen Schnittstellen beseitigen, nämlich dass man bei Java-artigen Schnittstellen keine Default-Implementierung für Schnittstellen-Member machen kann.

    Das ist ein Vorteil. Wozu sollte eine Default-Implementierung gut sein?

    hustbaer schrieb:

    Was auch der Grund ist warum viele Dinge in Java oder C# Klassen sind und keine Schnittstellen (z.B. Streams).

    Ich dachte immer das läge daran, dass Schnittstellen und Klassen da oft viel zu groß und möglichst eine Eierlegende sind.



  • TyRoXx schrieb:

    * In Java, denn darum geht es hier gerade.

    Sorry, hab' nur deinen Beitrag gelesen, und da du explizit "Java Schnittstellen" geschrieben hast, dachte ich es geht hier allgemein um Vererbung und nicht speziell um Java.

    TyRoXx schrieb:

    Das ist ein Vorteil. Wozu sollte eine Default-Implementierung gut sein?

    Weil man öfters Interfaces hat, wo es eine Funktion gibt die im Prinzip ausreicht, aber dann ein paar weitere die z.B. auf Geschwindigkeit optimiert sind. Bzw. sein können -- aber nicht in jeder Implementierung unbedingt optimiert werden müssen.

    Beispiel: Java's InputStream Klasse hat folgende Methoden:

    abstract int read();
    int read(byte[] b);
    int read(byte[] b, int off, int len);
    long skip(long n);
    

    Um einen (nicht optimierten) InputStream zu implementieren, muss man davon nur int read() implementieren. Die Default-Implementierung der anderen Funktionen ruft einfach wiederholt read() auf. Was in vielen Fällen gut genug ist.

    Weiters kann man so optionalen Funktionen eine Default-Implementierung mit throw new UnsupportedOperationException() verpassen.

    Natürlich könnte man das selbe auch mit Mixins machen, was vermutlich die elegantere Lösung wäre. Nur leider unterstützen Java, C#, C++ etc. keine Mixins.

    TyRoXx schrieb:

    hustbaer schrieb:

    Was auch der Grund ist warum viele Dinge in Java oder C# Klassen sind und keine Schnittstellen (z.B. Streams).

    Ich dachte immer das läge daran, dass Schnittstellen und Klassen da oft viel zu groß und möglichst eine Eierlegende sind.

    Dass das Klassen-Design der Java Standard Library nicht unbedingt das beste ist, ist klar. Gut. Was wirklich der Grund für irgendwas war kann man nachträglich oft nur mehr schwer sagen. Es sei denn man weiss zufällig warum Dinge damals so entschieden wurden.

    Sagen wir so: es ist der Grund warum man auch heute noch, in neuem Code, von Leuten die wissen was sie tun, immer noch Klassen findet, wo man eigentlich Interfaces verwenden würde. Wenn da eben nicht die Sache mit der Default-Implementierung wäre. Und es ist leicht möglich dass es der Grund, oder einer der Gründe war, dass man sowas auch in der Java Standard Library findet.



  • hustbaer schrieb:

    TyRoXx schrieb:

    Das ist ein Vorteil. Wozu sollte eine Default-Implementierung gut sein?

    Weil man öfters Interfaces hat, wo es eine Funktion gibt die im Prinzip ausreicht, aber dann ein paar weitere die z.B. auf Geschwindigkeit optimiert sind. Bzw. sein können -- aber nicht in jeder Implementierung unbedingt optimiert werden müssen.

    Vielleicht kann man das so zusammenfassen: eigentlich sollte ein Interface zwei Seiten haben, nämlich eine, die der Benutzer sieht, und eine, die der Implementierende erfüllen muß.

    Anwendungen dafür gibt es viele. Eine Standardimplementierung wie in deinem Beispiel wäre eine; eine andere das template method pattern. Oder mein Interface hat eine searchFoo() -Methode, die ich mit einer findFoo() -Methode augmentieren möchte, die eine Exception wirft, anstatt Null zurückzugeben, deren Implementierung aber natürlich generisch ist, weil sie nur searchFoo() aufruft, so daß es lästig wäre, sie jedes Mal mitimplementieren zu müssen.

    Zum Thema: ich finde die Sinnhaftigkeit von Vererbung bei vielem, was mit UI zu tun hat, einigermaßen naheliegend. Z.B. die Steuerelemente in einem Fenster; jedes hat eine Position und eine Größe oder eine Eigenschaft wie Visible oder Enabled oder eine Schriftart; und wenn ich eine Gruppe von Steuerelementen aktiviere oder deaktiviere, will ich alle gleich behandeln können. Und wenn ich einen Button haben möchte, der neben der Beschriftung eine kleine Graphik darstellen kann, ist es sinnvoll, von Button abzuleiten und nur die Zeichenroutine zu überschreiben und eine zusätzliche Eigenschaft hinzuzufügen.



  • TyRoXx schrieb:

    icarus2 schrieb:

    Natürlich gibt es die klassichen Beispiele wie z.B. die Tierwelt aber irgendwie finde ich diese doch recht künstlich.

    Es gibt kein gutes Beispiel für Vererbung, weil Vererbung kein gutes Sprachkonzept ist. Es ergibt viel zu selten Sinn.

    Falsch.



  • hustbaer schrieb:

    TyRoXx schrieb:

    Das ist ein Vorteil. Wozu sollte eine Default-Implementierung gut sein?

    Weil man öfters Interfaces hat, wo es eine Funktion gibt die im Prinzip ausreicht, aber dann ein paar weitere die z.B. auf Geschwindigkeit optimiert sind. Bzw. sein können -- aber nicht in jeder Implementierung unbedingt optimiert werden müssen.

    Das ist Premature Optimization. Und zwar die besonders eklige Sorte, weil so eine öffentliche Schnittstelle nie wieder geändert werden kann. Vielleicht fällt jemandem auf einmal eine "noch optimalere" Signatur ein. Wird dann noch eine Methode hinzugefügt? Und noch eine und noch eine?

    hustbaer schrieb:

    Beispiel: Java's InputStream Klasse hat folgende Methoden:

    abstract int read();
    int read(byte[] b);
    int read(byte[] b, int off, int len);
    long skip(long n);
    

    Um einen (nicht optimierten) InputStream zu implementieren, muss man davon nur int read() implementieren. Die Default-Implementierung der anderen Funktionen ruft einfach wiederholt read() auf. Was in vielen Fällen gut genug ist.

    Zur Not kann die JVM solche "optimierten" Methoden generieren. Macht sie nicht? Hmm, scheint sich wohl nicht zu lohnen. Dann lohnt sich das erst recht nicht für Menschen, die sich ewig mit redundanten Schnittstellen herumschlagen müssen.

    So hätte ich InputStream und so entworfen:

    public class InputStreamExamples {
    
    	public static void main(String[] args) {
    		InputStream s = new GeneratorInputStream(new ByteGenerator() {
    
    			@Override
    			public int read() {
    				return (int) '!';
    			}
    		});
    		byte[] b = new byte[2];
    		int r = s.read(b);
    		assert (r == b.length);
    		assert (b[0] == '!');
    		assert (b[1] == '!');
    	}
    }
    
    public interface ByteGenerator {
    	int read();
    }
    
    public interface InputStream {
    	int read(byte[] b);
    	//...
    }
    
    public class GeneratorInputStream implements InputStream {
    
    	private final ByteGenerator next;
    
    	public GeneratorInputStream(ByteGenerator next) {
    		this.next = next;
    	}
    
    	@Override
    	public int read(byte[] b) {
    		int r = 0;
    		for (; r < b.length; ) {
    			int element = next.read();
    			if (element < 0) {
    				break;
    			}
    			b[r] = (byte)element;
    			++r;
    		}
    		return r;
    	}
    }
    

    hustbaer schrieb:

    Weiters kann man so optionalen Funktionen eine Default-Implementierung mit throw new UnsupportedOperationException() verpassen.

    .. was eine ganz schlechte Idee ist. Wenn Schnittstellen anfangen zu lügen, kann man sich die auch gleich sparen.



  • TyRoXx schrieb:

    hustbaer schrieb:

    TyRoXx schrieb:

    Das ist ein Vorteil. Wozu sollte eine Default-Implementierung gut sein?

    Weil man öfters Interfaces hat, wo es eine Funktion gibt die im Prinzip ausreicht, aber dann ein paar weitere die z.B. auf Geschwindigkeit optimiert sind. Bzw. sein können -- aber nicht in jeder Implementierung unbedingt optimiert werden müssen.

    Das ist Premature Optimization. Und zwar die besonders eklige Sorte, weil so eine öffentliche Schnittstelle nie wieder geändert werden kann. Vielleicht fällt jemandem auf einmal eine "noch optimalere" Signatur ein. Wird dann noch eine Methode hinzugefügt? Und noch eine und noch eine?

    🙄
    Nein, es ist nicht premature, es ist bitter nötig. Leute wie du sind der Grund warum so viele Programm so unnötigerweise so langsam sind.
    Kannst du noch mehr als Phrasen dreschen?

    TyRoXx schrieb:

    hustbaer schrieb:

    Beispiel: Java's InputStream Klasse hat folgende Methoden:

    abstract int read();
    int read(byte[] b);
    int read(byte[] b, int off, int len);
    long skip(long n);
    

    Um einen (nicht optimierten) InputStream zu implementieren, muss man davon nur int read() implementieren. Die Default-Implementierung der anderen Funktionen ruft einfach wiederholt read() auf. Was in vielen Fällen gut genug ist.

    Zur Not kann die JVM solche "optimierten" Methoden generieren. Macht sie nicht? Hmm, scheint sich wohl nicht zu lohnen. Dann lohnt sich das erst recht nicht für Menschen, die sich ewig mit redundanten Schnittstellen herumschlagen müssen.

    Geh mal zum Aldi und hol dir ne Buddel Realität. Oder besser ne ganze Kiste.
    Nein, die JVM kann das nicht. Und sie könnte es auch nicht können.
    Boah.

    TyRoXx schrieb:

    So hätte ich InputStream und so entworfen:
    (...)

    Ja so stellt sich klein TyRoXx das vor.
    Probier' es doch einfach aus. Mach nen FileInputStream auf ein mittelgrosses File, sagen wir ein paar hundert MB.
    Und dann lies es 1x mit read(), und 1x mit read(byte[]) mit nem sagen-wir-mal 1MB grossen Array.
    Dann siehst du was Sache ist.

    TyRoXx schrieb:

    hustbaer schrieb:

    Weiters kann man so optionalen Funktionen eine Default-Implementierung mit throw new UnsupportedOperationException() verpassen.

    .. was eine ganz schlechte Idee ist. Wenn Schnittstellen anfangen zu lügen, kann man sich die auch gleich sparen.

    Siehe oben: ne Buddel Realität.

    -----

    In Summe: Wenn man keine Ahnung hat, einfach mal die Fresse halten!



  • EDIT: Ich weiß gerade gar nicht was dein Problem ist. Man muss bei meinem Ansatz ja nicht den ByteGenerator benutzen, wenn es schnell sein soll.

    Ich wollte nur zeigen, dass es nicht nötig ist zwei Schnittstellen zu einer zu verschmelzen. Und wenn man die Schnittstellen nicht unnötig groß macht, braucht man auf einmal keine Default-Implementierung für irgendwas mehr.

    Das Verschmelzen zweier Schnittstellen ist die von mir gemeinte Premature Optimization. Nicht das Lesen in Arrays.



  • TyRoXx schrieb:

    EDIT: Ich weiß gerade gar nicht was dein Problem ist. Man muss bei meinem Ansatz ja nicht den ByteGenerator benutzen, wenn es schnell sein soll.

    Ich wollte nur zeigen, dass es nicht nötig ist zwei Schnittstellen zu einer zu verschmelzen. Und wenn man die Schnittstellen nicht unnötig groß macht, braucht man auf einmal keine Default-Implementierung für irgendwas mehr.

    Das Verschmelzen zweier Schnittstellen ist die von mir gemeinte Premature Optimization. Nicht das Lesen in Arrays.

    Ergibt das überhaupt Sinn, wenn Du (wie Du selber sagst) Vererbung nirgends sinnvoll anwenden kannst, über Schittstellendesign zu referieren?





  • KennerDesJava8 schrieb:

    Kennt ihr eigentlich schon Java 8?
    http://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

    Nö, kannte ich nicht!
    Cool, danke!



  • volkard schrieb:

    Ergibt das überhaupt Sinn, wenn Du (wie Du selber sagst) Vererbung nirgends sinnvoll anwenden kannst, über Schittstellendesign zu referieren?

    Entschuldige bitte meine Undeutlichkeit. Ich möchte nur von Implementierungsvererbung abraten. Schnittstellenvererbung hingegen ist sehr wichtig.

    Für Implementierungsvererbung sehe ich keine sinnvolle Anwendung.
    In C++ gibt es da noch reine Implementierungsvererbung, also private Vererbung. Das ist etwas ganz anderes, weil keine öffentliche Methode überschrieben wird, und deswegen in Ordnung.



  • icarus2 schrieb:

    Angenommen ihr müsstet einer Gruppe von ca. 30 Studenten innerhalb von 45 Minuten Vererbung und Polymorphie erklären: Welches Beispiel würdet ihr dafür verwenden?

    Ich verwende in der Regel eine Klasse, die Strings verschlüsselt: Es gibt eine Basisklasse Encrypter mit virtuellen Funktionen encrypt und decrypt, davon leite ich Atbash (a<->z, b<->y...) und Caesar ab. Allerdings bringe ich den Leuten C++ praktisch bei. Das heißt, die programmieren das anschließend. Das ist mit 45 Minuten etwas zu knapp.

    Die Tierwelt finde ich für kleine Beispiele gut, nutze das Beispiel auch. Wenn viel Zeit ist, es also um ein Test-Projekt geht, halte ich die Shapes am besten, wenn Zeit genug ist, diese auch zu zeichnen.

    Ansonsten gefällt mir das beispiel im GoF-Entwicklungsmuster, wo ein Editor besprochen wird, der Zeilen aus einzelnen Zeichen zusammensetzt, die Grafiken oder Zeichen sein können, die ihre Größe auf dem Bildschirm zurückliefern und sich zeichnen lassen können.

    Kannst Du aber alles vergessen: In 45 Minuten würde ich schätzen, dass Du vielleicht 5 das verständlich machen kannst und die vergessen es bis zum Abend wieder. Von daher ist es relativ egal, was Du erzählst, die Zeit reicht nicht, um etwas sinnvoll zu vermitteln.

    cloidnerux schrieb:

    Vererbung und Polymorphie wurden innerhalb von zwei Vorlesungsstunden (2 Mal 45 Minuten) eingeführt. Sie sollte also schon einmal davon gehört aber wohl kaum richtig verstanden haben, da es sehr schnell ging (und jetzt muss ich das innerhalb von 45 Minuten richten 🙂 ).

    Ich habe die Erfahrung gemacht, dass man bei Studenten im 3. Semester quasi bei 0 anfängt, obwohl die schon 2 Semester Vorlesungen hatten. Gerade bei Java sind die Vorstellungen teils sehr... virtuell.
    Ich erkläre Vererbung und Polymorphie daher an der tatsächlichen Umsetzung: Ich erkläre eine Struktur, dass die Daten bei Vererbung hintereinander geschrieben werden, dass eine Methode auch nur eine Funktion mit this-Pointer ist und dass man über Funktionzeiger, bzw. die vtable an die Funktionszeiger kommt.
    Das ist eher imperative Herangehensweise, aber OOP ist auch nur ein imperatives Design-Pattern. Wenn man OOP die Magic nimmt, wird es leichter begreifbar.

    Das klappt eigentlich sehr gut, jedenfalls schließe ich das aus Aussagen wie "Jetzt habe ich verstanden, was ich die letzten zwei Semester eigentlich getan habe".

    Unter Berücksichtigung, dass sie das eigentlich ja schonmal gehört haben, würde ich schätzen, dass Du hier den Leuten am ehesten etwas Nützliches mitgeben kannst, weil sie eigentlich Bekanntes aufgreifen müssen und zusätzliche Informationen erhalten.

    Benutze eine Tafel, keine Präsentation, stelle richtungsführende Fragen und lass die Studenten das entwickeln. Wenn Du denen nur was von Rechtecken erzählst, musst Du die Hälfte danach wecken. Setz Dir sinnvolle Ziele für 45 Minuten Show und akzeptiere, dass Du in 45 Minuten mit einem kleinen Schritt weiter kommst, als wenn Du einen großen Versuchst und nur Verwirrung stiftest.



  • Xin schrieb:

    icarus2 schrieb:

    Angenommen ihr müsstet einer Gruppe von ca. 30 Studenten innerhalb von 45 Minuten Vererbung und Polymorphie erklären: Welches Beispiel würdet ihr dafür verwenden?

    Ich verwende in der Regel eine Klasse, die Strings verschlüsselt: Es gibt eine Basisklasse Encrypter mit virtuellen Funktionen encrypt und decrypt, davon leite ich Atbash (a<->z, b<->y...) und Caesar ab.

    Warum hat die Klasse Encrypter eine Methode decrypt ? Das ist ein Widerspruch.
    Warum hat Encrypter überhaupt Methoden für beide Richtungen? Man benötigt doch an einer Code-Stelle so gut wie immer nur eine der beiden Richtungen. Die Schnittstelle ist also unnötig groß.
    Und lass mich raten: Die beiden Methoden sind bei Caesar fast identisch. An einer Stelle steht so etwas wie 26-n wo in der anderen Methode nur n steht.

    Xin schrieb:

    Ich erkläre Vererbung und Polymorphie daher an der tatsächlichen Umsetzung: Ich erkläre eine Struktur, dass die Daten bei Vererbung hintereinander geschrieben werden, dass eine Methode auch nur eine Funktion mit this-Pointer ist und dass man über Funktionzeiger, bzw. die vtable an die Funktionszeiger kommt.
    Das ist eher imperative Herangehensweise, aber OOP ist auch nur ein imperatives Design-Pattern. Wenn man OOP die Magic nimmt, wird es leichter begreifbar.

    👍


Anmelden zum Antworten