Wir fangen trotzdem wieder, wie immer, mit einem Puzzler an.
Der ist ziemlich einfach und ziemlich kurz.
Also hier haben wir nur eine 5-zeilige Main-Methode.
Und da drin wird ein Integer angelegt, der fängt bei 0 an.
Und dann gibt es eine kleine Vorschleife, die läuft auch von 0 los.
Bis zur Grenze 100, also seiner 100.
Da drin wird immer J, das, was wir hier oben mit 0 angefangen haben,
erhöht.
J++ ist ja ein einfaches Inkrement.
Und die Frage ist jetzt, was kommt raus, wenn man an der Stelle hier
angekommen ist?
Schauen Sie nochmal genau hin.
Glaubt irgendjemand, dass da 100 rauskommt?
Sie dürfen Summen nicht melden, aber ein paar Hände habe ich gesehen.
Glaubt irgendjemand, es kommt 99 raus?
Weil immerhin steht hier ja kleiner 100.
Irgendjemand für 99?
Der Rest ist dann für irgendwas anderes oder für einen Absturz?
Ist irgendjemand für einen Absturz oder ein Compiler-Problem?
Das wäre jetzt die Chance?
Ein selbstbewusster Summer?
Und glaubt irgendjemand, es kommt vielleicht 1 raus oder 0 oder
irgendwas ganz anderes und möchte sich hier mal outen?
Da ist also jemand für was ganz anderes.
Aber die große Masse der ganz anderen sind nicht so selbstbewusst wie
der eine für den Absturz.
Das Quantitativ ist ein bisschen schwierig.
Aber das Selbstbewusstsein allein, das reicht heute nicht.
Also das Programm übersetzt und das führt auch aus, ohne Fehler.
Oh Wunder, weder 100 noch 99 kommen raus, sondern eben was anderes.
Möchte irgendjemand dieses andere nennen?
Traut sich jemand?
Also ihr Kommilitone glaubt, da kommt eine 0 raus?
Warum?
Also ihr Kommilitone sagt, da wird jetzt zwar J erhöht, aber das wird
ja vorm Erhöhen zugewiesen.
Und was dann erhöht wird, das war ja das alte J und nicht das neue.
Und das neue, das ist ja immer noch 0.
Oder so ähnlich.
Das ist nicht ganz falsch.
Aber auch nicht ganz richtig.
Weil es gibt hier tatsächlich nur ein J.
Aber es kommt trotzdem 0 raus.
Das liegt daran, dass dieser Ausdruck hier zwar J erhöht, aber das
unerhöhte J zurückgibt.
Also es gibt keine 2, sondern nur ein J.
Aber der Wert, der hier zugewiesen wird mit dem Istgleich, der ist
tatsächlich immer 0.
Egal wie oft man das macht.
Dementsprechend kommt hier die 0 raus.
Wenn Sie sich das vorstellen, wie das intern funktioniert.
Man kriegt da eine temporäre Variable, die hat den alten Wert von J.
Dann wird J erhöht.
Dann kommt erst die Zuweisung.
Und dieser Ausdruck, der gibt halt den alten Wert zurück.
Dementsprechend ändert sich hier leider gar nichts.
Bleibt J ziemlich klein.
Beheben kann man das einfach, indem man die Zuweisung da weg macht.
Also schon der ganze Trick.
Und wenn Sie sowas in Ihrem Programm haben, dann wird es halt
irgendwie unübersichtlich.
Also wenn Sie eine Variable innerhalb von einer Anweisung mehrfach
verändern, das ist irgendwie ungeschickt.
Weil es sieht zumindest anders aus, als es meistens funktioniert.
Das ist aber auch schon die ganze Lehre, die wir aus dem Puzzler
ziehen können.
Mir fällt noch eine Lehre ein.
Also ich würde zum Beispiel hier mehrere rausziehen.
Es gibt eben Operatoren, die liefern als Ergebnis irgendwelche Dinge,
die wir vorher hatten.
Also wie zum Beispiel hier, das liefert da halt den alten Wert zurück
und nicht den neuen Wert.
Das ist die Fehlannahme hier an der Stelle.
Und das gilt natürlich unter Umständen auch für andere Operatoren.
Wir haben letztes Mal aufgehört mit der linguistischen Analyse.
Irgendwo recht weit hinten.
Und ich hatte Ihnen eine lange Geschichte über thematische Rollen
erzählt.
Zur Erinnerung, das waren solche Dinge, die man an Wörter in Sätzen
dranschreiben kann, damit man weiß, was diese Wörter in diesem Satz so
alles tun.
Also da geht es nicht darum, ist Peter eine Person, sondern was macht
Peter in diesem Satz.
Und in diesem Satz gibt er irgendwas, nämlich die Uhr, das Habitum und
zwar an den Frank.
Daraus können wir UML-Klassendiagramme bauen.
Das haben Sie letztes Mal auch schon gesehen.
Und was ich Ihnen dann noch in den letzten zwei oder anderthalb
Minuten gezeigt habe, war, das funktioniert nicht nur mit Blödsinn
-Sätzen, sondern auch mit richtigen Spezifikationen.
Und was ich Ihnen mitgebracht hatte und auch jetzt wieder dabei habe,
ist ein Auszug aus der Whois-Protokollspezifikation von der IETF.
Und da kann man auch wunderbar viele solche thematischen Rollen
dranschreiben.
Wenn Sie diesen Text mal lesen, steht da am Anfang, dass dieser Whois
-Server wohl auf irgendwelche Requests hört von Whois-Clients.
Das heißt, da haben wir schon mal, wenn wir da naiv drauf gucken,
mehrere Klassen, die wir hinterher haben wollen.
Nämlich ein Whois-Server, ein Whois-Client und wahrscheinlich
Requests.
Wahrscheinlich wollen wir auch diesen TCP-Port 443 irgendwo sehen,
wenn wir da ein Klassendiagramm draus machen.
Und jetzt ist die Frage, wie sieht denn das aus?
Was steckt da für eine Semantik drin?
Wenn wir die thematischen Rollen nehmen, die wir uns letztes Mal
angeschaut haben, dann sieht das Ganze so aus.
Da sind jetzt also recht viele Rollen dran.
Und wenn wir gerade nochmal den Anfang anschauen, da gibt es jetzt den
Whois -Server.
Der hat gleich mehrere Rollen, aber unter anderem auch dieses
Rezipient.
Und der Whois-Client, der ist Donor.
Das heißt, hier steckt drin, der Whois-Client oder die Whois-Client,
die geben irgendwas an den Server.
Wenn wir jetzt noch weiter gucken, irgendwo muss noch ein Habitum drin
stecken, also irgendwas, was gegeben wird.
Das sind hier gerade die Requests.
Das heißt, wenn man sich ein bisschen Mühe gibt und diese thematischen
Rollen da dran schreibt, dann sieht es hinterher so aus, wie hier auf
der Folie.
Das Hübsche ist, wenn man die thematischen Rollen so hinschreibt, dann
muss man nicht hingehen und nochmal durch die SVT-Folien blättern und
nachschauen, was für UML-Muster kommen denn da raus und wie male ich
das dann und dann muss man das großartig zusammenstecken.
Sondern diese Rollen, die sind so gewählt, dass die Abbildung ins UML
so deterministisch ist, dass man das sogar automatisieren kann.
Das heißt, wenn Sie einen Text hernehmen, der so annotiert ist und Sie
die Semantik richtig getroffen haben, dann gibt es ein Werkzeug, da
kippen Sie den Text einfach rein und dann fällt ein Klassendiagramm
raus.
Wenn man die ganze Whois-Spezifikation hernimmt und annotiert, dann
fällt dieses Diagramm raus.
Das heißt, wir haben hier wieder den Whois-Server und wir haben die
Requests, die wir vorhin im ersten Satz hatten und wir haben auch hier
ein Whois-Client.
Wir sehen hier auch diese dreistellige Beziehung zwischen den dreien.
Ich muss gerade mal fragen, so wie in der Waschmittelwerbung, wird es
jetzt echt noch weißer?
Also wenn ich vorne in dem Text so Dinge wie das Triple Donor
Recipient Habitum richtig annotiere, dann macht es plopp und so ein
tolles Diagramm fällt raus?
Automatisch?
Automatisch.
Dann ist ja das, was Du gerade vorträgst unter Umständen gar nicht nur
Unfug.
Nicht nur Unfug.
Das kann man ja benutzen.
Das kann man in der Tat benutzen, ja.
Das hat einen kleinen Nachteil.
Wenn Sie eine Spezifikation hernehmen, die nicht eine DIN A4-Seite
hat, sondern sagen wir mal ein Ordner voll DIN A4-Seiten und Sie
kippen das alles gesammelt in das Werkzeug rein, dann kriegen Sie ein
Klassendiagramm raus und da ist alles drin.
Das hat dann nicht irgendwie 14 Klassen wie hier, sondern 423.
Die Beziehungen dazwischen, sofern Sie es richtig annotiert haben,
sind alle richtig.
Die stecken auch alle in dem UML-Diagramm drin.
Aber da fehlt dann gerade diese Übersichtlichkeit, die Sie, wenn Sie
modular und mit schönen Paketen das vorher von Hand gemacht haben, die
geht da leider verloren.
Das heißt, Sie müssen sich dann überlegen, welche Teile von der
Spezifikation gehören denn zusammen und welche geben ein schönes
abgeschlossenes Teilsystem und dann kriegen Sie so ein hübsches
Diagramm raus.
Und das kriegen Sie natürlich nicht als PowerPoint-Folie oder so,
sondern das kommt da als großes XML-Geschwurbel raus und das kann man
dann in normalen UML-Werkzeugen einfach öffnen.
Sie kriegen also mit einer Spezifikation, die Sie durchgearbeitet
haben mit der linguistischen Analyse, ein ziemlich einfach schickes
Diagramm.
Das Tolle ist, die Interpretation, was da für ein Diagramm rauskommt,
wie es zusammengesteckt ist, das haben Sie im Text dokumentiert.
Das heißt, in diesem Textdokument steht hinterher drin, wie Sie den
Text verstanden haben.
Dann können Sie zum Kunden laufen und sagen, also ich glaube ja, das
soll so aussehen.
Und zwar, weil in Deiner Spezifikation steht, der WhoisServer, der
hört auf irgendwas, was die Clients ihm schicken.
Da steht es und deswegen haben wir hier dieses Triple, um die
Beziehung hier darzustellen.
Wenn der Kunde sagt, nein, das stimmt nicht, soll er Ihnen erklären,
welchen Satz Sie falsch verstanden haben.
Wir haben also da eine Dokumentation drin.
Sie kriegen nicht ein hübsches Diagramm und nebendran steht der
Analyst, der es gebaut hat.
Der muss dann erklären, warum er das so getan hat, sondern Sie haben
das wirklich nachvollziehbar direkt am Spezifikationstext.
Diese Computerlinguistik oder diese linguistische Analyse, das ist
etwas, was wir am Lehrstuhl immer mehr machen.
Dementsprechend gibt es auch eine Fortsetzungsveranstaltung dazu.
Das heißt, diese paar Folien, das ist nur die Spitze vom Eisberg, gibt
eine ganze Vorlesung dazu, die Sie im Master besuchen können, wenn Sie
Lust haben.
Da gibt es noch einen Stall voll weiterer Werkzeuge, wo nicht nur UML
-Diagramme rausfallen, sondern wo Ihnen zum Beispiel auch aufgezeigt
wird, wo eine Spezifikation schlecht ist, wo eine Spezifikation
mehrdeutig ist oder andere Probleme hat, die auf die Sprache
zurückzuführen sind.
Wir hatten letztes Mal das Beispiel mit dem Tom Saw the Plane Flying,
wo Sie nicht wissen, wer von beiden fliegt jetzt.
Genau solche Probleme kann man automatisiert finden.
Dann haben Sie natürlich ein schickes Werkzeug, da können Sie einfach
den Kunden hinsetzen und der muss dann die Fragen beantworten.
Ja, wer fliegt denn hier eigentlich?
Dann sagt Ihnen der Kunde, ja der Tom.
Dann wissen Sie hinterher, was Sie bauen müssen.
Wenn Sie herausfinden wollen, wie man sowas programmiert, dann kommen
Sie im Master nochmal bei uns vorbei.
Damit ist das Kapitel 2 vorbei.
Das heißt, die ganze Analysephase ist jetzt durch und als nächstes
kommt die Entwurfsphase.
Das ist das, was Sie wahrscheinlich sonst als Design kennen.
Bei uns heißt es Entwurf und es gliedert sich in fünf Unterpunkte, wo
wir heute mal anfangen.
Das ist so eine Art Einführung und die Erklärung, warum es sich lohnt,
modular zu entwerfen.
Also nicht ein großes System zu bauen, wo alles mit allem irgendwie
vernetzt ist und jeder von jedem wissen muss, was der andere macht und
wann und wie und warum.
Sondern schöne kleine isolierte Teile bauen.
Und dann werden wir uns anschauen, wie man das objektorientierten
Entwurf tatsächlich macht.
Der modulare Entwurf, der ist ein bisschen älter.
Damals gab es noch keine Objektorientierung.
Aber die ganzen Konzepte, die darin erklärt werden und die
Begründungen, die gelten im objektorientierten Entwurf auch.
Und am Ende von diesem Kapitel werden wir uns damit auseinandersetzen,
wie man dann tatsächlich die Software strukturiert.
Das heißt, wir werden uns Architekturmuster anschauen, Entwurfsmuster,
die Ihnen für alltägliche Probleme, die Sie in der Softwaretechnik
haben, Patentelösungen an die Hand geben.
Das heißt, Sie kriegen da so ein Katalog.
Wenn Problem X, dann Muster Y und dann müssen Sie das eigentlich nur
so oder leicht abgewandelt nachbauen.
Das heißt, Sie müssen nicht das Rad wieder neu erfinden.
Da gab es schon Leute, die das vorher gemacht haben und die haben dann
Bücher geschrieben.
Davon können wir natürlich lernen.
Der Ausgangspunkt vom Entwerfen oder die Aufgabe vom Entwerfen ist,
die Anforderungen, die wir jetzt alle hübsch haben und die schön
aufgeschrieben sind, so in eine Komplettlösung zu überführen, dass das
System am Ende auch genau das macht, was in den Anforderungen drin
steht.
Und zwar nicht funktionaler Einzelebene, sondern eben das
Gesamtsystem.
Und deswegen ist Entwerfen auch sowas wie Programmieren im Großen.
Sie werden da vermutlich sehr selten Programmcode hinschreiben beim
Entwerfen oder Pseudocode.
Sondern da geht es darum, wie kriege ich die Anforderungen so
strukturiert, dass ich einzelne Module rauskriege, die bestimmte
isolierte Aufgaben haben.
Die idealerweise dann auch austauschbar sind.
Wir haben uns eben am Anfang der Definitionsphase angeschaut, was
haben wir denn eigentlich zu bauen, also was sind die Anforderungen.
Und jetzt überlegen wir uns, wie strukturieren wir das, dass ein
hübsches System dabei rauskommt.
Was da rausfällt, dieses hübsche System, das nennt man
Softwarearchitektur.
Das heißt, wir haben dann eine Gliederung von diesem Gesamtsystem in
Teilsysteme oder in Subsysteme oder Pakete und Bibliotheken.
Und was wir dann natürlich kriegen, ist ein Netzwerk von
Einzelkomponenten.
Die benutzen sich gegenseitig.
Was man gerne hätte, ist, dass diese Subsysteme sich nicht zyklisch
benutzen, sondern dass es eine Relation gibt, eine Hierarchie, wo man
sagen kann, dieses eine Modul hier, das benutzt zwar andere, aber das
wird von niemandem benutzt und deswegen kann ich es für sich genommen
alleine testen.
Es gibt am anderen Ende von dieser Kette Module, die benutzen nichts
anderes und die werden nur benutzt und das sind die, mit denen man
anfängt beim Test.
Weil die kann man isoliert fertig bauen und die hängen von nichts
anderem ab und dementsprechend kann man sie auch einzeln testen, ohne
dass das Gesamtsystem funktionieren muss.
Wenn Sie die Softwarekomponenten so zusammenstecken, dass jeder mit
jedem redet und jeder von jedem anderen abhängt, dann können Sie das
System erst testen, wenn es ganz fertig ist.
Und noch viel schlimmer, das Gesamtsystem funktioniert erst dann, wenn
alle Teilkomponenten funktionieren, weil ja jeder von jedem abhängt.
Also in einem Eck ein kleines Problem haben, dann ist das System
trotzdem ein Totalausfall.
Das ist etwas, was man natürlich nicht haben möchte.
Was man unter Umständen auch macht, das hatte ich kurz angerissen, ist
ein Entwurf.
Da schreibt man tatsächlich Pseudocode hin oder gibt Datenstrukturen
und Algorithmen vor.
Aber das ist doch eher die Ausnahme.
Hier geht es nicht darum, jeder Klasse in jeder Methode hinzuschreiben
mit Pseudocode, was sie dann macht.
Das ist die Aufgabe vom Entwickler.
Wenn man ein verteiltes System hat, das auf mehreren Rechnern läuft,
dann überlegt man sich in dieser Phase auch, auf welchem Rechner läuft
denn welche Komponente.
Welche Komponenten laufen auf welchen.
Das ist natürlich heutzutage besonders interessant, wenn man an
cloudbasierte Dinge denkt.
Auf welcher Cloud läuft jetzt was und was läuft überhaupt noch auf den
Client.
Wenn Sie sich ein Bild einprägen möchten, dann sieht es ungefähr so
aus.
Hier oben ist all das, was aus der Analysephase vorher herauskommt.
Also Plastenheft, Pflichtenheft, Konzept für die Benutzeroberfläche.
Idealerweise auch schon ein Handbuch und vielleicht ein Konzept, wie
die System- oder Programmhilfe funktionieren kann.
Das alles gießen Sie in Ihren Entwurfsprozess rein und am Ende fällt
dann eine hübsche Softwarearchitektur raus.
Das ist im Prinzip das Kapitel in sehr sehr kurz.
Es gibt ein paar Fragen, die man sich vorher stellen möchte.
Zum Beispiel, was für Komponenten, die wir jetzt identifiziert haben,
die gibt es schon.
Das heißt, welche haben wir schon im Keller liegen, weil wir die
letztes Jahr schon mal implementiert haben.
Oder welche kann man tatsächlich kaufen oder vielleicht sogar von
irgendjemandem herunterladen, weil sie als Open-Source-Software
irgendwo rumliegen.
Oder zumindest kostenlose Software.
Unter Umständen lohnt es sich, die Architektur, die man sich vorher
ausgedacht hat, ein bisschen anzupassen.
Weil natürlich ist es lohnenswert, wenn Sie schon ein Modul im Keller
haben, das zu 90% passt, dass Sie diese 90% auch nutzen, statt 100%
neu zu bauen.
Unter Umständen müssen Sie Ihre Architektur dann ein bisschen
hinmassieren, damit Sie mit dem Zeug auch umgehen können.
Sie sollten sich ein bisschen überlegen, was Sie mit Ihren Daten
machen.
Die Daten persistieren.
Haben Sie irgendwo Dateiablage oder verwenden Sie eine Datenbank?
Ist die relational oder ist es nur ein Key-Value-Store, wie man das
vor 50 Jahren gemacht hat und irgendwie vor drei Jahren wiederentdeckt
hat?
Und Sie sollten sich überlegen, wie funktioniert denn die
Ablaufsteuerung von dem Programm?
Das heißt, ist es ein Programm, das ich einmal anstoße und dann ist
schon vorgezeichnet, wie das bis zum Ende läuft?
Oder ist es vielleicht ereignisgetrieben?
Erwarte ich irgendwelche Events von außen, vielleicht von anderen
Systemen?
Kriege ich irgendwelche Benutzeranfragen?
Ist es ein Webdienst?
Ist das System in irgendeiner Form parallel?
Das heißt, kann ich mehrere Anfragen zur selben Zeit bekommen und muss
ich die dann auch beide beantworten?
Und dann sollten Sie sich ein paar grundsätzliche Gedanken darüber
machen, wie man das System ausrollt, wie man es einschaltet und wie
man es auch wieder abschaltet, wenn es dann mal läuft.
Sie haben ja nichts davon, wenn Sie ein System bauen, das keinen
Ausknopf hat und Sie müssen den Stecker ziehen und dann sind alle
Daten weg.
Und natürlich kommt auch hier wieder der Punkt mit den
Zugriffsrechten, wenn Sie im Lastenheft sowas drin stehen haben, wie
der Kundensachbearbeiter darf keine Seminare bearbeiten.
Dann müssen Sie sich hier ein bisschen Gedanken darüber machen, wie
Sie solche Zugriffsrechte modellieren, wie Sie sicherstellen, dass sie
auch eingehalten werden.
Weil wenn Sie es hier nicht gemacht haben, dann gibt es später
ziemlich sicher Chaos.
Was hier draufsteht, sind lauter Fragen, die, denke ich mal,
hauptsächlich aus den funktionalen Anforderungen kommen.
Es gab ja aber auch noch die nicht-funktionalen Anforderungen.
Es war ein relativ großer Boost von Dingen, wie zum Beispiel eine
schnelle Entwicklung, Flexibilität, eine niedrige Laufzeit.
Das Ding soll natürlich fehlertolerant sein, wenn da ein Anwender
Quatsch macht, dann soll das System möglichst keinen Quatsch machen.
Die Entwickler hätten gerne, dass das Ding gut fahrbar ist und hohe
Wiederverwendbarkeit hat und so weiter.
Und diese ganzen Ziele, das sind nicht Ziele, die haben alle, sondern
es gibt so ein paar Ziele, die haben natürlich alle gemeinsam.
Nämlich das System sollte zuverlässig sein, das will sowohl der, der
es bezahlt, als auch der, der es benutzt.
Natürlich will es auch der, der es entwickelt, weil je unzuverlässiger
das System ist, desto mehr Wartungsaufwand und Arbeit hat er.
Aber sowas wie saubere Schnittstellen, intern, das interessiert den
Anwender nicht.
Das System ist völlig Rille, wie Ihre API aussieht, solange sie
funktioniert.
Dem Auftraggeber ist es auch ziemlich egal, solange sie das liefern,
was sie versprochen haben, zu dem Preis, den sie versprochen haben, zu
dem Zeitpunkt, den sie versprochen haben.
Aber das Problem ist, diese ganzen Ziele, die ziehen so ein bisschen
aneinander.
Natürlich will der Kunde, dass es besonders schnell geht und besonders
billig und fehlerfrei.
Das können Sie natürlich liefern, aber halt nicht gleichzeitig.
Das heißt, meistens müssen Sie irgendwie abwägen.
Die Laufzeit, die hängt meistens mit dem Platzbedarf zusammen.
Wenn Sie ganz viel vorberechnen und das abspeichern, dann können Sie
es wahrscheinlich schnell nachschlagen, aber Ihr Speicherplatzbedarf
ist riesig.
Oder die Funktionalität und die Benutzbarkeit.
Wenn Sie ganz viele Funktionen haben, die Sie in Ihrer GUI anbieten,
dann leidet darunter vermutlich die Benutzbarkeit, weil Sie haben eine
Milliarde Menüpunkte, die Sie irgendwie verteilen müssen und der
Anwender muss jetzt lernen, wo Sie welchen versteckt haben.
Wenn das System nur eine Funktion hat, dann reicht natürlich ein
Knopf.
Das ist super einfach benutzbar, aber der Funktionsumfang ist nicht so
pralle.
Die Kosten und die Wiederverwendbarkeit zerren natürlich auch
aneinander und so ein hübscher Satz ist das hier.
Good, fast, cheap, pick any two.
Kann man sich dann aussuchen.
Natürlich kann der Kunde sagen, mein System soll saugeil laufen und
stabil und schnell und günstig und morgen geliefert werden, aber dann
wird es halt richtig teuer.
Wenn er sagt, ich will dafür nichts bezahlen, dann machen Sie das mal
in Ihrem Feierabend.
Dann werden Sie es auch irgendwann liefern, aber die Qualität, naja
gut, Sie haben es halt abends um 10 nach zwei Bier gemacht und
besonders schnell waren Sie da halt auch nicht mehr.
Das heißt, Sie haben da einfach Ziele, die miteinander in Konflikt
stehen.
Sie müssen sich als Systemhaus oder als Chef, als Projektleiter
überlegen, welche Ziele muss ich denn tatsächlich erfüllen und über
welche können wir vielleicht ein bisschen verhandeln.
Dann müssen Sie wieder mit Ihrem Kunden sprechen oder mit Ihrem
nächsthöheren Vorgesetzten.
Dann darf der zwei auswählen.
Sie müssen eben nur die Auswahlpalette hinlegen.
Wie gesagt, vorhin in der Kapitelübersicht, es gibt zwei große
Strategien, die wir uns anschauen.
Das erste ist der modulare Entwurf.
Das zweite ist der objektorientierte Entwurf.
Und dieser OO-Entwurf, der ist eigentlich nur weiter gedacht oder
moderner gedacht, was damals bei dem modularen Entwurf schon gezeigt
worden ist.
Dementsprechend fangen wir auch mit dem modularen Entwurf an.
Sie sollten sich das im Balzard nochmal anschauen.
Ein nicht ganz kleines Kapitel, aber es lohnt sich, da mal ein
bisschen durchzublättern.
Was Sie bekommen, wenn Sie mit dem modularen Entwurf fertig sind, ist
eine hübsche Systemarchitektur.
Da ist natürlich ein Modulführer dabei.
Da steckt drin, was für Module gibt es denn überhaupt.
Wie gliedert sich das System in diese Module?
Was für Funktionen haben die einzelnen Module?
Da haben Sie natürlich auch schon ein Entwurfsmuster auf sehr hohem
Abstraktionsniveau, wo Sie sehen können, wie ist denn die
Systemarchitektur.
Habe ich eine Schichtenarchitektur, wo ich oben die Anwendungsschicht
habe und ganz unten habe ich die Verarbeitungsschicht und dazwischen
gibt es eine Middleware?
Oder habe ich ein Fließband?
Solche Sachen, die kann man da schon sehen.
Und natürlich stecken da auch schon die Modulschnittstellen drin.
Das heißt, Sie schreiben da nicht nur hin, es gibt das Modul
Datenverarbeitung, sondern Sie schreiben da auch hin, was geht da rein
und was kommt da raus.
Insbesondere schreiben Sie da auch hin, wie sehen denn die
Dateiformate aus.
Sie können nicht hinschreiben, da geht eine XML-Datei raus und da
kommt eine HTML-Datei rein und da kommt eine HTML-Datei raus.
Wenn Sie nicht hinschreiben, wie diese XML-Datei strukturiert sein
muss, nach welchem XML-Schema die funktioniert zum Beispiel.
Wenn Sie kein so ein schickes Format haben wie XML, sondern einfach
irgendein Textformat, dann ist es besonders geschickt, wenn Sie sich
eine Grammatik für diese Textdatei ausdenken.
Damit man also sicher sagen kann, wie dieses Textdokument strukturiert
ist, wie das aufgebaut ist, und wie die Software funktioniert.
Wie das Modul damit umgeht.
Was ich vorhin schon angerissen hatte, war diese benutzte Relation.
Das ist einfach die Beschreibung, welches Modul benutzt welche anderen
Module.
Wissen, was hängt wovon ab und wo fange ich an zu entwickeln.
Natürlich fangen Sie nicht mit dem Modul an, das 30 Abhängigkeiten
hat, die Sie noch gar nicht haben.
Einfach ein bisschen ungeschickt.
Optional der Feinentwurf mit Pseudocode oder ähnlichen Dingen.
Es gibt zwei große Sichten auf diesem Entwurf.
Das eine ist der externe, das andere ist der interne Entwurf.
Im externen, da sehen Sie einfach die grobe Struktur und die
Schnittstellen von den Modulen.
Das heißt, das, was jemand zu sehen bekommt, wenn er ihr Modul
benutzt.
Beim internen Entwurf kriegen Sie die Abhängigkeiten von dem Modul und
den Feinentwurf.
Das ist etwas, was jemand, der es benutzen möchte, nicht braucht.
Er kriegt ja nur das Modul und es funktioniert.
Er muss nicht wissen, von was dieses Modul abhängt.
Er muss auch nicht wissen, wie es implementiert ist.
Für die externe Ansicht einfach irrelevant.
Und wie gesagt, die Konzepte, die da angesprochen werden, sehen Sie in
ähnlicher Form im objektorientierten Entwurf wieder.
Es gibt ein paar Anforderungen an Module oder an dieses Modulkonzept.
Ein paar recht plakative sind.
Ein Modul sollte ohne Kenntnis von der späteren Nutzung entworfen und
implementiert und insbesondere getestet werden.
Das heißt, Sie kriegen einfach diese Modulspezifikation.
Was soll es tun?
Wo wird es später eingesetzt?
Das ist nicht Ihr Problem.
Dementsprechend sollten Sie Ihr Modul auch so bauen, dass Sie die
Implementierungsdetails von anderen fremden Modulen nicht kennen
müssen.
Das ist sehr ungeschickt.
Dann haben Sie Abhängigkeiten nach außen.
Und wenn jemand dieses andere Modul, diese Interna von diesem anderen
Modul ändert, haben Sie ein Problem.
Also müssen Sie Ihr Modul auch patchen und nachpflegen.
Das heißt, so ein Modul, das ist eigentlich eine schicke Blackbox.
Da steht nur dran, was sie tut, was sie reinkriegt und was sie
rausgibt.
Aber da steht eben nicht drin, wie ist diese Blackbox aufgebaut, was
macht die Blackbox intern.
Ist die vielleicht innen drin rosa?
Das ist völlig egal.
Im Normalfall ist ein Modul nicht ein einzelnes Programm.
Also nicht ein Stück Quellcode, sondern das sind mehrere, die
zusammenarbeiten.
Aber wie wir letztes Mal auch schon gesehen haben, mit der
Paketstruktur um L, ist hier jetzt genau im Prinzip das gleiche.
So ein Modul ist nichts anderes als ein Paket, eine Zusammenfassung
von Einzelsoftwarekomponenten, sehr kleinen Komponenten
wahrscheinlich, die man eben zusammen benutzt, um das Ziel von dem
Modul zu erreichen.
Dementsprechend sollten diese Programmteile, die in dem Modul stecken,
natürlich eine starke Kohäsion haben.
Das ist exakt parallel zu dem Paket.
Idealerweise sollte ein Modul natürlich so einfach sein, dass Sie es
für sich selbst allein genommen verstehen können.
Das ist auch parallel zu dem Entwurfszeug.
Wenn das Modul 423 verschiedene Programme hat und 801 Ausgabeformate,
dann ist es vermutlich zu groß.
Dann macht es nicht nur eine Sache, dann ist vermutlich auch nicht
nachvollziehbar oder verstehbar, was es alles macht.
Das heißt, als Moduldefinition hätten wir jetzt sowas hier.
Ein Modul ist eine Menge von Programmelementen, die nach dem
Geheimnisprinzip gemeinsam entworfen und geändert werden.
Um diese Definition zu verstehen und zu benutzen, muss man natürlich
wissen, was genau das Geheimnisprinzip ist.
Das Geheimnisprinzip fasst diese Anforderungen ein Stück weit
zusammen, die wir gerade besprochen haben.
Das Geheimnisprinzip besagt, jedes Modul verbirgt eine wichtige
Entwurfsentscheidung hinter einer wohldefinierten Schnittstelle.
Und zwar so, dass sich die Schnittstelle nicht ändert, wenn ich die
Entwurfsentscheidung ändere.
Das soll heißen, wenn Sie irgendein Modul bauen und Sie definieren die
Schnittstelle, dann machen Sie das bitte so, dass Sie hintendran die
Implementierung ändern können, ohne dass sich die Schnittstelle
anpasst.
Das hat einfach den Hintergrund, wenn jemand Ihren Modul benutzt, dann
soll es ihm egal sein, wenn Sie nächstes Jahr auf die Idee kommen,
Ihre Implementierung hübscher zu machen.
Der soll nicht davon abhängen, was Sie später ändern.
Umgekehrt, wenn Sie andere Module benutzen, sind Sie natürlich auch
nicht darauf angewiesen, zu wissen, was deren Entwickler Ihnen ändern.
Wir haben ein kleines Beispiel mitgebracht.
Da habe ich Thomas gefragt, wir würden gerne ein
Grafikverarbeitungsprogramm machen.
Da ist besonders geschickt, wenn man da drin Strecken zeichnen kann.
Also Linien, die irgendwo anfangen und irgendwo aufhören.
Da hat er gesagt, gut, Java kann ich, Startpunkt, Endpunkt, da brauche
ich nicht viel.
Ich habe nur gehört, cheap, fast, good.
Und dann kam...
Ja, dieses Meisterwerk heraus.
Also eine Klasse, wir sind in Java, die hat keinerlei Geheimnisse.
Die sagt halt, ich habe einen Startpunkt, der hat einen x-Wert und
einen y-Wert und einen Endpunkt, auch wieder mit x und y.
Und das sind alle public.
Und wenn du jetzt eine neue Strecke haben willst, dann mach dir halt
eine neue Streckeninstanz und setz diese Attribute.
Dann hast du eine super Strecke, da ist alles drin, was man braucht.
Und damit ist es erledigt.
Also das war auf jeden Fall fast und cheap.
Das Problem ist, wenn wir diese Klasse jetzt benutzen wollen, dann
müssen wir tatsächlich wissen, wie die implementiert ist.
Also wir müssen wissen, wie die Attribute heißen.
Und das ist natürlich bei so einer Klasse relativ einfach.
Das sind ja nur vier Dinge, die man sich merken muss.
Aber das ist halt nicht so super gut.
Insbesondere, wenn der Herr Karcher auf die Idee kommt, aus dem
Startpunkt einen S-Punkt zu machen und den y vielleicht 1 und 2 zu
nennen.
Oder auf Englisch zu übersetzen oder so etwas.
Das ist irgendwie ungeschickt.
Da habe ich gesagt, das geht so nicht.
Kann man nicht machen.
Können wir so nicht verkaufen.
Wir haben ja gerade Geheimnisprinzip erklärt und jetzt kommst du mit
sowas an.
Also hat er mal schön nachgebessert.
Also ich habe gesagt, Schnittstelle, wohl definiert.
Und Schnittstellen im Klassenkontext, das sind ja Methoden.
Also brauchen wir irgendwelche Methoden, um diese Strecke anzulegen
oder diese Strecke zu verändern.
Gut, Fast, Cheap.
Fast und Cheap, wieder ganz groß geschrieben.
Ich mache das einfach auf Private.
Also diese Koordinaten, das sind jetzt private Variablen.
Auf die Interna kann man nicht mehr zugreifen.
Und die wohl definierte Schnittstelle.
Naja gut, man braucht also offensichtlich vom Startpunkt X und Y
Koordinaten und vom Endpunkt auch.
Also vier Getter und Setter.
Damit habe ich eine schöne Schnittstelle und kann hinten dran die
Variablen umbenennen.
Ich muss ja die Methode nicht mit ändern.
Das ist ja wunderbar.
Damit habe ich also quasi eine Kapselung auf die Attribute erreicht.
Man kommt nicht mehr direkt dran.
Wenn jetzt jemand die Attribute umbenennen möchte, dann kann er das
gerne tun.
Dann fangen die Methoden nicht an.
Aber so wirklich dolle ist diese Kapselung nicht.
Weil semantisch kapselt überhaupt gar nichts.
Wir wissen immer noch, es gibt ziemlich sicher vier Attribute und da
landen diese X und Y Werte drin.
Und da gab es also dann nochmal auf die Finger.
Hier refaktorisieren, das ist immer ganz groß.
Und Klassen kann man nie genug haben.
Deswegen werfen wir dieses X und Y hier oben weg.
Machen dafür was Neues.
Hier gibt es eine ganz tolle neue Klasse.
Hat jemand eine Idee, was man da jetzt gut brauchen könnte?
Punkt.
Genau.
Genau genommen brauchen wir sogar zwei.
Nämlich den Start- und den Endpunkt.
Und die haben wir hier einfach definiert.
Das heißt, es gibt jetzt eine neue Klasse Punkt.
Die hat natürlich wenigerweise X und Y Koordinaten.
Wenn ich jetzt den Startpunkt oder den Endpunkt setzen möchte, dann
muss ich halt auch einen Start- und einen Endpunkt übergeben.
Ich möchte an der Stelle mal betonen, dass also die Klasse Punkt jetzt
schon nach den neuen Richtlinien programmiert wurde.
Also wir greifen ja hier über GetX und GetY und sowas drauf zu.
Also auch dort sind die internen, die beiden Koordinaten, die einen
Punkt ausmachen, schon schön gekapselt und verborgen.
Habe ich also gleich richtig gemacht.
Ja, also hat er wirklich was gelernt.
Zweimal denselben Fehler war man nicht.
Und damit haben wir tatsächlich eine hübsche semantische Kapselung
erreicht.
Und das Schicke ist, irgendwas muss man mit diesen Strecken auch
machen.
Wir haben gedacht, die Länge von so einer Strecke abfragen, das will
man vermutlich relativ oft machen.
Dementsprechend gibt es jetzt hier eine GetLänge-Funktion.
Die rechnet jetzt eben die euclidische Distanz aus, von diesen zwei
Punkten, vom Start- und Endpunkt.
Und das hat er auch super implementiert, genauso wie es im Mathe
-Lehrbuch Klasse 7 steht.
Da war ich ja noch ganz groß.
Und dann ist eigentlich schon toll.
Da haben wir eine super Streckenimplementierung.
Das haben wir dann beim Kunden ausgerollt.
Und der hat dann mit Strecken gemacht, noch und nöcher.
Das war echt toll.
Bis der Kunde wieder bei uns aufgeschlagen ist.
Dann hat er gesagt, das ist sacken langsam, was ihr da habt.
Wir machen ja nicht viel mit diesen Strecken, aber wir fragen ganz oft
ab, wie lang die sind.
Dann rechnet das Programm natürlich jedes Mal die Strecke neu aus.
Da kann man auf die Idee kommen, wir machen das ein bisschen anders.
Wir speichern uns die Strecke zwischen, haben da quasi so einen Cache
eingezogen, damit jede Strecke ihre Länge nur einmal ausrechnen muss.
Das ist natürlich machbar, aber so einfach wollten wir es nicht
machen.
Die Schnittstelle gäbe das aber her.
Die Schnittstelle gäbe das her.
Also eine schöne Eigenschaft dieser Kapselung ist ja jetzt, wenn der
Kunde einen neuen Start oder einen neuen Endpunkt setzt, über die
Schnittstellenmethode, kann ich in der Methode auch sofort die Länge
der Strecke aktualisieren.
Das kann ich nur tun, wenn ich so eine Schnittstellenmethode habe.
Beim Abfragen muss ich also nie neu rechnen.
Das wäre ganz hübsch gewesen, aber dann hat der Kunde uns noch gesagt,
also eigentlich sind das ja keine Strecken, sondern das sind vor allem
Steigungen von meinem Aktiendepot.
Und außerdem war Speicherplatz noch eine Nebenbedingung.
Ja, also Speicherplatz...
Eigentlich implizite Eigenschaften einer Strecke, explizit den
Attributen abspeichern und so nebenbei Buchhaltung machen und so, ist
er eigentlich auch nicht so speicherplatzsparend.
Deswegen wollte er, dass wir also nach wie vor eher sparsam da sind.
Ja, aber was ihn halt furchtbar interessiert hat, war, welchen Winkel
hat dieses Ding jetzt, wenn man das in ein 2D-Koordinatensystem
einträgt mit X und Y, X horizontal, Y vertikal, was kommt da für ein
Winkel raus?
Das kann man natürlich jedes Mal ausrechnen, aber das ist halt auch
was, was man nicht jedes Mal ausrechnen möchte.
Aber so Strecken, die kann man ja noch anders speichern.
Da kam der Herr Karcher auf eine super Idee.
Matheklasse 7,5, ich weiß nicht genau, vielleicht auch 8.
Klasse.
Man kann so eine Strecke ja nicht nur mit Start- und Endpunkt
speichern, sondern mit einem Startpunkt, einem Winkel und einer Länge.
Ich hatte ja G9 bei mir aus der 8.
Klasse.
Ein bisschen verkürzt oder verlängert.
Wie dem auch sei, man kann diese Strecke jetzt auch noch ganz anders
implementieren.
Die Methoden, die Schnittstelle, die bleibt immer gleich.
Aber man kann jetzt sagen, setze Endpunkt und dann rechnet die Methode
aus, wie ist denn die Länge und wie ist der Winkel ausgehend von dem
Startpunkt.
Und diese beiden Informationen, die werden gespeichert.
D.h.
wir haben hier oben zwei neue private Variablen, die uns die Länge und
den Winkel angeben.
Das sind die beiden Attribute, die der Kunde ganz häufig abfragt.
Und nur noch einen Startpunkt.
D.h.
wir haben den Endpunkt getauscht gegen zwei neue Attribute, ohne dass
man das an der Schnittstelle irgendwie sehen kann.
Weil natürlich kann ich immer noch GetEndPunkt ausrechnen und den
richtigen Punkt zurückgeben.
Wenn das nicht so oft gebraucht wird, kann man das auch nicht so oft
machen.
Und an der Stelle haben Sie jetzt vielleicht gesehen, dass es sich
lohnt, so eine Schnittstelle sauber zu definieren.
Insbesondere direkt auf Klassenattribute oder auf Instanzvariablen
zugreifen von außen, das ist nicht besonders stabil.
Wenn Sie auf die Idee kommen, dieses Attribut brauche ich eigentlich
nicht mehr, könnte ich löschen.
Insbesondere muss ich es nicht immer mit pflegen.
Können Sie das nicht tun, weil irgendjemand von außen könnte ja dieses
Attribut benutzen.
Wenn Sie Zugriffsmethoden verwendet haben, dann können Sie das
Attribut einfach wegwerfen.
Halt in der Zugriffsmethode ausrechnen.
Aber Sie können dann Ihre Klasse intern schön strukturieren und
aufräumen.
Kurzum, Änderungen, die vorhersehbar sind oder die wahrscheinlich
sind, die sollten möglich sein, ohne die Schnittstelle zu ändern.
Das heißt, überlegen Sie sich mal, welche von diesen
Entwurfsentscheidungen macht man denn höchstwahrscheinlich falsch?
Das soll heißen, wo bin ich mir nicht ganz sicher, wie ich das jetzt
eigentlich umsetzen will?
Diese Dinge sind ein wunderbarer Kandidat, um hinter einer
Schnittstelle verborgen zu werden.
Wenn Sie sich nicht sicher sind, dann machen Sie halt eine hübsche
Schnittstelle, dann können Sie hintendran später austauschen, wenn die
erste Entscheidung doch eine Fehlentscheidung war.
Wenn Sie weniger vorhersehbare Änderungen haben oder welche, die
extrem unwahrscheinlich sind, dann können die schon mal eine
Schnittstellenänderung nach sich ziehen, weil das, was passiert, ist
ja auch eher unwahrscheinlich.
Insbesondere Änderungen, die sich auf häufig benutzte Module auswirken
und auf deren Schnittstellen, die dürfen nur ganz selten auftreten.
Da gibt es jetzt zwei Anmerkungen dazu, die uns Professor Tichy
draufgeschrieben hat.
Die erste ist, ganz oft führt das Geheimnisprinzip dazu, dass Sie
nicht direkt auf die Attribute zugreifen.
Das heißt, wenn Sie eine Klasse schreiben, die Attribute hat,
Markieren Sie die als Private und machen Sie Getter und Setter
Methoden dafür.
Dann haben Sie eine Schnittstelle und dann ist gut.
Das heißt aber nicht, dass Sie damit schon Kapselung erreicht haben.
Das heißt, wenn Sie dieses erste Beispiel von Thomas mit dem SetStartX
und SetStartY verwenden, dann haben Sie zwar Zugriffsmethoden, aber
gekapselt haben Sie da eigentlich nichts.
Und als Anmerkung zwei hat er uns mitgegeben, was bedeutet denn
deprecated in der Java-Dokumentation oder dieses Java-Attribut.
Kennt das jemand von Ihnen?
Einer, zwei, drei, fünf, zehn...
Vielleicht die Hälfte.
Möchte jemand der anderen Hälfte sagen, was das bedeutet?
Ihr Kommilitone sagt, wenn Sie eine Methode antreffen, wo dieses
Attribut dran steht oder wo in der Dokumentation dran steht, Sie seid
deprecated, dann heißt das, die ist überholt, veraltet.
Es gibt eine neue Methode, die das gleiche tut, aber wahrscheinlich
mit einer anderen Schnittstelle.
Verwenden Sie dann die.
Was hat das denn für Auswirkungen, wenn Sie die trotzdem benutzen?
Das hindert Sie ja niemandem, die Methode ist ja schließlich da.
Vielleicht wird sie vier Versionen, vielleicht 100 Versionen,
vielleicht nie.
Vielleicht aber schon nächste Version gelöscht.
Dann haben Sie ein Problem, dann müssen Sie Ihren Code umarbeiten.
Und wenn Ihnen die Entwickler von dieser Bibliothek, die Sie da
benutzen, schon sagen, also das hier besser nicht mehr nehmen, das
wollen wir eigentlich nicht haben, dann lassen Sie es halt auch besser
sein.
Sie handeln sich damit einfach nur...
Sie sparen sich ein bisschen Arbeit, wenn Sie es nicht tun.
Sie haben ja eine Methode gefunden, die das macht, was Sie haben
wollen, dann sind Sie fertig.
Aber wenn der Hersteller dann auf die Idee kommt, die Methode
wegzuwerfen, dann haben Sie halt den Aufwand später.
Sie haben auch den Ärger später, weil vermutlich ist Ihr Produkt dann
schon ausgerollt.
Dann kann es sein, dass Ihnen da beim Kunden was abbraucht.
Man könnte auch sagen, deprecated ist die Markierung einer
Schnittstellenänderung.
Eine Ankündigung vielleicht.
Wenn Sie nach dem Geheimnisprinzip entwerfen wollen, dann sollten Sie
sich so eine Liste von Entwurfsentscheidungen machen.
Sie haben ja Ihr System strukturiert.
Dann schreiben Sie sich einfach auf, was könnte sich denn ändern.
Was für Entscheidungen habe ich überhaupt getroffen?
Und dann schreiben Sie nebendran, wie schwierig war die Entscheidung,
also wie schwer ist sie Ihnen gefallen, wie unsicher waren Sie.
Dann sollten Sie hingehen und die Schnittstellen für diese
Entscheidung entsprechend robust bauen.
Und Sie sollten sich entscheiden, welche Entscheidung verberge ich
denn hinter welchem Modul.
Vermutlich gibt es mehrere Entscheidungen, die auf dasselbe Modul
zutreffen.
Und dementsprechend wäre es natürlich geschickt, wenn Sie das
thematisch auch sinnvoll zusammengruppieren.
Also wenn Sie sich entwurfsmäßig auf irgendein Muster entschieden
haben, und das taucht überall in Ihrer Software auf, dann wäre es
vielleicht geschickt, wenn Sie das nur in einem Modul implementieren
und nicht in allen.
Wenn Sie die Entscheidung dann ändern, müssen Sie auch nur ein Modul
entsprechend anpassen.
Klassiker für solche Entscheidungen sind Datenstrukturen.
Wie implementiere ich im Hintergrund die Datenstrukturen?
Nehme ich ein Array?
Nehme ich eine Liste?
Nehme ich einen Vektor?
Nehme ich eine Hashmap, einen Baum, was auch immer?
Das sollte man nicht nach außen geben, was man da tatsächlich will.
Das gleiche gilt natürlich für maschinennahe Details oder
betriebssystemnahe Details.
Also wenn Sie darauf angewiesen sind, irgendwas mit einem Dateisystem
zu machen, dann sollten Sie das kapseln.
Aber wenn sich das ändert aufgrund von Betriebssystemen, dann ist es
gut, wenn Sie nur ein Modul anpassen müssen.
Dasselbe gilt für alles, was mit Netzwerk zu tun hat und generell mit
Ein - und Ausgabe.
Schauen Sie, dass Sie einen Weg in Ihr System reinhaben und einen
raus.
Vielleicht mit verschiedenen Formaten, aber nur an einer Stelle und
nicht überall im System verteilt.
Was natürlich auch noch dazugehört, sind so Dinge wie Datenbanken und
Bibliotheken.
Schauen Sie, dass Sie, wenn Sie eine Datenbank einsetzen möchten,
nicht in allen Komponenten Ihrer Software auf die Datenbank zugreifen
und da direkt SQL-Statements schreiben.
Weil wenn Ihr Chef nächste Woche auf die Idee kommt, SQL-Datenbanken
sind uns viel zu teuer, macht das alles mal in Textdateien.
Dann ist halt geschickt, wenn Sie nur ein Eingabe- oder Ausgabemodul
anpassen müssen und nicht die ganze Software.
Benutzerschnittstellen sind ähnlich.
Das heißt, egal ob Sie jetzt eine GUI bauen oder ein
Kommandozeilenwerkzeug, gucken Sie, dass Sie diese Eingabe-Dinge, auch
wenn es jetzt keine Dateien sind, sondern Dinge, die der Benutzer oder
ein anderes System an Ihr Werkzeug schickt, durch eine Tür durchgeht
und nicht durch 47 Türen.
Wenn sich da was ändert, dann müssen Sie halt nur einen Türsteher
austauschen.
Das ist oft äußerst geschickt.
Und was Sie im JMJ RST auch gesehen haben, war dort diese Messages
-Datei.
Das sind Beschriftungen in Anwendungen.
Idealerweise bauen Sie heute nicht eine Anwendung für den deutschen
lokalen Markt oder für, was weiß ich, Südbaden-Württemberg, sondern
für den globalen Markt und dann werden Sie das Ding vielleicht
nächstes Jahr auf Englisch oder auf Hindu ausliefern müssen.
Wenn Sie Ihre gesamten Beschriftungen hardcoded im Programm drin
stehen haben, dann ist es natürlich schwierig, das zu ändern und zu
pflegen.
Insbesondere machen Sie mal mehrere Varianten gleichzeitig.
Dann müssen Sie das Programm ändern, neu kompilieren und dann haben
Sie zwölf CDs und je nachdem, welcher Benutzer gerade da ist, muss er
halt die andere CD einlegen.
Das ist ein bisschen doof.
Wenn Sie also eine Datei haben, wo all diese Beschriftungen
drinstehen, dann können Sie einfach nur diese Datei austauschen.
Wahrscheinlich sogar zur Laufzeit.
Das ist äußerst geschwind.
Was als Ergebnis hinten rausfällt, das hatte ich Ihnen schon gesagt,
das ist der Modulführer oder der Grobentwurf.
Da steht, wie eingangs auch gesagt, alles drin über Ihre
Systemstrukturierung und über die einzelnen Module.
Wie sind die zu verwenden?
Und idealerweise beschreibt dieser Modulführer auch, was für
Entscheidungen Sie da getroffen haben.
Also was ist das Geheimnis von diesem Modul?
Nicht für den Benutzer gedacht, diese Information, sondern für
jemanden, der das später anpassen, warten, ändern muss.
Der soll natürlich wissen, warum haben Sie die Schnittstelle genau so
gewählt, obwohl Sie die Hälfte davon gar nicht mehr benutzen.
Weil die Hälfte halt aus einer alten Entscheidung stammt.
Wir haben die Schnittstelle stabil gehalten, aber wir brauchen die
Sachen eigentlich nicht mehr.
Solche Dinge sollten Sie dokumentiert haben.
Insbesondere das Wort Geheimnis an der Stelle ist vielleicht ein
bisschen irreführend auch.
Also heutzutage mit Edward Snowden und so sind ja Geheimnisse ein
bisschen relativ.
Aber das ist nicht der Begriff von Geheimnis, den wir hier meinen.
Geheimnis ist nicht gemeint, Sie wollen irgendwie Sicherheit erzeugen
oder irgendwas in einem gedachten Tresor oder so etwas halten.
Mit Geheimnis verbinden Sie besser an der Stelle diese Kapselung.
Geheimnis ist einfach nur gemeint, derjenige, der Ihr Modul benutzt,
muss sich nicht um die Interna kümmern, weil die ein Geheimnis des
Modulentwicklers sind.
Also verwechseln Sie das Geheimnis an der Stelle bitte nicht irgendwie
mit Sicherheit oder so etwas.
Das ist nicht, was an der Stelle gemeint ist.
Wenn Sie so einen Modulführer sorgfältig aufschreiben oder es jemand
von Ihnen verlangt, tut er das nicht ohne Grund, sondern das hat
eindeutige Vorteile.
Also wenn Sie das mal aufgeschrieben haben, dann vermeiden Sie damit
natürlich Duplikate.
Wenn Sie einfach drauf los entwickeln, dann haben Sie hinterher
vielleicht zwei Komponenten, die die Dateien einlesen können.
Wenn Sie das vorher sauber strukturiert haben.
Irgendjemand hat hier Musik laufen, das finde ich furchtbar
irritierend.
Vielleicht ist es auch ein klingendes Handy.
Einfach ausmachen.
Wenn Sie den Modulführer sauber aufgeschrieben haben, vermeiden Sie
Duplikate.
Wenn Sie einfach drauf los entwickeln, dann haben Sie vielleicht zwei
Komponenten, die die Dateien einlesen können.
Wenn Sie vorher das System sauber strukturiert haben und sagen, das
ist mein Modul für die Eingabe, dann wird es auch nur einmal
implementiert.
Sie vermeiden aber auch Lücken.
Ihnen fällt nicht hinterher auf, oh hups, ich kann ja gar nichts
speichern.
Das haben wir ja vergessen.
Sondern das sehen Sie, wenn Sie das System strukturiert haben, schon
am Entwurf.
Sie kriegen natürlich dann auch eine Zerlegung von dem Gesamtproblem.
Wenn Ihnen jemand sagt, entwickeln Sie mir mal bitte ein SAP, dann ist
das wahrscheinlich ein recht großes Problem.
Wenn Sie das hinterher schön in eine Milliarde Module zerstückelt
haben, dann können Sie diese eine Milliarde Einzelprobleme getrennt
schick bearbeiten.
Und wenn Sie hinterher Probleme haben mit irgendeinem Modul, dann
hilft Ihnen natürlich auch der Modulführer, dieses eine wieder zu
finden, an dem Sie etwas ändern müssen.
Wenn irgendwo ein Fehler auftritt, wenn Ihnen ein Kunde anruft und
sagt, wenn ich im Programm arbeite, ist alles toll, wenn ich es
gespeichert habe, ist es kaputt.
Dann ist vermutlich dieses Modul für das Dateispeichern zuständig und
Schuld.
Da muss ich auch nur das angucken und raussuchen.
Das Ergebnis für so ein Modul, das hatte ich vorhin auch schon mal mit
dem hübschen Begriff Blackbox bezeichnet.
Sie kriegen da eine Beschreibung, die von dem Modul sagt, wie ist es
zu verwenden, warum kann man es verwenden, wie ist die Schnittstelle,
warum ist die so, wie sie ist.
Aber Sie müssen, wenn Sie diese Beschreibung gelesen haben, nicht
wissen, wie das implementiert ist.
Es kann Ihnen egal sein, ob da jemand ein Quicksort oder ein
Bubblesort gebaut hat.
Vielleicht ist sogar beides drin, das kann ja auch sein.
Das kann Ihnen einfach egal sein.
Und wenn Ihnen jemand ein Modul samt Modulführer auf den Schreibtisch
legt und Sie Ihre Software dagegen bauen, dann können Sie den
natürlich darauf festnageln.
Der kann dann nicht seine Schnittstelle einfach wild ändern, wie es
ihm gerade einfällt.
Weil wir haben ja hier ein Dokument, wo drin steht, wie Sie sein Modul
benutzen dürfen.
Und da muss er sich natürlich auch dran halten und liefern.
Das ist so ein gegenseitiger Vertrag unter Entwicklern.
Wir haben dann einfach die Sicherheit, das was ich da habe, das
funktioniert morgen auch noch so.
Solange Sie keine Duplicated Methoden verwendet haben.
In so einer Modulbeschreibung steht natürlich drin, was sind die
öffentlichen Programmelemente.
Wie sind die Ein-Ausgabe-Formate, sofern Sie Ein-Ausgabe-Modul
beschreiben.
Was sind die Parameter und was sind die Rückgabewerte von so einem
Unterprogramm.
Dann kriegen Sie natürlich eine Beschreibung, was passiert.
Wenn Sie da ein Auto reinstecken, dann kommt ein Flugzeug raus.
Sollte in der Methoden- oder in der Modulbeschreibung drinstehen.
Und es sollte auch drinstehen, wie schnell funktioniert das Ganze.
Wie ist die Genauigkeit von dieser Berechnung.
Wie ist vielleicht der Speicherplatzbedarf.
Ähnliche Dinge.
Natürlich steht da auch drin, was für Fehler können dann auftreten.
Und in welchen Fällen.
Wenn Sie eine Software haben, die ein Auto als Eingabe erwartet und
Sie stecken da ein Flugzeug rein.
Dann darf dieses Modul ja in einen Fehlerzustand gehen.
Und genau was für einen Fehlerzustand da auftritt und in welchen
Fällen, das steht da natürlich drin.
Und so eine Modulbeschreibung hat jeder von Ihnen schon mal gesehen,
da bin ich mir sicher.
Das ist genau das, was Sie in einem Java-Doc auch haben, wenn das
sauber geschrieben ist.
Da stehen genau diese Elemente drin.
Mal abgesehen von der Datei.
Sie kriegen Parameter und die Rückgabewerte, also die Signatur von der
Methode.
Sie bekommen hoffentlich Qualitätsaussagen und Sie bekommen auch die
Informationen, was für Fehler da auftreten.
Sie haben natürlich in der Signatur drin, welche Ausnahme ausgelöst
werden kann.
Aber in dem Java-Doc sollte drinstehen, wann und warum oder wie man
das beheben kann.
Also so eine Modulschnittstelle oder eine Modulbeschreibung hat jeder
von Ihnen schon mal in der Hand gehabt.
Dementsprechend glaube ich, dass Sie da verstanden haben, warum man
das eigentlich haben will.
Sie eine API bekommen, Methoden heißen alle nur X, Y und Z und die
Parameter genauso.
Sie haben keine Beschreibung, haben Sie verloren.
Wir haben eine kleine Fallstudie dabei, da würde ich Thomas noch ein
bisschen einbinden.
Da geht es um den Quick-Index, den Keywords-in-Context-Index.
Das hat man vor gefühlt 100 Jahren, wahrscheinlich sind es nur 40,
verwendet, um Dinge zu suchen oder Dinge wiederzufinden.
Die Idee ist, man hat eine Menge von Titeln, zum Beispiel Buchtiteln
oder Filmtiteln.
Dieser Index, der ist jetzt eine Liste, wo nicht jeder Titel einmal
drin steht, sondern jede Permutation von dem Titel.
Das heißt, wenn Sie beispielsweise den Titel haben, Harry Potter und
der Orden des Phönix, dann steht der nicht nur so drin, sondern auch
noch als Potter und der Orden des Phönix Harry und der Orden des
Phönix Harry Potter.
Ich glaube, Sie haben verstanden, was da passiert.
Also da wird immer vorne ein Wort weggelöscht und das wird hinten
angesetzt.
Das macht man so lange, bis man einmal durch ist.
Und die Stoppwörter werden ignoriert.
Idealerweise ignoriert man die Stoppwörter.
Sowas wie und, der, des und so weiter.
Weil das braucht man nicht zum Suchen.
Wenn Sie dann später hingehen und so einen Index haben, können Sie
natürlich nicht nur nach Harry Potter und der Orden des Phönix suchen,
sondern Sie können einfach P aufschlagen und da steht dann Phönix,
Harry Potter und der Orden des.
Das macht es einfach ganz schick, wenn Sie manuell in so einem Katalog
suchen müssen.
Manuell suchen ist heute nicht mehr so in, aber vor 40 Jahren war das
noch einigermaßen sinnvoll, sowas zu haben.
Heute kommt das auch noch vor, wenn Sie z.B.
in einem Buch so einen Index drin haben.
Wenn man das hübsch formatiert, sieht es so aus.
Sie haben also hier durch diesen senkrechten Strich quasi den Anfang
der Zeile markiert und sortiert ist das Ganze nach dem großgedruckten
Wort.
Das heißt alphabetisch von Flug nach Schwerter.
Zu lesen ist das jetzt so, der erste Eintrag ist Flug des Phönix, der.
Der zweite Eintrag ist Harry Potter und der Orden des Phönix.
Der dritte ist Klöster, Orden und, und so weiter.
Das heißt Sie können jetzt einfach nach einem beliebigen Wort aus dem
Titel suchen, indem Sie im Alphabet an der Stelle nachschlagen.
Und egal an welcher Stelle im Titel dieses Wort drin stand, Sie finden
den richtigen.
Und jetzt ist die Frage, wie baut man so einen Index möglichst
effizient auf.
Also was man offensichtlich braucht, sind zirkuläre Verschiebungen.
Also wenn man hier jetzt mal als Eingabe dieses Abs.
Integer absolute value hernimmt, dann kommen da diese vier
Verschiebungen raus.
Oder eben formatiert wie vorhin mit diesen senkrechten Strichen, das
hier.
Und jetzt ist die Frage, wie strukturiert man ein Programm, das so
einen Index erzeugt.
Das heißt, wir kriegen eine Liste von Titeln, die müssen wir einlesen,
müssen wir diese Verschiebungen bauen und dann müssen wir den Index
wieder abspeichern.
Und eine mögliche Modellierung, die man da machen kann, ist sowas
hier.
Das sieht ein bisschen wild aus, wir gehen einfach mal stückweise
durch.
Ganz oben gibt es eine Treiberkomponente.
Die ist dafür zuständig, dieses ganze System irgendwie zu steuern.
Die sorgt also dafür, dass die Eingabe gelesen wird und das, was die
Eingabekomponente hier links erzeugt hat, an die richtige Stelle
weitergegeben wird und rumgeschoben.
Und was wir offensichtlich brauchen, ist eine Komponente, die diese
Titel zeilenweise einliest aus der Textdatei.
Wir brauchen eine Komponente, die uns die Verschiebungen, die
Permutationen erzeugt.
Wir brauchen eine, die uns hinterher alle Permutationen sortiert.
Weil unsortiert nützt uns der Quickindex nichts.
Dann müssen wir das Ganze wieder irgendwie speichern in der Datei.
Das ist also, sagen wir mal, die Verarbeitungslogik, die man sich
einfach überlegen kann, wenn man das Problem anguckt und naiv
losläuft.
Irgendwie muss man das Ganze aber auch noch speichern.
Das heißt, es gibt einen Speicher, der diese Titel speichert,
zeilenweise.
Es gibt eine Verschiebeindexspeicherkomponente und eine
Sortierindexspeicherkomponente.
Und was die beiden machen, das erkläre ich Ihnen in ein paar Folien.
Fangen wir mit der Eingabe an.
Die ist ziemlich simpel.
Die liest zeilenweise einfach Titel ein.
Da muss man nicht viel machen.
Und die Speicher, die dann hinterher im Hauptspeicher...
Diese Zeichen, die werden einfach im RAM abgelegt.
War recht kompakt.
Dergestalt, dass wir einfach ein spezielles Zeichen noch einsetzen,
das das Wortende markiert.
Sie können sich vorstellen, wir ersetzen die Leerzeichen durch ein
Sonderzeichen.
Das Sonderzeichen ist backslash 0.
Das können Sie gleich sehen auf der Folie.
Und dann brauchen wir noch einen Index, der uns sagt, wo im Speicher
die einzelnen Zeilen anfangen.
Das sieht dann so aus.
Mit dem Beispiel apps.integer.absoluteValue kriegen wir also irgendwo
im Hauptspeicher diese vier Wörter.
Jeweils getrennt durch dieses Sonderzeichen backslash 0.
Und dann gibt es den Index, der sagt, okay, die erste Zeile oder die i
-te Zeile, die fängt in dieser Speicherzelle an.
Der nächste Titel oder die nächste Zeile aus unserem Eingabedokument,
die fängt hier hinten an.
Die dritte Zeile, die fängt noch weiter hinten an.
Das heißt, es ist relativ kompakt im Hauptspeicher abgelegt.
Das geht einfach fortlaufend weg.
Wir kümmern uns gar nicht darum, dass das mal Zeilen waren oder so.
Das ist sehr, sehr platzsparend.
Und das Einzige, was außer dem Text noch dazukommt, ist eben dieser
Zeilenindex.
Aber das ist ja nur eine Indizierung in der Anzahl der Zeilen von der
Textdatei auf die jeweilige Speicheradresse.
Jetzt müssen wir diese ganzen Verschiebungen erzeugen.
Und als dieses Programm geschrieben wurde vor 40 Jahren oder vor 100
oder noch länger, da war Speicher ein sehr knappes Gut.
Und die Speicherhändler haben gesagt, also ihr könntet für 5
Phantastilliarden noch mehr Hauptspeicher kaufen oder ihr nehmt
einfach für 1000 Dollar noch einen Entwickler, der macht euch eine
geschickte Lösung.
Deswegen gehen wir nicht hin und speichern alle Permutationen im
Hauptspeicher ab, sondern wir überlegen uns ein bisschen was
Intelligenteres dafür, was Speicher spart.
Der Verschieber, der macht jetzt nichts anderes als diese
Permutationen erzeugen.
Und der speichert nicht die Permutationen ab, sondern der sagt quasi,
zu dieser Zeile, die im Arbeitsspeicher steht, ist jetzt dieses Wort
das erste und nicht das erste von der Zeile.
Wenn Sie sich das auf dieser leicht chaotischen Grafik vorstellen
wollen, dieser Zeilenindex, den hatten wir gerade eben schon.
Also die erste Zeile, die fängt hier an.
Und jetzt gibt es einen Verschiebeindex, das ist das, was der
Verschieber erzeugt.
Der sagt, die erste Permutation von dem Iten-Titel, also der, der hier
oben anfängt, die erste Verschiebung, die fängt bei Abs an.
Die zweite Verschiebung vom Iten-Titel fängt bei Integer an.
Die dritte Verschiebung, die fängt bei Abs an und die vierte, die bei
Value.
Das heißt, für die Permutationen kriege ich jetzt nicht einen
Speicheraufwand, wo ich den ganzen Titel nochmal in allen seinen
Permutationen abspeichern muss, sondern ich muss nur die Anzahl der
Permutationen plus die Speicheradressen abspeichern.
Das ist sehr, sehr wenig Speicherbedarf für das, was da anfällt.
Aber man kann natürlich mit diesen zwei Indizes ziemlich einfach
rekonstruieren, wie alle Permutationen sind.
Ziemlich effizient.
Und was wir dann noch brauchen, war ja der sortierte Zugriff auf die
ganzen Permutationen.
Das ist ein bisschen hässlich, wenn man das jetzt jedes Mal ausrechnen
muss.
Deswegen gab es ja noch die Sortierkomponente.
Guckt sich jetzt also an, was haben die ersten beiden Module erzeugt.
Wie sieht es im Ergebnis im Hauptspeicher aus?
Und liest jetzt diese ganzen Verschiebungen ein, sortiert die und
speichert die in sortierte Reihenfolge wieder in den Index.
Das heißt, wir speichern auch jetzt nicht den ganzen Titel mehrfach
ab, sondern wir schreiben uns einfach einen Index, in dem steht, der
erste Eintrag ist folgender Titel in der folgenden Verschiebung und
der zweite Eintrag ist ein anderer Titel in einer anderen
Verschiebung.
Da haben wir jetzt also einen Sortierer.
Da stehen einfach von oben nach unten, alphabetisch sortiert, Zeiger
auf den Verschiebeindex drin.
Und dann kann man, wenn man diesen Sortierungsindex von oben nach
unten auflöst, alle Permutationen alphabetisch sortiert hinschreiben.
Das heißt, wir haben auch da nur einen sehr, sehr kleinen
Speicherbedarf, um alle Permutationen sortiert abzufragen.
Wenn wir das haben, können wir natürlich diese Ausgabekomponente sehr
einfach programmieren.
Man muss nur wissen, wie diese Indizes funktionieren.
Und dann kann man natürlich einfach die ganzen Verschiebungen auslesen
und in der Datei speichern.
Was ist jetzt das Problem?
Das Hauptproblem ist, ich habe Ihnen noch nicht gesagt, wie der
Treiber funktioniert.
Der Treiber ruft diese ganzen Untermodule auf, der behandelt
irgendwelche Fehlermeldungen usw.
Wie die genauen Schnittstellen funktionieren.
Das haben wir uns auch noch nicht angeguckt.
Das würde in einem richtigen Modulführer natürlich drinstehen.
Das lassen wir jetzt aber weg.
Ich glaube, jeder hat verstanden, wie das funktioniert.
Wir haben hinterher im Hauptspeicher die Titel einmal abgespeichert,
plus ein paar Indizes, in denen drinsteht, die zu verschieben, zu
permutieren und zu sortieren sind.
Wir haben da relativ viel Aufwand, was den Speicher angeht.
Aber dieser Entwurf, der hat ein paar Probleme.
Hält jemandem eins auf?
Ich glaube, ich muss mal eine andere Frage stellen.
Möchte jemand ins Freibad gehen?
Okay, also Sie hören mir noch zu, das ist nett.
Hat jemand eine Idee, warum dieser Entwurf ein Problem hat?
Der Kommilitone sagt, alle Module sind irgendwie voneinander abhängig.
Ein bisschen genauer heißt das, es kann sich echt viel ändern und dann
wird es chaotisch.
Also stellen Sie sich vor, jemand kommt auf die Idee, das nicht im
Hauptspeicher abzulegen, so wie gehabt, sondern auf einer Platte.
Oder irgendwie gemischt.
Oder die Wortgröße im Hauptspeicher ändert sich.
Oder sonst was.
Die ganze Speicherstruktur an sich könnte sich ändern.
Wir könnten das noch komprimieren wollen.
Vielleicht kommen andere Zeichensätze.
Vielleicht will aber auch jemand die Stoppwörter, die Thomas vorhin
angerissen hat, ignorieren.
Wenn man das machen will, dann wird es echt übel, weil dann muss man
alles ändern.
Wie dieser Index erzeugt wird und die Verschiebungen funktionieren,
das sind alles Dinge, die man eigentlich hätte kapseln wollen.
Jedes Modul muss jetzt wissen, wie die Datenstrukturen, die eigentlich
internen Datenstrukturen, von den Vorgängermodulen aufgebaut sind.
Wenn man die Ausgabeformate oder die Eingabeformate ändert, die Datei,
aus der gelesen wird und die Datei, in die geschrieben wird, dann muss
man auch in diesem Entwurf nur das erste und letzte Modul ändern.
Aber bei allen anderen Änderungen, sind in diesem Entwurf Änderungen
in allen Modulen nötig.
Wenn Sie irgendeine Entscheidung ändern, man mag das nicht im
Hauptspeicher, man mag das anders sortiert haben, man möchte diese
Indizes nicht, weil Speicher ist jetzt ja übrigens billig geworden,
dann muss man alles anpassen.
Das ist nicht besonders geschickt.
Deswegen haben wir einen alternativen Entwurf dabei.
Da sieht die Benutzrelation nicht ganz so chaotisch aus.
Insbesondere gibt es nicht diese 17-fachen Überkreuzungen, wie gerade
eben.
Die Module sind immer noch die gleichen.
Es gibt einen Treiber, der sich darum kümmert, wie das Ganze abläuft.
Es gibt die Eingabe, den Verschieber, den Sortierer und die Ausgabe.
Aber es gibt nur noch eine Zeilenspeicherkomponente.
Die kümmert sich um den ganzen Ablauf, die Kommunikation mit dem
Hauptspeicher und wie es da drin funktionieren soll.
Da müssen wir uns überlegen, wie sehen da die Schnittstellen aus, wie
könnten da die Schnittstellen aussehen.
Der Zeilenspeicher speichert Zeilen.
Da gibt es ein paar recht einfache Prozeduren.
Es gibt eine zum Schreiben und eine zum Lesen.
Die zum Schreiben hat vier Parameter.
Der letzte, das ist das Zeichen, das tatsächlich gespeichert werden
soll.
Die ersten drei sind die Indizes, die wir vorhin hatten.
R ist die Zeile in dem Eingabedokument.
W ist das Wort in dieser Zeile.
Und C ist das Zeichen.
Das heißt, ich kann jetzt sagen, ich möchte in der siebten Zeile das
dritte Wort, den vierten Buchstaben, aufsetzen.
Und dann kümmert sich das Modul darum, dass es auch abgespeichert
wird.
Natürlich gibt es auch die Gegenmethode, mit der ich dieses Zeichen
wieder herauslesen kann.
Was ich auch noch machen kann, ist abfragen, wie viele Zeilen es
überhaupt gibt.
Das ist für die Steuerung ganz geschickt.
Und natürlich gibt es auch Möglichkeiten, um herauszufinden, wie lang
ist denn eine Zeile und wie lang ist ein Wort.
Damit kann ich einfach durch diese Datenstruktur, die wir jetzt leider
noch im Hinterkopf haben, durchnavigieren, ohne zu wissen, wie die
abgelegt ist.
Insbesondere ist die interne Darstellung völlig beliebig.
Ich kann damit mit diesen paar Methoden jetzt logisch zugreifen auf
das, was ich eingelesen habe.
Aber wie das im Rahmen liegt oder ob das auf der Festplatte ist oder
ob da jemand in den Keller laufen muss und den Aktenschrank aufziehen,
das ist völlig egal.
Und auch wenn ich von dieser Lauf-in-Keller-Lösung auf CD-ROMs
umsteige, ändert sich die Schnittstelle nicht.
Weil ich habe immer noch ein Dokument mit Zeilen und darin gibt es
Wörter und darin gibt es Zeichen.
Das wird immer gleich bleiben, wenn ich bei so einem Quicktext bin.
Sie könnten sich jetzt als Übungsaufgabe überlegen oder sogar ein
Programm schreiben, das all diese Aufgaben übernimmt.
Furchtbar viel Arbeit.
Aber es macht vielleicht sogar ein bisschen Spaß, sich mal diese
Datenstrukturen auszudenken.
Das machen wir jetzt nicht.
Wenn wir eine Zeile oder wenn wir alle Zeilen ausgeben wollen, dann
kriegen wir das mit einer dreifach gescheiterten Vorschleife hin.
Einmal indem wir mit dem R über alle Zeilen drüber iterieren, dann
über alle Wörter in der Zeile und dann über alle Zeichen in dem Wort.
Dann wird das einfach ausgeprintet.
Nach jedem Wort kommt ein Blank und nach jeder Zeile kommt ein 0.
Das heißt, ohne zu wissen, wie das Ding im Hintergrund aussieht,
können wir die Textdatei wieder erzeugen.
Und dementsprechend ist es auch einleuchtend, dass man mit den Daten
arbeiten kann, auch wenn man nicht weiß, wie die im Hintergrund
aussieht.
Jetzt brauchen wir noch einen Verschieber.
Der muss ja damit irgendwie arbeiten.
Was man sich überlegen muss, ist, wie viele Verschiebungen gibt es
denn überhaupt?
Und wie komme ich an diese verschobene Darstellung dran?
Da mache ich mir auch einfach wieder so eine Schnittstelle, die mir
sagt, okay, gib mir mal das so und so fehlende Zeichen vom so und so
fehlenden Wort von der so und so fehlenden Verschiebung zurück.
Das heißt, ich kann mit einer ähnlichen Schnittstelle auf die
ursprünglichen Daten auch auf die verschobenen zugreifen.
Dadurch gibt es auch noch die entsprechenden anderen Methoden, um auf
die Wortanzahl und auf die Zeichenanzahl vom Wort bei den
Verschiebungen zuzugreifen.
Diese Methoden sind analog zu den Standardmethoden, die wir gerade
eben hatten, nur dass sie eben nicht auf den ursprünglichen Titeln
arbeitet, sondern auf den jeweils verschobenen.
Sie arbeitet aber immer noch auf dem Zeilenspeichermodul.
Wir haben da keine Datendopplung oder so was gebaut oder erzwungen.
Der Verschieber kann jetzt natürlich auch noch diesen Index erzeugen.
Der kann die Verschiebung direkt erzeugen, wenn er Lust hat.
Der kann das dann machen, wenn man ihn danach fragt.
Das ist alles völlig der Implementierung überlassen.
Ob das alles en bloc durchgerechnet wird und das nur noch
Abfragemethoden sind oder ob in der Methode tatsächlich noch gerechnet
wird, das ist die Entscheidung, die der Entwickler trifft.
Wenn Sie der Benutzer von diesem Modul sind, sind das völlig egal.
In der Übungsaufgabe können Sie sich auch noch überlegen, wie man das
hinkriegt, diese Stoppwörter rauszufiltern, und zwar ohne sie aus dem
Titel rauszulöschen.
Natürlich kann man hingehen und aus den Zeilen beim Einlesen alle
Stoppwörter rauskillen.
Dann hat man natürlich das Ziel erreicht, dass die ignoriert werden.
Aber man kann den richtigen Titel nicht mehr ausgeben.
Überlegen Sie sich mal, wie man mit so einer Strukturierung die
Stoppwörter ignorieren kann, ohne sie zu löschen.
Was wir noch brauchen am Ende, ist ein Sortierer.
Wie gesagt, dieser Quick-Index ist alphabetisch sortiert, gerade nach
dem jetzt vordersten Wort von der Permutierung.
Und wann dieses Ding sortiert und wie dieses Ding sortiert, und ob es
es überhaupt macht oder erst wenn es abgefragt wird, das ist wieder
völlig beliebig.
Das können Sie von außen nicht sagen, wenn Sie das Modul zur Benutzung
bekommen.
Und es kann Ihnen auch egal sein, ob der Programmierer das so gebaut
hat, dass es sofort komplett durchgerechnet und sortiert wird oder
nicht.
Das ist wahrscheinlich lediglich eine Frage der Laufzeit.
Wenn Sie eine kleine Eingabedatei haben, dann ist es wahrscheinlich
geschickt, die gleich komplett durchzurechnen.
Das kostet ja nicht viel.
Wenn Sie eine riesige Eingabedatei haben, dann ist es vielleicht
geschickt, anzufangen und möglicherweise Anfragen schon zu
beantworten, bevor das Ding fertig ist.
Also an der Stelle kann man einfach spielen mit der Implementierung im
Hintergrund, ohne dass die Benutzer sich darum kümmern müssen, was da
eigentlich passiert.
Was wir noch brauchen, damit das ganze Ding sauber funktioniert, ist
eine Ein- und eine Ausgabe.
Da brauche ich zwei Prozeduren oder zwei Methoden, die die Textdatei
einliest und an den Zeilenspeicher übergibt.
Das ist auch eine Prozedur, die die Verschiebungen aus dem
Zeilenspeicher rausholt und in die Datei druckt.
Aber das ist ja kein Hexenwerk mehr.
Das ist im Prinzip die Vorschleife mit den Verschiebungen drumherum.
Hat jemand das Gefühl, er hat diesen Quick-Index, warum man das so wie
vor 40 Jahren oder so wie vor 39 Jahren strukturiert oder eben nicht
strukturiert?
Verstanden?
Oder hat da jemand Fragen?
Das war mein erstes Projekt als Doktorand hier.
Aber seit es mit dem Elite nicht mehr ist, muss man das schon vor der
Bachelorarbeit gönnen.
Wenn jemand dazu eine Frage hat, haben wir Ihnen noch ein zweites
Beispiel, das vielleicht fünf Jahre neuer ist, mitgebracht.
Das ist aus der Java Klassenbibliothek, aus AWT.
AWT beschäftigt sich mit Grafikobjekten, mit Benutzeroberflächen, mit
GUIs.
Da gibt es Komponenten und man kann jetzt Komponenten ineinander
schachteln und dementsprechend will man natürlich wissen, wie viele
Komponenten sind denn in einer anderen drin?
Wie kann ich noch eine reinstecken?
Wie kann ich eine wieder rausholen?
Aber wie das innen drin funktioniert, das ist Ihnen als Anwender
eigentlich egal.
Ob die Komponente jetzt ein Array hat oder eine Liste oder ein Set
oder irgendwas anderes, das ist völlig irrelevant.
Sie wollen einfach nur darauf zugreifen können.
Sie möchten in die Komponente weitere reinstecken, damit sie
schachteln können.
Sie möchten vielleicht was rausholen.
Aber wie das intern passiert, kann Ihnen ziemlich gleich sein.
Eine Komponente oder ein Container sieht in AWT unter anderem so aus,
dass es hier also Methoden gibt zum hinzufügen, zum rausholen und zum
löschen.
Das heißt, es gibt hier eine Methode addComponent.
Da können Sie in den Container eine andere Komponente reinstecken, zum
Beispiel wieder ein Container.
Und es gibt implizit eine Reihenfolge für diese Schachtelung.
Natürlich kann man auch eine Add-Methode haben wollen, wo man eine
bestimmte Stelle reinsetzt, statt an den Anfang von der Liste.
Die Zugriffsmethoden mit Get, denke ich, sind selbsterklärend.
Man kann einfach Komponenten geben lassen oder alle Komponenten geben
lassen.
Und natürlich kann man auch wieder welche rausschmeißen.
Entweder eine bestimmte, wo wir die halt nochmal übergeben.
Hier die Remove-Component.
Also das ist nicht so, dass hier jetzt irgendwie der Parameter-Quatsch
ist, sondern wenn Sie eine haben, wo eine zweite drinsteckt, dann
können Sie sagen, die zweite, die mach mal bitte raus.
Oder wir greifen direkt auf den Index zu und sagen, die 17.
Komponente, die da drin geschachtelt ist, die wirf mal weg.
Sie könnten sich zur Übung mal überlegen, wie man einen Iterator
schreiben kann, der alle Komponenten da in der gespeicherten
Reihenfolge rausholt.
Und zwar ohne die Get-Components-Methode.
Also nicht rausholen und selber verwalten diese Menge, sondern greifen
Sie mal über diese Get-Methoden drauf zu auf diesem Container und
holen Sie da nach und nach alles raus, was drin steckt.
In Programmiersprachen gibt es auch Gegenstücke zu Modulen.
Bis jetzt waren wir eher in so einer akademischen, theoretischen Ecke.
Ich mache einfach ein Stück Software, dann mache ich ein Kringel drum
und nenne das dann mein Modul.
Das ist natürlich nicht alles.
Ein Modul hat auch eine Entsprechung tatsächlich in Software, in der
echten Implementierung.
Programmiersprachen wie C oder Ada und Modula, falls das jemand von
Ihnen schon in den Fingern hatte, haben spracheigene Mittel, um Module
zu beschreiben.
Die Idee ist einfach, zerlege dein Modul in Schnittstellen und mache
eine Implementierung dazu.
Das trägt natürlich den gleichen Namen.
Das ist alles nicht überraschend, weil das haben Sie schon benutzt.
Das haben Sie beim Programmieren alle schon in den Fingern gehabt.
Sie haben wahrscheinlich nicht Modul dazu gesagt, aber genau das ist
es.
Das heißt, die Schnittstelle, die sagt, was ist öffentlich, was steht
im Modulführer, welche Methoden sind da drin, welche sind sichtbar, es
könnten auch Konstanten sein oder gar ganze Typen.
Und die Implementierung selbst, die Klasse, die das Interface
implementiert, die enthält dann den ganzen privaten Kram, von dem man
nicht wissen muss, wie er funktioniert, wenn man es nur benutzt.
Und die Idee, sowas über Schnittstellen abzubilden, das ist quasi
allen objektorientierten Programmiersprachen gleich.
Sie werden das immer wieder sehen, können sich daran erinnern, ah, das
war das, wo der Landhäuser stundenlang über Module referiert hat.
Das ist ja Quatsch.
Das finden Sie ganz sicher ihr Leben lang in den Programmiersprachen
wieder.
Wenn man das Ganze in C oder C++ implementieren will, was wir gerade
gemacht haben.
Geht schnell.
Okay, der Kollege sagt, es geht ganz schnell.
Was man braucht, sind Klassen oder Module für jedes einzelne von
diesen Modulen, die wir theoretisch gerade besprochen haben.
In C ist es ja so, dass Sie eine Header-Datei brauchen, eine
Implementierungs -Datei.
In der Header-Datei steht drin, wie das Modul aussieht.
Also was sind die öffentlichen Dinge, wie sind die Schnittstellen und
so weiter.
Da haben wir jetzt also für jedes Modul eine Datei.
Verschieber.h, Teilenspeicher.h, wahrscheinlich gibt es auch irgendwo
einen Treiber.h.
Und in der zugehörigen Datei, die mit Punkt C, diese Implementierungs
-Datei, versehen ist, da drin wird einmal die Header-Datei inkludiert.
Das heißt, da weiß man dann, wie das Zeug funktioniert.
Das heißt, das Inklude hier holt sich die Modulbeschreibung von dem
jeweiligen Modul, das da steht.
Unglücklicherweise gibt es keinerlei Überprüfung von Namenspaarungen,
soweit ich weiß.
Das heißt, C ist da ein bisschen darauf angewiesen, dass Sie sauber
arbeiten und dass Sie das auch überall gleich benennen, wenn Sie
dasselbe meinen.
Und wenn man das so macht, dann hat der Übersetzer genügend
Informationen, um nachzuvollziehen, ob das, was man in einem Modul aus
einem Fremden aufgerufen hat, auch tatsächlich da ist.
Was da sein muss, steht ja in den Header-Dateien.
Das heißt, man kann zur Übersetzungszeit schon beweisen, ob richtig
auf dieses fremde Modul zugegriffen wird.
Und ganz nebenbei kann man auch überprüfen, ob man seine eigene
Modulbeschreibung erfüllt.
Wenn Sie sich vorstellen oder daran erinnern, wie das mit dem
Zeilenspeicher war, da haben wir hier mal die Header-Datei.
Die ganzen Methoden oder die Prozeduren, die vorhin auf den Folien
waren, die stehen in dieser Header-Datei drin.
Da haben wir den Rückgabetyp, genauso wie in Java, den Methodennamen
und dann kommen die Parameter.
Das waren genau R für die Zeile, W für das Wort, C für den Buchstaben
und D für den Buchstaben, der gesetzt werden soll in der Sätze-Zeichen
-Methode.
Und oben drüber haben wir einfach den Kommentar.
Das heißt, das ist die technische Darstellung von der
Modulbeschreibung.
Wenn man das sauber gemacht hat, sollte da alles drinstehen, was man
braucht.
Beim Verschieber ist es genauso.
Das heißt, alle Methoden, alle Prozeduren, die wir auf den Folien
vorhin besprochen haben, die stehen in dieser Header-Datei drin.
Hier haben wir jetzt beispielsweise die Methode, die ein Zeichen aus
einer Verschiebung rauszieht.
Also dieser zusätzliche Parameter L hier, der ist für die Verschiebung
gedacht.
Und dann kommt folgende Benutzrelation raus.
Wenn man das Ganze, was wir uns vorhin als Modul angeschaut haben,
implementiert, dann kommt das hier raus.
Wir haben eine Schnittstelle für den Zeilenspeicher und eine
Implementierung.
Also eine Header-Datei, das ist das dunkelblaue, und die
Implementierungsdatei mit Punkt C, das ist das grüne.
Dann gibt es die Eingabe und die Schnittstelle für den Zeilenspeicher,
die benutzt die Implementierung von der Eingabe.
Dann gibt es noch den Verschieber, der wird natürlich auch benutzt.
Dann gab es noch den Sortierer und die Ausgabe.
Und ich hoffe, jetzt sind alle Zeilenspeicher drauf.
Ja, blöd.
Beide sind genau andersrum zu lesen.
Also das Hauptprogramm, sieht man hier, benutzt die Eingabe und
benutzt den Verschieber und benutzt den Sortierer.
Und dementsprechend benutzt auch der Verschieber den Zeilenspeicher
und nicht umgekehrt.
Das hatte ich gerade falsch erzählt.
Was man hier jetzt aber sieht, ist, dass diese Implementierung
Zeilenspeicher nur von der Schnittstelle abhängt und die Eingabe
Implementierung auch nur von der Schnittstelle der Eingabe und der
Schnittstelle des Zeilenspeichers.
Das heißt, man kann jetzt schön von unten nach oben die
Implementierung tatsächlich machen, weil man keine Abhängigkeiten hat
oder nur auf Dinge, die es halt schon gibt.
Ne, ist noch nicht ganz so weit.
Was nicht geht, ist ein Modul mehrfach anzulegen.
Das heißt, ein Paar von Schnittstelle und Implementierung bildet das
einzige Exemplar von so einem Modul in dem Software-System.
Und dementsprechend, wenn man mehrere Exemplare braucht, muss ich halt
eine Kopie davon holen.
Das entsprechend umbenennen und dann ist gut.
Das geht in objektorientierten Sprachen ein bisschen besser, weil da
können wir einfach von einer Klasse mehrere Instanzen erzeugen.
Da müssen wir natürlich nichts mehr umbenennen, sondern da kriegt
einfach die Instanz eine andere Variable und dann ist okay.
Und man kann natürlich diese Schnittstellen in objektorientierten
Sprachen so implementieren, dass man nicht nur eine Schnittstelle
implementiert, sondern mehrere gleichzeitig.
Das nächste, was kommt, ist schon der Vergleich zu dem OO-Teil und den
werden wir dann am Montag anfangen.