Der obligatorische Literaturhinweis, das, was wir jetzt besprechen,
die Java-API, die Programmierschnittstelle, die in der Java-Umgebung
bereitgestellt wird, findet man auch Dokumentation zu, hier in diesem
Buch, was ich genannt hatte, von Wiesenberger und anderen.
Zum einen im Kapitel 12, wo Hilfsklassen auch konzeptionell eingeführt
werden.
Genau das ist auch das, was wir heute machen.
Und auch nachher nochmal im Anhang eine Erläuterung zum Umgang mit der
Spezifikation.
Ja, also wie gesagt, jetzt hier in dieser Vorlesung erstmal, wir haben
uns wahrscheinlich gar nicht die ganzen 90 Minuten, sondern
wahrscheinlich nur so 40, 45 Minuten beschäftigt, die Java-API.
Also was kann man sich erst einmal API vorstellen?
API ist eine Abkürzung für Application Programming Interface.
Das ist also für den Entwickler von Softwareanwendungen eine
Schnittstelle, auf die er zurückgreifen kann, wo er jede Menge
sinnvolle Klassen schon vordefiniert findet, die er nicht mehr selber
oder sie nicht mehr selber schreiben muss, sondern wo man sagt, die
kann ich einfach verwenden, um meine Anwendung zu bauen, um mich zu
konzentrieren eigentlich auf meine Anwendung.
Das ist auch sinnvoll und weil man das nicht nur, wie soll ich sagen,
im Sinne der Produktivität sinnvoll ist, sondern auch im Sinne der
Wartbarkeit des Programmes, weil die Programme, die eine API
verwenden, natürlich besser verständlich sind für den nächsten Leser,
als wenn jemand es sich selber überlegt hat.
Das ist auch sinnvoll im Sinne der Qualität.
Das ist klar, bevor ich jetzt selber wieder meine Xplus-erste-Listen
-Implementierung mache, mit den klassischen Fehlern, die man immer so
gerne macht beim ersten Mal hinschreiben, kann man sich alles sparen.
Hier gibt es die Klassen schon.
Es gibt einige Sachen, die kennen Sie schon.
Objekte kennen Sie klar.
String hatten wir schon kennengelernt, etwas die Klasse.
Comparable hatten wir schon mal kurz angesprochen.
Das sind alles Sachen, die ein Teil eben von dieser API sind.
Weitere Informationen finden Sie hier unter der Online-Dokumentation
zum Java-System natürlich.
Und das Ganze ist sehr, sehr, sehr umfangreich.
Wichtige Pakete sind javalang.
Das ist, wo wir auch schon einiges heraus kennengelernt haben.
javayutil, da werden wir heute noch ein bisschen was drüber reden.
Und java.io werden wir nur ganz knapp streifen.
Aber das sind nur drei von, wie Sie sehen, jeder Menge Paketen.
Das ist jetzt noch ein hierarchischer Überblick über das, was haben
Sie in Java.
Sie haben irgendwo alles zusammengefasst, kann man sagen, ist die Java
-Sprache.
Da gibt es natürlich verschiedene Programme dazu.
Die Ausführungsumgebung, den Compiler, System und Packages zu machen,
ja und so weiter.
Ich will jetzt gar nicht im Detail auf all das eingehen.
Das soll Ihnen eigentlich nur mal zeigen, wie viel es eigentlich hier
an der Stelle gibt.
Wir werden über einen ganz kleinen Teil heute in der Vorlesung
sprechen, aber es ist der Teil, wo man vielleicht ein gewisses
konzeptionelles Verständnis für haben sollte, nämlich die sogenannten
Collections, das sogenannte Java-Collection-Framework.
Was ist eine Collection?
Das ist im Prinzip nichts anderes als eine Sammlung von Objekten.
Und es ist eine einheitliche Architektur, ein einheitliches Konzept.
Wie kann man die verschiedenen Datentypen, die man nutzen kann, um
Sammlungen zu verwalten, sinnvoll ordnen.
Es gibt im Prinzip zwei Klassenbäume.
Das eine ist Collections als abstrakte Oberklasse und dann gibt es
hier verschiedene Varianten, wie man das realisiert.
Einmal als Menge, einmal als Liste, einmal als Schlange und bei der
Liste nochmal den Spezialfall der sortierten Liste.
Wir werden nachher sehen, das sind alles Interfaces, nochmal mit
verschiedenen Implementierungen dahinter.
Und auf der anderen Seite gibt es dann noch eine Abbildung.
Das kann man sich vorstellen, dass man so Schlüssel-Wertpaare, Key
-Value -Pairs hat.
Und dann gibt es verschiedene Implementierungen hierfür.
Genauso nochmal das Ganze spezialisiert als sortierte Abbildung.
Wenn wir erstmal hier in die Collections reingehen, sehen wir, das ist
eine Schnittstelle, die erstmal über einen beliebigen Datentyp E
arbeitet.
Sie sehen also hier das als generic formuliert.
Und Collection ist erstmal das Allgemeinste.
Das heißt, es macht keine Aussage darüber, ob ich eine geordnete Liste
habe, wo es eine Ordnung gibt, erste, zweite, dritte Position.
Es macht keine Aussage darüber, ob ich eine Mengensemantik habe,
sprich keine Duplikate habe.
Das ist klar, Sie können dasselbe Element nicht zweimal in einer Menge
speichern.
Das wird alles hier erstmal gar nicht spezifiziert, sondern man
verwendet eben Collection, wenn man möglichst wenig Einschränkungen
machen soll.
Und dann gibt es eben die spezielleren Collections mit zusätzlichen
Randbedingungen, z.B.
Set als Menge hat keine Duplikate, List hat eine Reihenfolge, Queue
hat auch eine Reihenfolge.
Das sind jetzt spezielle Collections.
Aber wenn man sich jetzt erstmal nur das Interface Collection
anschaut, sieht man, hat man hier sehr, sehr elementare Routinen
definiert.
Man kann die Größe abfragen mit Size.
Man kann mit IsEmpty abfragen, ob sie leer ist.
Contains kann man prüfen, ob ein Element enthalten ist.
Mit Add kann man ein Element hinzufügen.
Mit Remove kann man das Element wieder entfernen.
Man kann sich einen Iterator darüber besorgen.
Sie erinnern sich an die Iteratoren, die wir besprochen hatten.
Man kann prüfen, ob eine andere Collection hier enthalten ist.
Jetzt dieser ulkige Fragezeichen-Operator hier.
Das werden wir nachher ausführlich besprechen, was das bedeutet.
Man kann eine ganze andere Collection hinzufügen.
Auch wieder hier Fragezeichen-Extens E.
Besprechen wir nachher, was das heißt.
Oder man kann auch eine ganze Collection wieder entfernen.
Man kann das Ganze in eine Array konvertieren.
Man kann es einmal als Objekte zurückbekommen oder auch typisiert als
Array von diesem Typ.
Jetzt machen wir einen kleinen Exkurs, um dieses Fragezeichen, das wir
hier hatten, zu erklären.
Was bedeutet das?
Und zwar ist das Fragezeichen, das man Wildcard, also Joker, nennt,
für diese generische Typvariable, vom Generic.
Was heißt das?
Im Prinzip wollen Sie sich damit sagen, ich habe eine Collection über
einen Typ, den ich hier aber noch nicht festlege.
Aber hier muss ein fester Typ nachher rein.
Zum Beispiel Integer oder Double oder String.
Es dürfen, wenn das Fragezeichen so alleine steht, hier gar keine
Annahmen gemacht werden.
Das kann also sehr allgemein sein, das kann Objekt sein, das kann aber
auch sehr speziell sein.
Das Interessante ist, ich kann über das Fragezeichen jetzt anfangen,
Einschränkungen zu machen.
Also wie gesagt, hier, wenn das Fragezeichen alleine ist, heißt das
beliebiger, aber fester Typ.
Wenn ich jetzt zum Beispiel Fragezeichen Super T habe, dann heißt das
nicht Oberklassen von T.
Und Fragezeichen Extens heißt, Fragezeichen kann T- oder Unterklassen
sein.
Und wenn wir uns mal das hier an diesem Beispielvererbungsbaum, den
wir schon mal so ähnlich hatten, über unsere verschiedenen Tierklassen
anschauen, also Katze, Hund, Säugetier, Tier, lebender Organismus und
natürlich als oberstes immer Objekt, dann, wenn ich Fragezeichen habe,
kann alles davon eingesetzt werden.
Wenn ich jetzt sage, nein, das Ganze muss eine Oberklasse von Animal
sein, dann kann es Animal sein oder eben jede Oberklasse.
Und das Pendant dazu ist Fragezeichen Extens Animal.
Wenn man sagt, ich kann Animal oder eine Spezialisierung, eine
Ableitung von Animal nehmen, dann werden diese Klassen möglich, hier
die rot eingefärbten, also alle Unterklassen von Animal.
Sie sehen, das geht logischerweise natürlich transitiv weiter.
Jetzt gibt es noch eine andere Sache, die nicht ganz unwichtig ist,
dass man es einmal verstanden hat.
Es läuft manchmal über den Begriffen Co-Varianz, In-Varianz.
Was damit gemeint ist, ist, wie verändern sich
Spezialisierungsbeziehungen, Vererbungsbeziehungen, wenn ich jetzt
sowas wie Arrays oder Collections bilde.
Hier sieht man das mal in dem Beispiel.
Ich habe ein Integer als Unterklasse von Number.
Und daraus folgt, weil nämlich das mit Arrays Co-Variant-Typsystem bei
Java behandelt werden, dass ein Array von Integer auch eine
Unterklasse von ein Array von Number ist.
Also langsam, was ist hier passiert.
Durch die Array-Bildung auf beiden Seiten, einmal Integer, einmal
Number, ändert sich die Richtung der Vererbung nicht.
Deswegen Co-Variant.
Also mit dem Draufsatteln von der Vererbung des Arrays bleibt die
Vererbungsrichtung dieselbe.
Nämlich ein Array von Integer ist immer noch eine Unterklasse von
einem Array von Number.
Hier sehen wir, was wir im Code damit machen können.
Wir können zum Beispiel sagen, wir haben A mit dem statischen Typ
Array von Number und wir weisen ihm ein Integer-Array der Größe 1 zu.
Und dann können wir zum Beispiel A0 einen Integer zuweisen.
Das ist kein Problem.
Man könnte sagen, wenn das so ist mit der Co-Variance bei den Arrays,
dann gilt das vielleicht auch bei den Generics.
Und das gilt leider nicht so.
Das ist invariant, das heißt man kann nichts daraus folgern.
Also wenn ich jetzt zum Beispiel eine Arraylist von Number habe und
eine Arraylist von Integer, stehen die eben nicht in einer
Beerbungsbeziehung.
Auch wenn Integer erbt von Number.
Das heißt also so eine Zuweisung hier, wie wir das hier oben noch
gemacht hatten, würde nicht gehen.
Es gibt einen Kompilierfehler.
Wenn wir jetzt sagen, ich habe eine Liste und da darf ein Typ
Parameter rein, der eine Spezialisierung von Number ist und darum
erzeuge ich konkret eine neue Arrayliste.
Dann geht das und zwar deswegen, weil natürlich Integer eine
Spezialisierung von Number ist.
Aber per Default geht es halt eben nicht.
Und dann, wenn ich jetzt sage, ich möchte jetzt hier diesem Add
integer aufrufen, das geht wieder nicht, weil nämlich eigentlich wurde
hier nur ein Number erwartet.
Jetzt könnte man sich fragen, warum ist das so?
Das ist doch irgendwie ein bisschen holprig.
Und die Antwort ist, ja ist es.
Es liegt aber daran, weil man hier bei den Generics eben diese
eingeschränkten Wildcard-Operatoren erlaubt hat.
Und deswegen hat man gesagt, also wenn, dann muss hier einer explizit
hinschreiben, wenn er eine Subtyp-Beziehung haben will.
Dann muss das im Prinzip hier auch erlauben, dass eben das, was
eingesetzt wird, hier tatsächlich auch ein Subtyp von was anderem ist.
Und dann kann man diese Zuweisung machen.
Nur man sieht halt eben, dummerweise ist das für das B immer noch ein
List von Number für das Typ-System beim statischen Prüfen des
Programms und hier an der Stelle würde das dann eben deswegen nicht
gehen, weil B.Add hier auch eine Number erwarten würde.
Ja, wie verwendet man jetzt diese Wildcards?
Also, das hatten wir jetzt in dem Beispiel gesehen.
Durch diesen Fragezeichen-Operator mit Extents kann ich erzwingen,
dass das, was hier eingesetzt wird, entweder T ist oder eine
Unterklasse von T.
Und damit sagt man auch manchmal, T ist der Upper Bound, die obere
Schranke.
Wenn wir uns unser Beispiel von diesem Vererbungsbaum anschauen, haben
wir gesehen, wir können alle Unterklassen nehmen, aber eben höher,
allgemeiner als T können wir nicht gehen.
Ja, könnte man sicher noch sagen, naja, das Allgemeinste, wenn ich
jetzt hier so was habe, List, Spitze, Klammern, Fragezeichen und
Spitze, Klammern, Objekt, wo ist hier eigentlich der Unterschied, weil
Objekt ist ja auch das Allgemeinste.
Syntaktisch kann man erstmal beides hinschreiben, aber es bedeutet
etwas anderes.
Und zwar List mit dem Wildcard-Operator heißt, es muss eine Liste von
einem festen Typ geben, der Typ ist aber nicht eingeschränkt.
Wohingegen List von Objekten heißt, ich kann alles, entweder Objekte
oder Spezialisierung von Objekten, speichern in einer Liste und zwar
konterbunt durcheinander.
Das eine Element könnte eben String sein, das andere könnte ein
Integer sein.
Das geht beides, weil beides mal String und Integer finalerweise
natürlich Spezialisierung von Objekten sind.
Das heißt, bei einer Liste von Objekten sind ganz unterschiedliche
Elementtypen gleichzeitig möglich.
Das geht bei der nicht, das ist eine homogene Liste.
Das heißt, es ist immer derselbe Typ hier, was immer hier einmal
eingesetzt wurde.
Und das sehen wir jetzt auch wieder hier bei unserem Beispiel.
Genauso, wenn ich ein Objekt habe, kann ich das einsetzen beliebig,
weil das ist alles Spezialisierung von Objekten.
Hier kann ich es aber nicht einsetzen, weil das im Prinzip immer noch
das Allgemeinste ist und Integer schon zu speziell ist.
So, nochmal unser Interface, jetzt nachdem wir das gesehen haben.
Die interessanten Operationen sind hier bei ContainsAll, kann man eine
beliebige Collection eingeben von einem beliebigen Typ.
Bei AddAll kann man nur entweder Elemente, die vom Typ E sind, wo
meine Collection E von der Typ ist, oder aber Spezialisierung,
Unterklassen, davon einfügen.
Das sollte man auch sinnvollerweise erlauben, weil ich kann es im
Prinzip eh nicht verhindern vom Typsystem her.
Ich kann natürlich immer eine Spezialisierung in den Kontext der
Oberklasse einsetzen.
So, jetzt wird es ein bisschen interessanter.
Jetzt spezialisieren wir diese Collection-Schnittstelle und zwar
einmal eine Liste.
Das heißt, ich habe jetzt eine geordnete Collection, die kann
allerdings auch Duplikat enthalten.
Das hat jetzt einfach eine Listen-Semantik und mit Liste meint man,
man kann jetzt auch mit einem Index darauf zugreifen.
Und es gibt zwei Implementierungen, die Sie auch schon verstehen.
Einmal ArrayList, das heißt, ich habe das Ganze mit einem Array
finalerweise implementiert innen drin.
Oder aber, das hatten wir ja auch gehabt, bei rekursiven
Datenstrukturen mit einer verknüpften Liste, also mit einer
LinkedList, die ich durchnavigieren kann.
Dann gibt es eine weitere Spezialisierung, die der Menge, Set.
Hier geht es darum, eine mathematische Menge zu modellieren, sprich
ich habe keine Duplikate drin.
Hier gibt es auch zwei Implementierungen.
Einmal als HashSet, wo man letzten Endes etwas einfügt, indem man es
einem bestimmten Wert gibt.
Ein HashSet, das werden Sie in der Algorithmik-Vorlesung später noch
kennenlernen, im Weiterverlauf des Studiums.
Und dadurch abspeichert, dass es da ist.
Oder ein TreeSet, wo man das Ganze letztendlich in einer baumartigen
Datenstruktur einsortiert und dann nachher auch wieder über das
Durchgehen in den Baum sehr schnell finden kann.
Oder es gibt die dritte Variante, eine sortierte Liste.
Das ist eine Spezialisierung hiervon, das heißt es gibt also auch
keine Duplikate.
Hinzu kommt noch, dass das Ganze aufsteigend sortiert ist.
Man kann das Ganze nachher auch auslesen, sofort in der richtigen
Reihenfolge.
Das kann man natürlich bei der normalen Menge nicht.
Und die Implementierung, die es hierzu gibt, ist TreeSet, wie wir es
auch schon hier hatten.
Liegt daran, weil es in den Baum immer einsortiert wird, an die
richtige Stelle im Baum, kann ich nachher den Baum traversieren und
ich habe immer die korrekte Reihenfolge, die richtige Sortierung.
So, das waren jetzt diese drei Spezialisierungen.
Ich gehe nochmal gerade zurück auf unsere Anfangspolien.
Was wir jetzt gemacht haben, war also die Schnittstelle Collection
durchzusprechen und zu sagen, es gibt jetzt hier drei
Spezialisierungen davon, von denen es dann auch wieder nochmal
verschiedene Implementierungen gibt und wir haben uns auch Set und
SortedSet angeguckt, die List.
Die Schlange haben wir uns bisher noch nicht angeguckt.
Letzten Endes hat das eine Last-In-First-Out-Semantik.
Wie das bei einer Schlange ist, sie können im Prinzip Dinge in die
Schlange einreihen und sie können hinten was einreihen und auf der
anderen Seite wieder rausholen.
Also, sorry, First-In-First-Out, das wurde so in der Reihenfolge
gespeichert.
Was wir jetzt noch machen müssen, ist, wir müssen uns die Map angucken
und die SortedMap.
Diese Map, zu deutsch Abbildung, modelliert eine mathematische
Funktion von Schlüsseln nach Werten und so im Englischen heißt
Schlüssel Key, Value, deutsch Wert.
Und weil das Ganze eine Funktionseigenschaft ist, darf eine Map einen
Schlüssel nicht mehrfach enthalten.
Das heißt, ein Schlüssel darf immer nur auf einen Wert abbilden.
Und jetzt kann ich erstmal darauf zugreifen, ich kann die Abbildung im
Prinzip mir auslesen, indem ich sage, ich möchte den Wert haben, den
das Objekt mit dem Key hat.
Das heißt, man könnte im Prinzip auch sagen, was ist der Funktionswert
an einem bestimmten Parameterwert.
Und um das Ganze erstmal zu füllen, gibt es die Put-Operation als
Pendant dazu, wo ich diese Key-Value-Paare eingeben kann und dann auch
den Value zurückbekomme.
Und ich kann auch den Wert quasi beim Objekt Key entfernen und damit
lösche ich quasi wieder das, was ich zum Beispiel bei Put mit
reingegeben habe.
Sie sehen, der Zugriff klappt immer bei Remove und Get über den Key
und das ist gerade auch eben die Idee hier.
Den Schlüssel muss man kennen, um zu wissen, was ist dann der Wert
dahinter.
Und was der Wert ist, das sagt einem die Map.
Ich kann mir auch wieder natürlich abfragen, ob ein Schlüssel
überhaupt enthalten ist.
Ich kann auch nachschauen, das ist schon interessanter.
Ob aber auch ein bestimmter Wert, Value, enthalten ist.
Das ist meistens eine deutlich langsamere Operation, weil man
natürlich erstmal durchschauen muss, ob alles da ist.
Also alle einzeln durchgucken muss, während beim Key, nach dem kann
ich ja nachschlagen.
Size, Größe ist Empty, hatten wir eben auch.
Dann Put-All, damit kann man eine komplette andere Map einfügen.
Und zwar eine, die typmäßig, wenn dann spezialisiert.
Also auch den Key-Typ hat und den Value-Typ oder eben Spezialisierung
davon.
Ich kann es löschen, ich kann mir die Menge aller Schlüssel ausgeben
können und ich kann mir auch die Menge aller Werte ausgeben lassen.
Wir können uns das mal kurz an einem Beispiel anschauen.
Wir erzeugen uns eine neue Tree-Map hier.
Data-Introduce-No-Map-String-Interchange-Mountains, also wir wollen
Berge abspeichern.
Und dann kann ich mir eine Tree-Map, eine neue Instanz von holen und
das füge ich hier Paare ein.
Sie sehen, das ist mal der Baum und die Höhe über normal nulle Metern.
Der Mount Everest mit 8.848 Metern, der K2 als nächster mit 8.611, der
Lot C mit 8.516 und so weiter.
Also Sie können jetzt beliebig viele Berge, wenn Sie das wissen,
einfügen.
Wenn Sie jetzt schauen, können Sie erst mal abprüfen, ist dieser Berg
als Key eingefügt worden, Lot C, dann würde die Ausgabe True sein, der
Wahrheitswert, weil Contains liefert ein Bouches-Ergebnis.
Sie können aber jetzt auch den Wert von K2 herausgeben, das ist dann
das dazugehörende Value-Teil von dem Key-Value-Paar.
Es würde also hier diese Ausgabe geben, 8.611 und Sie könnten auch
sagen, Get auf einen Schlüssel, von dem wir eigentlich wissen, dass er
gar nicht drin ist, Zugspitze, und dann gibt es eine Null zurück.
Das haben wir nicht drin gespeichert.
Wir können K2 entfernen, wie gesagt, die Adressierung geht hier über
den Key und dann können wir nachfragen, wie viele Elemente sind jetzt
drin.
Es sind natürlich nur noch zwei drin, vorher waren drei drin, K2 ist
entfernt, sind zwei drin und mit Values kann man jetzt beide Werte
ausgeben und Sie sehen, die kommen hier dann als Array-Elemente auch
zurück.
Und das ist jetzt einmal die Höhe von Mount Everest und einmal vom Low
C.
Dann gibt es noch eine Klasse Collections, die enthält alle statischen
Methoden, die generell immer wichtig sind zum Umgang mit einer
Collection, wie wir das in der Schnittstelle auch gesehen haben.
Und zwar kann ich einmal prüfen, ob das Ganze wirklich disjunkt ist,
ob beide Mengen unterschiedlich sind, kein gemeinsames Element haben.
Dann kann man zählen, wie häufig sind Objekte in C, die identisch sind
zu O, also bei denen equals true gibt.
Reverse List kann man eine Liste umdrehen einfach, sie nennt sich
Liste, Reihenfolge spielt auf einmal eine Rolle, insofern kann man auf
einmal auch sinnvoll umdrehen, macht beim Set keinen Sinn, aber bei
einer Liste schon.
Replace All können Sie im Prinzip alle Werte mit Old Value durch New
Value ersetzen und es geht im Prinzip die gesamte Liste durch.
Dann können Sie das Ganze sortieren und das klappt aber natürlich nur
dann, wenn Sie einen Typ haben, der Comparable erweitert.
Sie erinnern sich, das ist diese allgemeine Schnittstelle, die man
braucht, um zu sagen, ob etwas größer oder kleiner ist, ist das
überhaupt vergleichbar.
Und wie gesagt, wenn ich jetzt Elemente hier drin habe, die
vergleichbar sind, das soll das heißen, dann kann ich das Ganze
sortieren und hier besortiert gemäß sogar eines noch mitgegebenen
Komparators eine Vergleichsfunktion dieses DLL.
Wenn ich jetzt sagen will, ich habe vielleicht nicht Dinge, wo
offensichtlich ist, wie die Ordnung ist, wie bei Zahlen, zum Beispiel
alleine schon, wenn Sie Zeichenketten haben, ist vielleicht nicht mehr
ganz so offensichtlich, wie man die jetzt sortieren sollte mit
Anfangsbuchstaben oder nach der Länge oder so, dann kann man hier eben
den Komparator mitgeben als Funktion und dann wird es sortiert
entsprechend.
So kommen wir noch zu einem anderen Paket, das heißt eben alles Java
.util.
Java.io ist die Möglichkeit, um Input-Output mit Java zu betreiben,
also sprich eine Ausgabe von Datenströmen und Dateien.
Die wichtigsten Klassen, also auch das Ganze ist sehr, sehr
umfangreich, das ist jetzt erstmal nur das Allerwichtigste, ist, dass
ich also eine Ausgabe machen kann, die Byte-basiert ist und Zeichen
-basiert.
Byte -basiert sind die Streams, Input- und Output-Stream, und man kann
das zum Beispiel auch auf einer Datei lesen, File-Input-Stream, File
-Output -Stream.
Die Character-Streams sind Sachen, die im Prinzip direkt ausgebbar
sind, druckbar für die Menschen und umgedreht auch wieder eingebbar
sind über die Tastatur.
Und da gibt es einen Input-Stream und einen Output-Stream-Reader und
analog auch das Ganze für den Datei, File-Reader und File-Writer.
Jetzt ist das Interessante, Sie können das nach dem Dekoratormuster,
keine Sorge, das bekommen Sie später nochmal dran, quasi Attribute
hinzufügen zu diesen Streams, zum Beispiel sagen, die sind gepuffert.
Das heißt also, Sie können im Prinzip hieraus Sachen zum Beispiel,
hier versteht man es am ehesten, reinschreiben und das wird erstmal
gepuffert, bevor es dann wirklich zum Beispiel auf einer Festplatte
landet oder weitergeschrieben wird.
Und ja, schauen wir uns mal Beispiele an, wie nutzt man das Ganze?
Wenn man jetzt sagt, ich möchte Zeichen von der Standard-Eingabe,
Standard -In, lesen.
Das ist die Frage, was ist Standard-In?
Der Linux-User weiß das.
Standard-In kann das Keyboard sein, aber es kann zum Beispiel auch
sein, wenn Programme in der Kommandozeile hintereinandergepiped
werden, das ist im Prinzip die Ausgabe von vorhergehenden Programmen.
Und wie gesagt, hier hat man erstmal eine Instanz von String s.
Es gibt den Input-Stream-Reader ISR, den erzeugt man sich mit new,
eine neue Instanz.
Character ist jetzt das Zeichen, was man hieraus liest.
Und solange das nicht minus eins ist, lese ich im Prinzip immer mehr
und mehr Zeichen hier durch.
Und in dem Fall addiere ich sie jetzt auf dieses s.
Also füge ich das zu dem s hinzu.
Oder ich schreibe Zeichen von einem String auf die Standard-Ausgabe.
Hier habe ich dafür dann, weil ich was ausgebe, einen Output-Stream
-Writer.
Den besorge ich mir auch in der Instanz hier und jetzt habe ich eine
Schleife.
Und solange eben mein String s ist, schreibe ich jetzt quasi das ite
-Element und i inkrementiere ich hier auf diesen Writer.
Wie gesagt, den Writer selber interessiert das i nicht.
Das i ist nur quasi zum Auslesen von meinem String.
Das ist immer das nächste Element.
Und am Ende sage ich noch flush, dass es auch tatsächlich geleert wird
und auch nach außen geschrieben wird.
Und auch hier wieder, wo das erscheint, kann die Konsole sein.
Also quasi der Bildschirm, von dem das gestartet wurde.
Oder aber natürlich auch ein Programm, das damit verknüpft würde in
der Kommandozeile.
Und das liest dann quasi als Standard eben das wieder.
Und das war das, was ich eben auf der Tonspur schon gesagt hatte.
Es gibt ein Entwurfsmuster, was es erlaubt, während im Prinzip ein
Programm läuft, die Eigenschaften von Objekten zu verändern, ohne dass
man aber jetzt Vererbung hat.
Vererbung ist immer ein statischer Mechanismus.
Das müssen Sie quasi zur Design-Time, also zur Zeit, wo Sie das
Programm schreiben, anwenden, um Funktionalität hinzufügen, an dem Sie
also erben und was hinzufügen, an einen neuen Methoden.
Beim Decorator ist es so, dass Sie quasi durch einen Konstruktoraufruf
ein neues Objekt bekommen, was erweiterte Möglichkeiten hat,
dekorierte Möglichkeiten.
Das werden in der Software-Technik 1 nächstes Jahr im Sommer noch
ausführlich kennen werden, dieses Muster.
Hier nur ganz kurz, wir haben also hier einen Output-Stream, sich
besorgt, File-Output-Stream.
Sie können jetzt beidweise lesen, schreiben in diese Datei.
Wenn Sie sagen, das Ganze soll jetzt gepuffert werden, dann besorgen
Sie sich einen New-Buffered-Output-Stream, geben dem als Parameter
diesen Output-Stream, den Sie schon haben, der auf die Datei File.txt
mit der verbunden ist.
Und jetzt bekommen Sie einen Buffered-Output-Stream, der auch auf File
.txt auf Ihrer Festplatte verknüpft ist, aber jetzt eben gepuffert
ist.
Und jetzt können Sie zum Beispiel sagen, ja, der soll sogar auch noch
ein Deflater-Output-Stream sein, das heißt aber doch das Ganze
komprimieren nach dem ZIP-Algorithmus.
Und dann können Sie auch wieder hier genauso das Dekorierer-Muster
anwenden.
Neben dem Konstruktor-Aufruf geben Sie das als Parameter mit, was
dekoriert werden soll und das ist dann das Ergebnis.
So weit wäre das jetzt erstmal der Einblick in die Java-Collections
und dann die ganze Java-API.
Wie gesagt, das gesamte Thema ist viel, viel länger, aber mein Tipp
ist einfach, lesen Sie das einfach bei Bedarf durch.
Wenn Sie in eine Situation kommen beim Programmieren, wo Sie merken,
okay, jetzt brauchen Sie was, dann können Sie sich da nachgucken.
Aber Sie wissen jetzt schon mal für die gängigen Sachen,
Dateieinausgabe, Standardeinausgabe, wo guckt man nach?
Und bei den Collections eben auch die klare Botschaft, hier gibt es
quasi schon alles vordefiniert.
Sie müssen halt nur noch überlegen, wann wähle ich was aus, aber
implementiert ist es schon alles.
Was jetzt folgt, im Wesentlichen ist noch Information zu einem guten
Programmierstil.
Wir hatten das ja schon manchmal on the fly erwähnt.
Deswegen ist das jetzt so ein bisschen am Ende von dem, wo wir über
Sprachkonstrukte oder vordefinierte Elemente von Java reden.
Weil Stil ist, wie gesagt, jetzt was, was eher die Ästhetik ist, wie
Sie schreiben, wie Sie gute Software schreiben, wie Sie gute Programme
schreiben.
Aber deswegen am Ende jetzt, bevor wir wieder zum Stil übergehen,
nochmal ein Hinweis auf ein Buch.
Und zwar ist das zunehmendermaßen eher für die Fortgeschrittenen
interessant.
Java Puzzlers.
Da gibt es auch ein Video zu, was in die Richtung geht.
Joshua Boch ist ein Name, den man sich ohnehin mal merken kann.
Er hat viele interessante Bücher über Java geschrieben, wo er Puzzlers
hier im Sinne von kleinen Rätseln, was macht ein Programm.
Und dann zeigt es einem, dass es wahrscheinlich nicht das tut, was man
erwartet hatte.
Aber das ist, wie gesagt, für diejenigen, die noch sagen, okay, ich
will noch ein bisschen mehr über Java erfahren, ich will ein bisschen
tiefer gehen, als das jetzt hier im Rahmen einer Vorlesung möglich
ist, noch so ein Literaturtipp.
Also aber keine Sorge, das ist jetzt nicht relevant für die
Abschlussaufgabe, dass Sie das kennen.
Das ist eher nur für die, die jetzt besonders interessiert sind.
Ich hatte Ihnen ja zum Laufe unseres Wintersemesters an einigen
Stellen gesagt, ja klar, kann man hinschreiben, ist aber kein guter
Stil.
Sie werden noch viel mehr als für mich genervt sein durch den
Praktomaten, der natürlich auch noch gewisse Stilprüfungen macht.
Und noch schlimmer als der Praktomat sind natürlich gewisse Tutoren,
die dann auch noch Dinge Ihnen anstreichen, wo Sie sagen, mein Gott,
das habe ich doch schon mein Leben lang anders programmiert.
Und jetzt auf einmal hier bilden diese Schnapsnasen sich ein, das zu
korrigieren.
Das ist halt der Punkt mit Stil oder auch jetzt Entwurfsprinzipien,
der Compiler schluckt das natürlich.
Es geht jetzt nicht darum, den Compiler oder das Typ-System glücklich
zu machen, sondern es geht darum, was ich Ihnen ganz von Anfang an
gesagt hatte, Achtung, mein Laserpointer ist neu aufgeladen.
Dass Sie immer die Software schreiben für den Nächsten, der es liest.
Sie schreiben Ihre Software nicht für den Compiler, Sie schreiben Ihre
Software nicht für die Java Virtual Machine.
Das sind Maschinen, die blind ja Ihre Software ausführen, egal in
welchem Stil Sie sind.
Aber der nächste Mensch, der Sie lesen muss und insbesondere stellen
Sie sich vor, Sie sind in einem Projekt, wo Sie auf einmal Millionen
Zeilen Code haben.
Sie müssen eine Funktionalität hinzufügen oder Sie müssen einen Bug
beheben, irgendwas anpassen.
Dann ist natürlich klar, dann haben Sie eine ziemliche Aufgabe.
Sie werden gar nicht in der Lage sein, diese Millionen Zeilen
zeilenweise durchgehen zu können, die Zeit fehlt Ihnen.
Sie werden auch nicht einfach sagen können, oh super, ich habe 10
Programmierer, der nimmt sich mal hier die ersten 100.000 vor und so
weiter und der Nächste die zweiten 100.000.
Auch damit wird man kein Gesamtbild bekommen, sondern man wird die
Software nur verstehen, wenn man letzten Endes, wenn sie gut
geschrieben, wenn sie gut strukturiert ist, eine vernünftige
Architektur hat.
Und nur dann haben Sie die Chance, diese Software zu nutzen im Sinne
von weiterentwickeln, anzupassen, zu pflegen.
Und es gibt zwei Vorlesungen, die wir hier in Karlsruhe anbieten.
Einmal komponentenbasierte Softwarearchitektur, die Ihnen beibringt,
wie programmiert man nicht nur, sondern wie macht man auch guten
Entwurf, gute Architekturen.
Und die andere Vorlesung ist Software-Evolution, wo es auch darum
geht, besonders wartbare Software zu schreiben.
Da könnte man sagen, das ist immer ein bisschen blöd, in Gummistiefeln
von der Software von anderen Leuten rumzulaufen, will ich gar nicht,
ich will neue Software schreiben.
Stimmt, klar, klingt attraktiver, aber wie gesagt, die Realität ist
halt so, der Wert eines Softwaresystems bestimmt sich im Wesentlichen
aus der Fähigkeit, dass es wartbar ist.
Ja, weil sonst die Software eigentlich schon verschwendet und man kann
sie gar nicht mehr nutzen, weil man muss halt Software, das ist quasi
Naturgesetz, immer wieder anpassen, damit sie überhaupt nutzbar
bleibt.
So, lange Rede, kurzer Sinn.
Das, was wir jetzt hier an Prinzipien im Folgenden haben, soll eben
genau diesem Ziel helfen, dass Menschen leichter in der Lage sind, die
Software zu verstehen, weil sie eben nach bestimmten Prinzipien
geschrieben ist.
Was wir jetzt nochmal zusammenfassen, sind einige Sachen, die wir
schon vorher on the fly behandelt haben, die zum Teil auch in den
Tutorien dran waren, aber jetzt hier noch zusammen sind.
Kapselung von Daten, Komposition über Vererbung, gegen Schnittstellen
programmieren, die sogenannten SOLID-Prinzipien, das ist so eine
Abkürzung, insbesondere dabei das O, das Open-Closed-Prinzip.
Dann werden wir nochmal ein bisschen allgemeiner über Schnittstellen
-Entwurf sprechen, ausgeweite Prinzipien dazu.
Und auch hier wieder, es gibt Material von Bob Tarff aus der
University of Maryland, zum Teil basiert in HIDI-Folien auch darauf,
zum gewissen Teil, aber hier gibt es auch noch weitere Informationen.
Das erste Prinzip ist Datenkapselung.
Minimieren Sie die Zugriffsmöglichkeiten auf Ihre Klassen und
Attribute.
Sie erinnern sich, ich hatte Ihnen gesagt, es ist immer besser, über
Getter und Setter Routinen zuzugreifen, als auf die Attribute direkt.
Versuchen Sie die Attribute, wenn möglich, private zu machen.
So, jetzt ist hier ein Beispiel hilfreich, um auch den Vorteil zu
sehen.
Also, was man sieht hier, ich habe als Attribut Speed, also die
Geschwindigkeit in Kilometer pro Stunde und sage jetzt Public Double
Speed.
Das soll ich jetzt also ersetzen durch Private Double Speed in meiner
Klassendefinition und dann noch zusätzlich Getter und Setter Routinen,
GetSpeed, SetSpeed mit der vollkommen naheliegenden Signatur und
Implementierung.
Jetzt werden Sie sagen, warum soll ich das machen?
Das ist ja erstmal Mehrarbeit.
Ich tippe mehr Code, um eigentlich genau dasselbe zu können wie
vorher.
Ja, halt, ganz so einfach ist es nicht.
Was ist jetzt der Vorteil, wenn ich auf Speed keinen direkten Zugriff
habe, sondern immer nur gezwungenermaßen über die Getter und Setter
gehe?
Sie können zum Beispiel jetzt Prüfungen vornehmen.
Sie können zum Beispiel sicherstellen, dass keine negativen
Geschwindigkeiten, weil das vielleicht im Anwendungsbeispiel keinen
Sinn macht, so was.
Sie können sicherstellen, dass sowas gilt.
Das ist ganz einfach.
Sie nehmen die SetSpeed Routine, da muss man durchgehen, die muss man
aufrufen, wenn ich den Wert von Speed verändern will und prüfe einfach
ab, ist das Ganze negativ.
Und wenn nicht, dann kann ich halt eine Exception werfen oder einfach
eine Fehlernachricht ausgeben oder was auch immer geeignetes machen.
Das heißt also, wenn Sie jetzt stattdessen direkten Zugriff gehabt
hätten, indem Sie gesagt hätten, ich nehme einfach eine Public-Speed
-Variante und greife auf die zu, dann kann natürlich jeder alles
Mögliche eingetragen, ohne dass Sie eine Chance haben, so eine
Invariante, so eine Bedingung hier durchzusetzen.
Also das ist schon mal einer der Vorteile.
Andere Sache ist, was ich auch schon in der Vorlesung erwähnt hatte,
Sie können durch diese Kapselung Entwurfsentscheidungen leichter
ändern.
Das ist ja auch die grundsätzliche Idee von Interfaces.
Ich kapsele eine technische Entwurfsentscheidung hinter einer
Schnittstelle.
Schnittstelle ist jetzt bei uns ganz banal Getter und Setter.
Und auf die Art und Weise stelle ich sicher, dass ich die
Entwurfsentscheidung hinten ändern kann, ohne dass der Rest vom Code
davon betroffen ist.
Und ich weiß sogar, dass er auch nicht betroffen ist.
Also angenommen, ich sage jetzt, ja, das soll zwar noch ein Double
sein, aber das soll jetzt lieber Meter pro Sekunde sein, weil das
vielleicht für einige Berechnungen der bequemere Wert ist, anstatt
kmh.
So, dann kann ich jetzt sagen, ich habe hier die Set Speed, New Speed,
das ist nach wie vor Kilometer pro Sekunde.
Und dann setze ich eine Konvertierungsroutine hier ein, die meinen
Speed -Wert darauf setzt.
Und wo ich jetzt kmh umwandle, tun Meter per Second.
Und diese Konvertierungsroutine habe ich auch selber hier schnell
implementiert, indem ich es mit diesem Faktor multipliziere hier.
So, und der Gag auch wieder hier ist, die Schnittstelle ist die
gleiche gewesen.
Ich brauche also keinen Code, der jetzt hier diese Klasse verwendet
hat, wo Speed definiert ist, drin anpacken, sondern ich habe das rein
lokal abgefackelt.
Und das ist immer der Traum in der Software-Evolution, dass ich eine
Änderung lokal machen kann, ohne den gesamten Rest vom Code verstehen
zu können oder auch Angst haben zu müssen, dass da was passiert.
Und Sie könnten noch sagen, wenn jemand die Geschwindigkeit ändert,
dann müssen bestimmte Seiteneffekte, also Dinge ausgeführt werden, die
sich woanders auswirken, noch an einer anderen Stelle, deswegen
Seiteneffekte.
Unser Beispiel Notify Observers, also dass zum Beispiel irgendwelche
Observer, zum Beispiel Anzeigeboxen am Bildschirm, mitbekommen, ah,
der Wert wurde geändert, ich muss jetzt einen neuen Wert anzeigen.
Oder sie speichern zum Beispiel diese Änderung der Geschwindigkeit in
irgendeinem Log-Datei.
Vieles ist denkbar, aber das ist der entscheidende Punkt hier.
Wenn Sie solche Seiteneffekte ausführen möchten, dann können Sie das
natürlich über Get-und-Set-Routinen forcieren, dass das immer
konsistent durchgeführt wird, wie eben zum Beispiel auch unsere
Bedingungen dieser In-Variante, dass die Geschwindigkeit nicht kleiner
Null ist.
Und das können Sie natürlich nicht sicherstellen, wenn natürlich ein
direkter Zugriff auf Speed möglich ist.
Oder wenn ein direkter Zugriff auf Speed möglich wäre, dann müsste
jeder, der von außen die Speed-Variable setzt, selber noch daran
denken, diese Seiteneffekte durchzuführen.
Und das ist natürlich die Quelle wieder von Inkonsistenzen, weil der
kann das natürlich vergessen.
Nächstes Prinzip.
Man soll Kompositionen über Vererbungen sehen.
Es ist halt so, als damals Ende der 80er die objektorientierten
Sprachen in der Allgemeinheit immer bekannter wurden, hat jeder
gedacht, oh cool, jetzt habe ich Vererbungen.
Vorher hat man viele Programmiersprachen gehabt, die keine Vererbungen
kannten.
Jetzt hatte man plötzlich diese Vererbungsoperationen.
Und dann dachten halt die Leute naiverweise, oh super, jetzt aber
vererben.
Also jetzt neue Potenzial der objektorientierten Sprachen ausnutzen.
Und ohne Witz, Anfang der 90er gab es also Klassenbibliotheken, die
Vererbungstiefe 26 aufweisen konnten.
Und Sie ahnen schon, die Wartbarkeit war jetzt nicht so richtig toll,
weil natürlich im Prinzip alles, was sich änderte irgendwann im Laufe
dieser 26 Klassen, natürlich immer eine Abhängigkeit hatte, wieder
nach unten.
Also war nicht so richtig toll.
Und dann hat man auch, wie das so ist, wenn man ein neues
Programmierkonstrukt hat, hat man das erst nach einer gewissen Zeit
gelernt und verstanden, wann macht das Sinn und wann macht das keinen
Sinn.
Und ich hatte Ihnen ja schon an der Stelle, wo ich Vererbung
eingeführt hatte, dieses Buch von Bertrand Meyer erwähnt, Object
-Oriented Software Constructions, da gibt es ein ganzes Kapitel
darüber, was Vererbung bedeuten kann.
Aber jetzt vielleicht mal so als Faustformel.
Vererbung heißt Spezialisierung.
Das heißt, Sie haben eine Klasse, die speziellere Entitäten hat als
eine Oberklasse.
Dann sollten Sie Vererbung nehmen.
Vererbung ist aber nicht Instanzierung.
Das heißt nämlich einfach, Sie haben einen New Operator und rufen den
auf und dann bekommen Sie eine Instanz.
Also wenn Sie verschiedene Objekte von einer Klasse brauchen, brauchen
Sie nichts vererben, dann nehmen Sie einfach permanent New.
Und es heißt insbesondere auch nicht, die haben was miteinander zu
tun.
Die haben was miteinander zu tun heißt nämlich Aggregation oder
Assoziation allgemeiner noch.
Und Komposition ist letzten Endes ein Spezialfall der Assoziation, der
Aggregation.
Nämlich das Zusammensetzen zu einem neuen Objekt, was eine erweiterte
Funktion hat, dadurch eben neben dem Zusammensetzen von bestehenden
Objekten.
Die neue Funktionalität hat man durch im Prinzip eine Delegation, also
den Aufruf, Methodenaufruf an andere Komponenten oder Objekte.
Manchmal in der Literatur wird auch Komposition und Aggregation
zusammengeworfen.
Das will ich nur erwähnen, damit Sie nicht verwirrt sind.
Aber der Unterschied ist in der Regel der, wenn Sie etwas
konstruieren, was von der Existenz seiner inneren Objekte abhängt,
dann heißt das Komposition.
Wenn Dinge zusammengesetzt werden, aber das eine auch ohne das andere
existieren kann, dann heißt das Aggregation.
Also ein klassisches Beispiel hier, ein Motor besteht eben aus
mehreren Zylindern.
Und es ist klar, wenn die Zylinder nicht da sind, ist auch der Motor
nicht da.
So einfach ist das.
Und deswegen ist das eine Komposition.
Aggregation wäre halt eine etwas losere Sache, Leuchtturm,
Leuchtturmwärter.
Ja gut, klar, kann beides alleine existieren.
Klar, funktioniert nicht beides.
Klar, Sie brauchen für funktionierenden Leuchtturm einen Leuchtturm
und einen Leuchtturmwärter.
Aber im Prinzip, selbst wenn der Leuchtturm quasi mal kaputt geht, ist
der Leuchtturmwärter immer noch am Leben, hoffentlich.
Und umgedreht, wenn der Leuchtturmwärter stirbt, fällt auch nicht
sofort der Leuchtturm ein.
Insofern wäre das eine losere Form der Kopplung, wie eben keine
Komposition ist.
So, wenn wir uns unser Beispiel anschauen hier.
Ich habe eine Table, eine Tabelle-Klasse und die nutzt intern eine
Table -Data-Klasse und fügt zum Beispiel noch eine grafische
Repräsentation hinzu.
Das wäre jetzt also eine Komposition oder für mich ist es auch
Aggregation, je nachdem, wie man das sieht, aber eben keine Vererbung.
Ja, was sind Vor- und Nachteile der Komposition?
Der Zugriff auf die Komponenten erfolgt immer ausschließlich über den
Interface.
Das heißt, Sie können auch eine Implementierung austauschen, ohne dass
Sie sich Gedanken machen müssen, ob das Ganze noch funktioniert.
Das Ganze ist dadurch Blackbox, also interne Details der Komponente
brauchen nicht sichtbar zu sein, wird auch nicht drauf abgehoben, also
sind auch nachher austauschbar.
Sie haben eine gute Datenkapselung, weniger Abhängigkeiten in der
Implementierung, das hatten wir oben auch schon gesagt.
Jede Klasse kann sich immer genau auf eine Aufgabe konzentrieren.
Wenn eben Sie eine Eigenschaft brauchen, wie eben unsere Tabelle, die
eine grafische Repräsentation hat, dann können Sie sagen, okay, das
setzt sich zusammen aus zwei einzelnen Klassen, die erstmal isoliert
bestehen.
Der eine macht eine grafische Repräsentation, der andere die
Datenhaltung.
Und die Komposition kann dynamisch zur Laufzeit erfolgen, durch
Referenzen auf die Komponenten.
Das ist auch so eine Sache, die ausgenutzt wird bei diesem Decorator
-Pattern, wo Sie auch sehen, dynamisch zur Laufzeit bei einem
Konstruktoraufruf gebe ich als Parameter das zu dekorierende Objekt
mit und das ist natürlich dann auch einkomponiert.
Nachteile sind, Sie haben natürlich mit mehr Objekten insgesamt zu
tun, steigt dadurch die Komplexität und Sie müssen natürlich die
Schnittstellen auch sauber definieren, damit sie eben möglichst auch
gut wiederverwendbar sind.
Das ist natürlich generell immer wahr, wenn Sie mit Schnittstellen zu
tun haben.
Demgegenüber bei der Vererbung ist es so, bei der Vererbung leiten Sie
von der Klasse ab, Sie können Funktionalität hinzufügen, das heißt
also die abgeleitete Klasse hat quasi eine Basisfunktionalität und
zusätzliche Methoden, aber Sie müssen im Prinzip auch dadurch die
Implementierung der Oberklasse kennen.
Also werden das nachher noch bei mir in der komponentenbasierten
Software -Architektur-Vorlesung kennenlernen.
Wenn Sie einfach überschreiben beim Erben, kann es halt sein, dass
Sie, wenn Sie den Code der Oberklasse nicht verstanden haben,
irgendwelche lustigen Seiteneffekte haben, die Sie eigentlich nicht
wollten.
Und deswegen ist es so, dass die Vererbung also auch Vor- und
Nachteile hat.
Die Implementierung der abgeleiteten Klasse ist einfacher, da das
meiste einfach vererbt wird, Sie müssen also Dinge nicht nochmal neu
implementieren und Sie modifizieren einfach die Basisklasse und
erweitern die.
Aber der Nachteil ist also, Sie haben keine Datenkapselung und es ist
eine Art Whitebox-Wiederverwendung.
Wie ich sagte, Sie müssen den Code der Oberklasse kennen, bevor Sie
davon erben.
Sie können nicht einfach wieder an der Stellstelle sagen, ist mir
egal, was dahinter steht.
Und Änderungen an abgeleiteten Klassen können auch wieder erforderlich
sein, wenn nämlich auch die Oberklasse verändert wird.
Das heißt also, Sie haben auch eine Abhängigkeit in diese Richtung.
Und Implementierungen, die Sie von der Oberklasse erben, können
natürlich nachher auch nicht mehr verändert werden.
Währendgegen Komposition, wie gesagt, können Sie zur Laufzeit
aufsetzen und auch wieder auflösen.
Schauen wir uns das mal am Beispiel an, ich glaube, dann wird es ein
bisschen leichter.
Stellen Sie sich vor, wir wollen eine neue Variante von Hashtag
schreiben, die aber mitprotokolliert, wie viele Elemente sind im
Hashtag drin.
Jetzt machen wir das einmal über Vererbung.
Wir sagen, okay, super, es gibt ja schon eine Klasse Hashtag, haben
wir ja gesehen, von Java Util vordefiniert.
Ich habe jetzt mein instrumentiertes Hashtag als neue Klasse, die
davon ableitet.
Also Instrumented Hashtag implementiere ich jetzt hier, indem ich
sage, das erbt von Hashtag.
Und jetzt habe ich meinen neuen Zähler, den muss ich auch als Attribut
definieren, erstmal auch auf 0 setzen.
Jetzt habe ich meine Konstruktoren hier.
Hier werden Sie sich vielleicht ein bisschen wundern, was sind das für
Faktoren?
Init, Cap und Load Factor.
Das sind, wenn Sie auch ganz normal bei Java ein Hashtag anlegen,
immer zwei Initialisierungsparameter.
Das eine sagt, wie viele Behälter sollen in dem Hashtag drin sein, in
dem nachher Werte rein können.
Das ist erstmal eine initiale Schätzung von Ihnen, das kann nachher
dynamisch erweitert werden.
Aber es soll einfach dem Java-System sagen, wie viel sollst du denn
mal von vornherein anlegen, weil ungefähr die wirst du wahrscheinlich
sowieso brauchen.
Das ist natürlich schneller dann, als wenn jedes Mal mühsam die
Hashtag erweitert und erweitert und erweitert werden muss, wenn mehr
und mehr Elemente dazukommen, sodass man sagt, von vornherein, das ist
schon mal was, was du initial anlegst.
Zu groß sollte der Wert natürlich auch nicht sein, weil sonst hat man
natürlich Speicherplatz verschwendet.
Und der Load Factor gibt quasi prozentual an, deswegen auch hier als
Float, wie viel Prozent erwartet man sind eigentlich ausgelastet
nachher von diesen Buckets, also von diesen Behältern in der Hashtag.
Es gibt irgendwie so eine klassische, fast Naturkonstante, 75 Prozent
hier, immer 0,75 gibt man klassischerweise immer an, weil das immer so
sagt, okay, also wenn dieser Wert überschritten wird, wenn mehr als 75
Prozent der Behälter in der Hashtag belegt sind, dann soll das System
automatisch wieder mal anfangen, neue Behälter zu besorgen.
Und auch hier wieder, wenn man weniger angibt hier, dann ist es im
Prinzip so, dass man Speicherplatz verschwendet.
Wenn man mehr angibt, wird der Zugriff nachher sehr langsam auf die
Hashtag.
So, aber das ist eigentlich jetzt, wie gesagt, gar nicht so wichtig.
Ich wollte Ihnen nur gerade erklären, was so diese Edit Cap und Load
Factor hier soll, aber an sich für unser Beispiel ist es gar nicht
wichtig.
Sie sehen auch, das wird beim Konstruktor hier einfach
entgegengenommen als Parameter und dann wird super zum Konstruktor der
Oberklasse, also vom Hashtag, einfach weitergereicht.
Also insofern auch, wenn man das jetzt nicht im Detail versteht, macht
das nichts.
Aber hier habe ich jetzt meine neue Add-Routine.
Die überschreibt das Add, was ich sowieso schon vom Hashtag habe.
Nämlich, was es macht ist, es zählt jetzt mit, aha, ein Element wird
hinzugefügt, dann rufe ich super.add auf und bei Add All, ja, also ich
nehme eine ganze Collection entgegen, sage ich natürlich, dass jetzt
um alle Elemente, die in dieser Collection sind, die ja alle
hinzugefügt werden, soll jetzt die Größe, also Add Count erhöht
werden.
Ja, also Plus, Gleich, Erhöhen, um diesen Wert 10 Punkt sei es.
Wie gesagt, um die Menge der Elemente, die in C sind, das soll
hinzuaddiert werden, weil die alle aufgenommen werden.
So, ich könnte mal sagen, es sieht ja alles gut aus.
Jetzt habe ich noch ein Getter für meine Add Count Variable.
Auch das ist normal, so sagt man, ist glücklich, ja, super.
Haken ist, ich habe jetzt natürlich nicht reingeguckt, wie Hashtag
implementiert ist, obwohl ich in der Vorlesung eben noch selber gesagt
habe, es ist eine Whitebox-Technik, Vererbung.
Jetzt können wir gleich sehen, wie wir auf die Nase fallen.
Und zwar habe ich hier meine Routine Main.
Ich besorge mir hier so ein instrumentierter Hashtag mit New und füge
ein diese Liste, die sich ergibt durch Erase as List, Hund, Katze,
Maus.
Das sind also die drei Elemente, die drin sind.
Und jetzt sage ich, jetzt gucken wir mal an, was Get Add Count ist.
Und Add Count müsste ja eigentlich drei sein, weil ich drei Elemente
hier eingefügt habe.
Hund, Katze, Maus.
Und das sollte ja gerade die Semantik sein.
Dummerweise ist aber das Ergebnis sechs und nicht drei.
Warum?
Ja, hätte ich mal besser in den Code der Oberklasse reingeguckt.
Denn die Routine Add All von Hashtag ruft intern die Add Methode auf.
Warum ist das schlimm?
Achtung, wir gehen nochmal zum Code zurück, dann sieht man es.
Ich rufe also hier das Add All auf.
Add All addiert zu Add Count die Größe dazu von der Collection.
Also drei habe ich hier drin stehen.
Dann rufe ich Super Add All auf.
Super Add All hätte ich mir aber besser angucken sollen, habe ich aber
nicht gemacht.
Ruft intern wieder Add auf.
Add habe ich hier, ja super, klasse.
Dann wird nämlich das aufgerufen, nicht das Add von der Oberklasse,
sondern das, was ich überschrieben habe.
So, und hier wird natürlich auch jedes Mal nochmal Add Count.
Also für alle drei Elemente wird einzeln auch nochmal der Zähler
erhöht.
Und damit komme ich natürlich auf sechs statt drei.
Hier sieht man also schon gleich, das ist ein wunderschönes Beispiel
darüber, Sie sollten den Code der Oberklasse bitte kennen, bevor Sie
etwas überschreiben.
Und das ist nur ein Beispiel.
Ein weiteres, wie gesagt, können Sie bei mir auch noch nachher im
Master mal hören.
Aber der entscheidende Punkt ist hier einfach der, man dachte, naja,
das wäre ja so elegant, das hinzuzufügen.
Ist es aber, wie man sieht, gar nicht.
So, und wie würde man das jetzt mit Komposition lösen?
Ja, man sagt, ich habe ein Instrumented Set, das implementiert ein
Set.
Intern nutzt es auch ein Set.
Und jetzt code ich halt die ganzen Sachen eben selber.
Also, ich mache hier auch wieder meine Add-Routine, meine Add-All
-Routine mit der Collection-Zelle, rufe es Add-All auf, hier bei dem
Set.
Jetzt weiß ich aber, der frickelt mir eben nicht mehr meine Add-Count
-Variable rum.
Warum?
Vielleicht zählt da ja was drin, aber ich kann trotzdem sicher sein,
weil Add-Count ist meine Variable hier.
Da hat Set überhaupt keinen Zugriff drauf.
Weil ich mich ja nicht erbe davon.
Also hat auch keiner Zugriff auf meine Variable.
Und das ist halt eine Sache, die zwar auf der einen Seite immer auf
den ersten Einblick so, hm, macht aber mehr Arbeit, jetzt muss ich
alles nochmal hinschreiben, um die Schnittstelle zu implementieren.
Auch die Sachen, die ich gar nicht verändern will, aber sie haben halt
die Kontrolle drüber.
Und das haben sie halt eben bei der Vererbung.
Bekommen sie zwar ein paar Implementierungen geschenkt durch die
Vererbung, aber dummerweise, wie gesagt, wenn sie nicht genau
verstanden haben, was sie tun, legen sie sich aufs Kreuz damit.
So, und jetzt müssen sie natürlich dümmerweise auch noch den Rest
implementieren.
Das ist aber relativ einfach.
Ja, also Clear, rufen sie S.Clear auf oder bei Contains rufen sie eben
auf, was bei normalen Contains, was da draus kommt.
Aber wie gesagt, die müssen das zwar noch alles hinschreiben, das
hätten sie bei Vererbung nicht hinschreiben müssen, weil das eben
automatisch dabei gewesen wäre.
Aber wie gesagt, die sehen, so rasend viel Arbeit ist es jetzt auch
nicht.
Und es ist ziemlich straightforward und es ist auch gut wartbarer
Code, weil natürlich jeder auch sofort versteht, was hier passiert.
So, und diese neue Klasse, die sie jetzt haben, die implementiert eben
auch die Set-Schnittstelle, kann eben auch immer in dem Kontext
eingesetzt werden, wo ein Set-Objekt gebraucht wird.
Und ist eben insgesamt damit sehr flexibel.
Und wenn sie mal nachher, sag ich mal, sagen, ich möchte das irgendwie
anders implementieren, können sie bei sich eben reingehen und sie
brauchen sich keine Gedanken machen über andere an der Stelle.
So, dann nächstes Prinzip.
Programmieren sie gegen Schnittstellen und nicht gegen eine
Implementierung?
Das ist im Prinzip dasselbe Prinzip wie eben nur umgedreht, jetzt eben
nicht aus der Sicht des Implementierenden, also von der Implementierer
der Schnittstelle, sondern von dem Nutzer.
Und generell auch wieder, was sind die Vor- und Nachteile von
Schnittstellen?
Also eigentlich hat es fast nur Vorteile, ja.
Also die Verwender einer Schnittstelle, die Clients, kennen genau die
Klasse des Objekts, kennen die genaue Klasse eben nicht, die die
Schnittstelle implementiert.
Die ist austauschbar dadurch.
Also klar, wir hatten das ja eben gesehen.
Wenn ich jetzt ein Set haben will, kann das dahinter ein Preset oder
ein Hashtag sein, interessiert mich nicht, ist trotzdem dieselbe
Schnittstelle, die realisiert wird.
Objekte können also leichter nachher auch ausgetauscht werden.
Die Objektverbindungen müssen nicht fest vertraten programmiert
werden.
Dadurch habe ich auch eine bessere Wiederverwendung von meinem Code.
Und wie ich schon gesagt hatte, die Entwurfsentscheidungen werden
gekapselt und können also dadurch auch nachher leichter angepasst
werden.
Einziger Nachteil ist, ja, ich habe die Schnittstelle dadurch noch als
eigenständige Entität mit rumfliegen in meinem Programm.
Also ein bisschen mehr Komplexität, als wenn ich sie eingespart hätte.
So, Beispiel.
Ich habe eine Schnittstelle-Interface für ein Fahrzeug, also ein
manövrierbares Vehikel.
Und dem kann man jetzt sagen, unser Beispiel, left, right, forward,
reverse, also rückwärts, set speed und get speed.
Ja, dass jetzt die Schnittstelle über dieses Fahrzeug angesprochen
werden kann.
Wie auch immer das dann nachher implementiert ist, und hier sehen Sie
das schon, ein Auto kann diese Schnittstelle implementieren, ein Boot
oder ein U-Boot.
Alle können sinnvoll diese Schnittstelle implementieren.
Und wenn ich jetzt das nutze, diese Schnittstelle, kann ich sagen,
also ich habe jetzt eine Reise, die ich vorgebe, und es ist vollkommen
egal, mit welchem Vehikel ich das mache.
Ich stelle eine Geschwindigkeit, also vorwärts, dann nach links und
setze dann die Geschwindigkeit auf null.
Und im Prinzip kann ich diese Sache generisch programmieren, ohne
festzulegen, ob das jetzt ein Auto, ein U-Boot ist oder sonst was.
Dadurch ist es leichter wiederverwendbar.
So, das nächste Prinzip ist ein bisschen leicht schwieriger, deswegen
verwende ich etwas mehr Zeit.
Das ist das sogenannte Open-Closed-Prinzip, was im Wesentlichen so
etwas sagt wie, wenn ich eine Softwarekomponente herstelle, sollte die
offen sein für die Erweiterung, aber geschlossen sein für Änderungen.
Geschlossen für Änderungen heißt, ich muss nicht unbedingt reingehen,
um etwas an Funktionalität hinzuzufügen, sondern ich kann das anders
machen, durch Erweitern, aber nicht durch Ändern von bestehendem Code.
Ein Aspekt davon ist, dass man versuchen sollte, Programmcode so zu
schreiben, dass man ihn eigentlich gar nicht mehr anpacken muss, weil
er hinreichend generisch ist.
Wir hatten das eben schon an dem Beispiel mit der Schnittstelle
gesehen.
Schauen wir uns das nochmal am Beispiel an.
Ich habe hier eine Funktion, die ein Array von Parts nimmt und den
Gesamtpreis berechnet.
Man könnte sich sowas vorstellen, so ein Aufsummieren, wenn ich z.B.
einen Lieferschein berechnen muss oder eine Rechnung.
Okay, jetzt sollen die Preise aufsummiert werden.
Am Anfang ist die Summe eben null und dann wird das ganze Array
durchinterviert und immer mit GetPrice von Parts das aufgerufen.
Was schon mal ganz gut ist, wenn Part hier ein Interface ist und nicht
eine Klasse, dann kann ich sogar ein Array von verschiedenen Parts
haben, hauptsache sie implementieren diese Schnittstelle und ich kann
GetPrice aufrufen.
Also das ist schon mal nicht so schlecht.
Aber was ist jetzt, wenn die Art, wie die Preise berechnet werden,
doch sehr unterschiedlich ist?
Und eine Sache, wie man es nicht machen sollte, ist jetzt dieses
Beispiel hier.
Und zwar, wenn ich jetzt abfrage, ist das Ganze z.B.
ein Motherboard, dann muss ich mit dem Faktor 1,2 das Ganze
faktorieren nochmal.
Oder ist das Ganze ein Speicherbaustein, dann bitte der Faktor 1,1 und
ansonsten gar keinen Faktor, also Faktor 1.
Und warum ist das jetzt schlecht?
Das ist aus vielerlei Hinsicht schlecht.
Erstens mal, ganz banal argumentiert, performancemäßig schlecht.
Instance Office ist ein sehr langsamer Operator, das sollte man
vermeiden.
Zweitens, ob ich die Hierarchie von Parts erweitere, sollte ich nicht
an dieser Stelle nochmal reflektieren müssen, sondern wenn ich jetzt
in diesen Baum von Parts was hinzufüge oder eine weitere Klasse habe,
die Parts implementiert, dann sollte das nur an dieser Stelle, wo ich
das definiere, relevant sein, aber nicht an irgendwelchen obskuren
anderen Stellen im Code.
Keine Ahnung, wie ich sonst auf diese Stelle hier überhaupt kommen
würde.
Und dann auch zum Beispiel hier nochmal ein weiteres Instance Office,
was weiß ich, Graphic Adapter oder sowas einfügen.
Also das ist erstmal von vornherein schon mal eine Gefahr, wenn die
Information, wenn sich jetzt hier was ändert, wie komme ich überhaupt
darauf, dass ich hier noch was hinzufügen muss?
Wenn jetzt irgendwo einer, wie gesagt, Grafikkarte nochmal als Klasse
einfügt, das weiß er erstmal nicht.
Das ist schon mal die Gefahr von Fehlern.
Das andere ist, es ist natürlich auch sau doof, mit Instance Office
nachzufragen, man könnte das ja zum Beispiel in die GetPrice
-Implementierung mit einbauen.
Wenn ich das nicht will, weil ich sage, GetPrice weiß vielleicht gar
nicht, also ein Bauteil weiß nicht, wie es speziell faktoriert werden
soll.
Das ist eine Sache, die kann der Programmierer von Motherboard oder
Memory gar nicht wissen.
Dann macht man das so, und zwar, dass man sagt, ich habe eine eigene
Routine SetPricePolicy und die definiere ich bei meinem Part mit dazu
und die wird dann entsprechend vorher gesetzt.
Und da kann ich dann eben den Preis, SetBasePrice, und durch die
PricePolicy kann ich dann zum Beispiel angeben, wie nochmal dieser
Preis manipuliert werden soll, bei GetPrice.
Dann kann ich eben hier diese PricePolicy noch den Faktor mit angeben
im Konstruktor und nachher dann eben GetPrice entsprechend anpassen,
dass dieser Faktor immer mit berücksichtigt werden soll.
Das ist eine Möglichkeit, wie ich jetzt mit diesem Open-Close-Prinzip
dafür gesorgt habe, dass Part an sich abgeschlossen ist.
Ich brauche in Part nicht mehr reingehen, wenn die Art, wie Preise
berechnet werden sollen, geändert werden soll.
Auch wenn ich einen neuen Grafikadapter einführe als Unterklasse von
Part, brauche ich da auch nicht mehr reingehen, weil der erbt es schon
richtig.
Ich muss halt nur dafür sorgen, dass ich die richtige Pricing-Policy
nutze.
Das, was wir bisher gehört haben, schon mal in dieses Solid-Prinzip
eingebastelt.
Solid ist so ein Merksatz.
S steht für Single Responsibility-Prinzip.
Also eine Klasse sollte immer nur eine spezielle Verantwortung haben.
Da sollte man sich überlegen, was möchte ich in dieser Klasse
implementieren.
Und das sollte halt eben nicht sein, die Anzeige, die Datenhaltung und
noch am besten die ganze Codierung über das Netzwerk und auch noch,
wie ich mit dem User darüber interagiere, sondern nein, immer nur eins
davon.
Open -Close-Prinzip haben wir gerade besprochen.
Dann das Liskov'sche Substitutionsprinzip, auch ganz wichtig, das
hatten wir auch beim Subtyping schon besprochen.
Und zwar geht es darum, dass eine Instanz der Unterklasse immer in den
Kontext der Oberklasse auch eingesetzt werden kann.
Also ich habe ein Objekt der Basisklasse und ich habe ein Objekt einer
abgeleiteten Klasse, einer Unterklasse und das sollte sich immer wie
ein Objekt auch der Basisklasse verhalten.
Dass es zusätzliche Informationen hat, okay, muss mich ja nicht
stören, aber es sollte zumindest mal das tun, was auch in der
Basisklasse passiert.
Dann das Interface-Segregation-Prinzip, darüber werden wir nachher
noch ein bisschen sprechen.
Klassen sollten nicht gezwungen sein, an einer Schnittstelle Sachen zu
implementieren, die eigentlich gar nicht notwendig sind.
Mit anderen Worten, lieber eine große Schnittstelle aufteilen in
kleinere spezifische Schnittstellen, die alle in ihrer Komplettheit
sinnvoll zu implementieren sind, anstatt eine Mega-Schnittstelle mit
ganz, ganz vielen Methoden zu implementieren.
Und jeder, der nachher diese Schnittstelle implementieren will, muss
halt unter Umständen Methoden implementieren, die gar keinen Sinn
machen für ihn.
So, und das Letzte besprechen wir jetzt hier in dieser Vorlesung
nicht, das Dependency-Inversion-Prinzip, was aber im Wesentlichen
besagt, versuche dich nicht an die benötigten Klassen, Objekte, die
man braucht, um was zu erbringen, fest dran zu koppeln, sondern
versuche im Prinzip, das auch variabel zu halten, wieder als Element
im Konstruktor.
Dass man sagt, auf die Art und Weise kann ich leichter einen Codeblock
in einem neuen Projekt wiederverwenden, wo ich vielleicht sage, ich
möchte andere Dinge da ankoppeln.
Aber wie gesagt, das ist nicht Teil der Vorlesung hier.
Was ich zum Schluss hier machen möchte, ist noch ein paar weitere
Prinzipien für den Schnittstellenentwurf Ihnen anzugeben.
Warum?
Im Prinzip das, was ich zu Motivation schon vorher gesagt habe.
Eine gute Schnittstelle erhöht einfach die Lesbarkeit von Programmen,
automatisch auch die Dokumentation und ist damit eben für den nächsten
Menschen, der die Software nach Ihnen lesen muss, ein echter Segen.
Also, gute Schnittstellen verbergen wirkliche Implementierungsdetails.
Man muss sich immer fragen, wovon abstrahiert eine Schnittstelle weg?
Und das sollte immer von Implementierungsdetails sein.
Und besitzen ein konsistentes Abstraktionsniveau und auch ein
angemessenes Abstraktionsniveau.
Jetzt sind das natürlich so sehr schwammige Begriffe.
Was heißt konsistent?
Was heißt angemessen?
Und das werde ich jetzt im Folgenden mit Ihnen besprechen.
Einmal natürlich die Frage, sollte man bei einer Schnittstelle die
interne Repräsentation einer möglichen späteren Implementierung
widerspiegeln?
Jetzt würden Sie sofort sagen, hoffentlich nein, natürlich nicht, sind
Sie verrückt.
Aber trotzdem könnte man manchmal in Versuchen kommen.
Das Beispiel soll einfach nur zeigen, ich habe jetzt eine Container
-Klasse, die Kundendaten speichert und jetzt könnte man sich
vorstellen, ich habe AdCustomer drin.
Und ich möchte jetzt zum Beispiel sagen, naja gut, es ist ja auch
bequemer, wenn ich da eine Liste von Kunden auf einen Schlag einfügen
kann.
Eben nicht nur einen einzelnen Kunden, sondern viele Kunden.
Und das ist dann eine Liste halt.
Und das ist doch schön, wenn ich so eine Routine habe im Vergleich
jetzt nur zu dieser ganz generischen, allgemeinen, so einen Kunden
hinzufügen.
Und nachher ist es ja eh quasi als Liste implementiert.
Nein, das sollten Sie natürlich wissen.
Das Exponieren der internen Repräsentation sollte man natürlich nicht
machen.
Zumal das auch jetzt vorschreibt, dass immer auch eine Liste
wiederverwendet werden kann.
Aber man möchte natürlich trotzdem in der Lage sein, mehr als einen
Kunden auf einen Schlag hinzuzufügen.
Das wäre vielleicht schon ganz schick.
Was man aber macht, ist, die allgemeinste Klasse zu nehmen.
Das ist nicht Liste, sondern Set.
Set, sagt man, so eine Menge von Kunden soll hinzugefügt werden.
Aber ich mache mit diesem abstrakten Set gar keine Annahme darüber,
wie das Ganze dann tatsächlich nachher implementiert ist.
Also deswegen Nutzen von möglichst generellen, möglichst abstrakten
Superklassen, Schnittstellen eben hier bei Customers.
Also Sie könnten sich auch vorstellen, jetzt so wie wir das gelernt
hatten mit Generics, Add Customers Collection von Customer.
Das wäre vollkommen okay.
Aber nicht sagen, eine Liste ist das schon.
Muss es ja gar nicht notwendigerweise sein.
Dann ein weiteres Prinzip.
Trennung von Befehl und Anfrage.
Das hatten wir auch gehabt schon mal von Bert und Meier.
Command Query, Separation.
Sie erinnern sich.
Entweder habe ich eine Methode, die einen Zustand zurückliefert.
Und das ist eine Anfrage und die arbeitet eigentlich wie eine
Funktion.
Ich kann diese immer wieder neu aufrufen und die liefert mir immer
denselben Wert.
Und sie hat eben keine Seiteneffekte.
Also sie sorgt nicht dafür, dass der Zustand von dem Objekt sich
ändert.
Oder ich habe einen Befehl, der den Objektzustand ändert.
Und das ist klar, wenn ich den mehrfach hintereinander aufrufe, kann
das natürlich tatsächlich den Objektzustand auch ändern.
Seiden das idempotent.
Aber das ist aber eine Ausnahme.
Wir hatten das mal diskutiert.
Auch Sie erinnern sich an unsere Vorlesung über die Iteratoren.
In Java ist die Welt so, ich habe das Has Next und das Next.
Has Next ist ein Befehl, weil es fragt das nächste Element ab und
schaltet weiter.
Und Next liefert mir jetzt auch was zurück und schaltet weiter.
Und das ist im Prinzip schlecht, weil es ist hier durchbrochen.
Entschuldigung, das müsste eigentlich Anfrage heißen.
Befehl ist falsch, hier streichen sie es durch.
Das ist eine Anfrage hier.
Nur das ist Befehl oder Anfrage?
Das ist eine Anfrage.
Und bei Move Next in C-Sharp hat das genau andersherum.
Mit Move Next schaltet man eins weiter und mit I Current fragt man ab.
Current ist prima, es ist nur eine Anfrage, aber Move Next ist wieder
beides.
Es ist weiterschalten und ein Befehl.
Und auch hier wieder Command Query Separation durchbrochen.
Wir hatten uns aber auch überlegt, warum ist das tatsächlich so
gemacht?
Naja, die ideale Sprache wäre natürlich so, dass ich eine
IsCurrentValid Abfrageoperation habe, dann eine I Current, auch nur
eine reine Abfrage, kein Weiterschalten und dann mit Move Next.
Das wäre Command Query Separation perfekt durchgezogen, alles ganz
sauber getrennt.
Dummerweise aber habe ich jetzt drei Befehle pro Schleifeniteration,
anstatt nur zwei aufrufen.
Und das war der Grund, warum man gesagt hat, nee, das ist ein bisschen
mehr Schreibarbeit, Performance overhead, mehr Aufrufe.
Also das ist der Grund, warum man das hier durchbrochen hat.
Aber wie das bei den meisten Entwurfsprinzipien ist, sie können sie
natürlich durchsprechen, aber sie sollten wenigstens wissen, warum.
Sie sollten ein gutes Argument haben.
So, dann hatten wir eben gesagt, schlechte Abstraktionen.
Was ist jetzt eine schlechte Abstraktion?
Angenommen, ich habe hier ein Programm, da gibt es einen Befehl,
Initialize Command Stack, Push Command, Pop Command, dann Shutdown
Command Stack.
Initialize Report Formatting, Format Report, Print Report, Initialize
Global Data, Shutdown Global Data.
Warum ist das eine schlechte Abstraktion?
Also zum einen, weil hier offenbar von drei verschiedenen Sachen
gesprochen wird in derselben Schnittstelle.
Wer soll denn das sinnvoll implementieren?
Also entweder habe ich sowas wie ein Command Stack, dann habe ich
Initialisierung, Shutdown, Push und Pop.
Oder ich rede über irgendwelche Reports, die ich gebe, auch schön,
dann habe ich aber die drei Routinen.
Oder ich habe globale Daten und die möchte ich vielleicht auch mal
initialisieren und auch wieder loswerden.
Aber bitte nicht alles drei in derselben Schnittstelle.
Also hier sind mindestens drei Schnittstellen.
Deswegen ist das hier eine schlechte Abstraktion.
Also das wäre halt schon mal besser.
So ein bisschen das Interface Segregation Prinzip.
Und generell gilt, man sollte eben Schnittstellen und Klassen
aufteilen, wenn einige der Methoden nur mit der Hälfte, letzten Endes
der Attribute arbeiten oder ein Teil der Attribute und die anderen
Methoden mit dem anderen Teil.
Spätestens dann sollte man überlegen, offenbar hängt das doch nicht so
ganz kohärent zusammen, sollte man unter Umständen lieber entkoppeln.
Jetzt ist es klar, Sie sollten nicht beliebig feinkranulare Klassen
machen.
Auch da gibt es natürlich irgendwo eine Grenze, die nicht mehr
sinnvoll ist.
Aber im Prinzip sollten Sie sich immer überlegen, kann es sein, dass
mehrere verschiedene Evolutionsdrücke, Zwänge dazu führen, dass diese
Klasse angepackt wird.
Und wenn es mehr als einer Evolutionsdruck ist, dann ist es ein gutes
Zeichen, dass Sie das Ganze vielleicht aufteilen sollten.
Weil es ist immer ideal, wenn möglichst wenig Evolutionsdruck auf
einer Klasse liegt.
Evolutionsdruck ist zum Beispiel, die Ausführungsumgebung ändert sich.
Neue Version von JVM oder ich will das Ganze portieren von Java nach
.NET oder umgedreht.
Oder ein anderer Evolutionsdruck ist halt, Teile der Funktionalität
ändern sich.
Ich will jetzt auf einmal statt in Euro in Drachen faktorieren oder
sowas.
Was immer man sich in solchen Szenarien vorstellen kann.
Wie Sie sehen, das sind alles Evolutionsdrücke und wenn man merkt, auf
einer Klasse lasten extrem viele Evolutionsdrücke, dann ist es ein
gutes Zeichen, dass man das mal besser aufteilt.
Weil es natürlich immer gut ist, wenn ich Dinge unterhalbhängig
voneinander anpacken kann.
Das heißt also, hier diese Schnittstelle, die wir hier eben hatten, da
baut man dann eben zwei raus.
Auch schlecht, ich rede zwar über dieselbe Sache, zum Beispiel hier
Employees.
Ich habe eine Umfrage über Employee-Angestellten-Daten.
Und da habe ich jetzt Add-Employee, Remove-Employee.
Und dann habe ich aber nachher sowas wie Next-Item-in-List, First
-Item, Last-Item.
Hier an dieser Schnittstelle riecht man, aha, der hat das als
verknüpfte Liste im Hintergrund implementiert und auch die Namen so
gewählt.
Das sind aber zwei verschiedene Abstraktionsniveaus.
Das eine redet hier über Employees, sagt mit Add und Remove eigentlich
nichts, wie das Ganze implementiert ist.
Und hier unten auf einmal iteriere ich durch.
Ja klar, iterieren ist eigentlich gut, das natürlich muss ich
irgendwie auch zugreifen können.
Aber bitte nicht mit einem Listen-Abstraktionsniveau, sondern von mir
aus so, wenn das sinnvoll ist bei der Implementierung.
Und wie das Ganze intern implementiert ist als Liste, das muss ich
halt nicht nach außen geben.
Das will ich vielleicht auch mal in Zukunft ändern.
So, dann sind wir noch bei so einem Thema, was extrem wichtig ist,
aber eigentlich ziemlich banal.
Wie nennen Sie Ihre Methoden?
Können Sie sagen, ist mir doch egal, Hauptsache der Compiler
akzeptiert das.
Ja, genau, das ist aber eben das, was nicht besonders weiterhilft.
Sie sollten also nicht eine Routine nennen, Do Computations.
Das ist so allgemein, da können Sie quasi alles, was Sie in Ihrem
Informatikstudium lernen, erwarten, dass das dahinter auch ist.
Sondern wenn Sie stattdessen einen sprechenden Namen haben wie
Calculate Invoice Sum, also berechne Rechnungssumme.
Dann weiß man okay, aha, es geht hier scheinbar um Rechnungslesungen,
es muss irgendwas addiert werden.
Do Computations, super.
Dann wird da vielleicht eine Fibonacci-Zahl berechnet oder das alte
Problem versucht zu lösen, keine Ahnung.
Andere Sache, die auch immer so ein Übel ist, Ziffern zu verwenden in
Bezeichnernamen.
Das ist natürlich immer schnell, wenn man sich leicht einen neuen
Bezeichnernamen gebastelt hat.
Hochzählen kann man auch in schlimmeren Zuständen.
Aber der Witz ist einfach der, das hat ja keine Semantik.
Output User 1, Output User 2, ja wodurch unterscheiden sie sich?
Was ist eigentlich der Unterschied?
Also das 1 und 2 habe ich halt jetzt mal hingemacht, damit der
Compiler glücklich ist und Zwei-Namen hat.
Aber was das Semantisch der Unterschied ist, ist überhaupt nicht klar.
Und was übrigens auch man nicht unterschätzen darf, insbesondere wenn
man nächtelang debuggt hat dafür, unentdeckt bleibende Bugs durch
Tippfehler.
Die vertippen sich mal schnell, haben aber trotzdem wieder einen
gültigen Namen erwischt und 1 und 2 liegen auch noch dummerweise auf
der Tastatur direkt nebeneinander.
Und der Compiler ist glücklich, Typ-System ist glücklich, nur
natürlich wird das falsche Ergebnis rauskommen.
Das ist extrem mühsam zu finden, sowas.
Und das gilt jetzt natürlich nicht nur bei Ziffern, das gilt
grundsätzlich.
Sie sollten, hatte ich Ihnen ja auch gesagt, X-Post und Y-Post, auch
immer eine schlechte Sache.
X und Y leben auch direkt nebeneinander.
Aber wie gesagt, ganz banale Regel, Sie sollten immer zu schauen, dass
Sie etwas mehr Distanz in den Namen haben und bitte Semantik-Namen,
nicht sowas, das ist semantikfrei.
Und dann gibt es auch wieder für den nächsten Leser, die nächste
Leserin gedacht, einfach mal Methodennamen, die mittlerweile Common
Sense sind in der Programmierung.
Und die sind im Englischen ziemlich fest, im Deutschen ist das alles
noch nicht so klar, deswegen auch der Tipp, lieber den Code auch auf
Englisch schreiben.
Diejenigen Leute, die nachher den Code lesen, das ist eine hohe
Chance, dass die auch Englisch können.
Es gibt wenig deutsche Informatiker, die kein Englisch können.
Also, da gibt es auch immer so Wortpaare und wenn ich eine Add-Routine
habe und keine Remove-Methode finde, dann wundere ich mich schon.
Und natürlich können Sie sich sagen, ich könnte doch die Remove
-Methode auch anders nennen.
Ja klar, der Compiler versteht das, nur wie gesagt, denken Sie an den
nächsten Leser.
Genau, wenn ich was habe, Create erwarte ich auch, dass es irgendwann
sowas wie Destroy gibt.
Wenn ich First habe, habe ich Last, wenn ich Get habe, habe ich auch
ein Put oder ein Set.
Wenn ich eine Increment-Routine habe, habe ich auch ein Decrement.
Wenn ich ein Insert habe, dann habe ich auch irgendwo ein Delete.
Und genau da weiß man auch, das sind Paare und das gehört zusammen.
Nicht, weil es der Compiler erwartet, aber weil es der Mensch so
versteht.
Dasselbe geht weiter, Lock, Unlock, Min, Max, Next, Previous oder
Prev.
Old, New, Open, Close, Show, Hide, Source, Target, Start, Stop, Up,
Down.
Ganz banale Regeln, nichts geistig Anspruchsvolles, aber extrem
wichtig.
Übrigens, ein Haufen von diesen guten Programmier-Tipps sind in dem
Buch von Steve McConnell, CodeComplete.
Bei Microsoft Press erschienen, trotzdem guter Code.
Genau, also hier so ein Beispiel.
Wenn Sie eine Light-On-Routine implementieren, dann sollte auch
irgendwo ein Light-Off existieren.
Einfach, weil man das schon so erwartet.
Wenn Sie ein Add-Item haben, dann sollten Sie ein Remove-Item haben.
Und jetzt bitte keine Inkonsistenzen, nicht so ein Add und dann ein
Remove -Item.
Also entweder Add-Item, Remove oder Add und Remove, aber nicht so
mischen.
Weil dann fragt sich jeder, habe ich hier etwas übersehen?
Gibt es einen Unterschied zwischen Add?
Gibt es irgendwo vielleicht doch noch ein Add-Item?
Die Verwirrung ist schon von vornherein da.
Und die Leute sind gezwungen, haufenweise im Code rumzulesen, um zu
merken, nee, das ist einfach nur eine Inkonsistenz in der Benennung.
Kann man sich vermeiden, den Stress oder eben ihren Nachfolger.
Gut, dann wäre ich jetzt hiermit auch schon am Ende.
Nächste Vorlesung reden wir nochmal über ein paar technische Sachen
beim Ablauf von einem Programm.
Und dann war es das auch schon für dieses Wintersemester gewesen.
Bis zum nächsten Mal.