“Die Türme von Hanoi” ist eine Aufgabenstellung die in Programmiererkreisen sehr beliebt ist. Erstens ist der Lösungsalgorithmus recht einfach hinzuschreiben, zweitens ist er ein schönes Beispiel für rekursive Methoden, und drittens ist das Ergebnis für die Mitmenschen meist recht beeindruckend, was immer eine wichtige Motivation darstellt. Dementsprechend viele Versionen gibt es daher bereits, wahrscheinlich ähnlich viele, wie es Züge braucht, um 64 Scheiben vom linken Stapel auf den rechten zu legen. Und ungefähr die Hälfte davon kann man sich im WWW anschauen. Warum füge ich der beachtlichen Sammlung also ein weiteres Exemplar hinzu? Das kann ich gleich mit einigen Argumenten rechtfertigen:
Genug. Daher habe ich mich entschieden, eine objektorientierte Lösung zum Problem "Die Türme von Hanoi" anzubieten, komplett mit Aufgabenstellung, OOA, OOD und Implementierung in Java, und das ganze Ding als jar Archiv verpackt fertig zum download. Ich wünsche viel Spaß beim Durchlesen und freue mich auf Feedback.
Der Ausdruck Aufgabenstellung umschreibt ungefähr jenen Teil eines Pflichtenhefts, in dem die Funktionalität des Produkts bis ins Detail abgeklärt wird. Da es immer gut ist zu wissen, was man tun will, bevor man es tut, soll als erstes klar dargestellt werden, was die Aufgabe des Systems (→ der objektorientierten Applikation TowerSimulation) sein soll:
Die objektorientierte Applikation TowerSimulation soll das Problem "Die Türme von Hanoi" für eine bis zehn Scheiben mit Hilfe des rekursiven Algorithmus lösen und die Lösung grafisch ausgeben.
Die TowerSimulation muss nach dem MVC Design Pattern entworfen sein.
Bei "Türme von Hanoi" gibt es 3 gleiche Sockel. Zu Beginn befinden sich alle im Spiel befindlichen Scheiben am Startsockel. Diese Scheiben sollen vom Startsockel zum Zielsockel transferiert werden, wobei immer nur eine Scheibe auf einmal von einem Sockel zu einem anderen bewegt werden darf, und immer nur eine kleinere Scheibe auf einer größeren Scheibe zu liegen kommen darf.
Die Ausgabe soll grafisch erfolgen. Der User soll die Anzahl der Scheiben zwischen 1 und 10 variieren können, wobei 3 der Vorgabewert sein soll. Der User soll auch den Startsockel und den Zielsockel vorgeben können. Schließlich soll der User die Geschwindigkeit vorgeben können, mit der die Scheiben zwischen den Sockeln hin und her fliegen. Außerdem soll das ganze Geschehen mit angenehmer Hintergrundmusik ausgemalt werden. Wenn eine Scheibe von einem Sockel abhebt, soll das typische "Whoosh" Geräusch ertönen, und wenn sie wieder landet, soll sie es mit einem "Klonk" tun. Insgesamt soll alles ungefähr so aussehen, wie in der untenstehenden Grafik. Und das Programm soll als Applet und als Applikation ausgeführt werden können!
Abbildung 1: so will es der Kunde haben
Ok, prima alles klar. Der Kunde weiß was er will, das ist zwar nicht immer so, macht aber die Sache einfacher. Bleibt nur mehr eine Frage:
Der rekursive Algorithmus basiert auf der folgenden Annahme, und dafür wollen wir gleich die Grafik aus Abbildung 1 zitieren: Um die 3 Scheiben vom ganz linken Schober auf den ganz rechten zu kriegen, müssen zuerst die 2 oberen Scheiben auf den mittleren Sockel platziert werden. Dann kann man einfach die letzte und größte Scheibe auf den ganz rechten Sockel legen und zum Schluss die zwei übrigen Scheiben obenauf auf der ganz großen Scheibe am rechten Schober platzieren, schon geschafft. Hier wird oft der Einwand gebracht, dass man ja gar nicht zwei Scheiben auf einmal vom ganz linken auf den mittleren Schober schießen darf, weil das ja gegen die Regeln vom Spiel verstößt! Genau, und das ist jetzt der Punkt, wo der rekursive Algorithmus ins Spiel kommt. Um also 3 Scheiben von ganz links nach ganz rechts zu bringen, muss man zunächst die zwei kleineren Scheiben auf den mittleren Sockel legen. Und das geht genauso wieder: Um 2 Scheiben vom ganz linken Stapel auf den mittleren zu schaufeln, muss man zuerst die oberste auf die ganz rechte Schaufel stapeln, und kann dann die zweite Scheibe auf den mittleren Schober tun, und dann die kleinste Scheibe auf die mittlere in der Mitte legen. Dann legt man die dicke Scheibe auf den ganz rechten Stapel und hat so die erste Hälfte vom ersten Rekursionsschritt erfüllt. Das Problem wird dann gelöst, wenn der zweite Rekursionsschritt noch mal ausgeführt wird, nämlich wenn die beiden Scheiben aus der Mitte nach rechts nachwandern. Das ganze bitte mal mit 3 verschieden großen Münzen nachvollziehen, bis aus dem Häh-Erlebnis ein Aha-Erlebnis wird.
Ok, prima alles klar. Sagen wir, das war verständlich. Bleibt nur mehr eine Frage:
MVC ist ein sogenanntes Design Pattern, bei dem es zum einen darum geht, ein größeres Software System (also ein Programm) in mehrere kleinere und überschaubare Subsysteme zu zerlegen, und zum anderen soll das System so modularisiert werden, dass einzelnen Subsysteme ersetzt werden können, ohne dass andere Teile des Systems davon direkt betroffen sind. Das bedeutet, wenn ein Teil des Systems umgeschrieben werden muss, können alle anderen Teile unverändert werden und ich spar mir viel Arbeit dabei.
Das Wort MVC ist JAVA (Just Another Vague Acronym) und steht für die Anfangsbuchstaben der drei Komponenten, in die ein System durch dieses Design Pattern geteilt wird: Model View Controller.
Das Model repräsentiert oder modelliert die Daten, die vom Programm gespeichert und verarbeitet werden (auf welchem Sockel befinden sich gerade wie viele Scheiben, und wie soll es weitergehn?), die View steht für eine Sicht der Daten, also für die Art und Weise, wie die Daten dem User präsentiert werden. Eine View kann sehr einfach sein und die Daten in Form von reinem Text ausgeben. Eine View für "Türme von Hanoi" könnte die Lösung des Problems, 3 Scheiben vom ersten Sockel zum letzten zu bringen, etwas so ausgeben:
wobei die Zahlen jeweils für den Sockel stehen, von dem eine Scheibe starten oder auf dem die Scheibe landen soll. Eine durchdachtere View kann das ganze aber auch mit einer 3D Animation und vielen interessanten Spezialeffekts machen.
Der Controller schließlich ermöglicht es dem User, die im Model gespeicherten Daten zu manipulieren. Der Controller könnte aus einigen GUI Komponenten wie Textfeldern und Buttons bestehen, mit denen der User sagen kann, dass 4 Scheiben vom mittleren Schober zum linken fliegen sollen, und das ganze bei maximaler Geschwindigkeit und annehmbarer Lautstärke.
So teilt sich das gesamte Programm also in einige Subsysteme:
Abbildung 2: Das MVC Prinzip
Diese einzelnen Komponenten sind in der Regel nur lose aneinander gebunden und können nur über klar definierte Interfaces miteinander kommunizieren (das Interface eines Objekts besteht aus den öffentlichen Methoden und stellt so die einzige Schnittstelle zur Außenwelt dar). Will das Controller-Objekt das Model-Objekt informieren, dass der User einen neuen Startsockel ausgewählt hat, dann sendet es eine einfache Nachricht an das Model:
setStartSockel ( int sockelNummer );
Das Model kann dann mit der Nachricht machen, was immer es für notwendig hält. Meistens führt eine solche Nachricht dazu, dass ein Model seine internen Daten aktualisiert. Da die View dafür zuständig ist, die Daten des Models augengefällig zu repräsentieren, muss das Model jetzt die View benachrichtigen, damit auch die Anzeige aktualisiert werden kann. Also sendet das Model eine Nachricht an die View:
modelDatenUpdated ();
Eine gute View wird daraufhin die Anzeige aktualisieren. Eine reine Textausgabe könnte die Daten in einer einfachen Tabelle ausgeben, eine grafische Ausgabe würde eine bestimmte Anzahl von Scheiben auf einem Sockel materialisieren lassen.
Dieses Interface, über das die Kommunikation der einzelnen Systemkomponenten erfolgt, ist die einzige Vorgabe, an die sich die einzelnen Objekte halten müssen. Wie das Model seine Daten tatsächlich speichert ist für Controller und View irrelevant, während sich das Model kaum interessiert, ob die View die Daten als Strichgrafik über einen 6-Nadeldrucker oder als 3D Wunderwerk auf einem Plasmabildschirm ausgibt. In jedem Fall sendet das Model immer die gleiche Nachricht modelDatenUpdated (), sobald sich etwas in seinen Daten getan hat.
Die drei Systemkomponenten verbergen also alle Implementierungsdetails vor ihren Kollegen und erreichen so einen hohen Grad an Unabhängigkeit voneinander. Die angenehme Konsequenz für den Systementwickler ist, dass er die Komponenten einfach austauschen kann, wenn sich die Gelegenheit bietet. So könnte man für ein Softwareupdate die lahme 2D Version der View einfach durch eine peppige 3DView ersetzen, ohne dass man an Model oder Controller auch nur eine Codezeile verändern müsste. Ebenso könnte man mehrere Views und Controllers verwenden, um dem Kunden ein facettenreicheres System zu liefern:
Abbildung 3: Das MVC Prinzip (nochmals)
Ein Controller bekommt die Aufgabe, Eingaben über die Tastatur zu verarbeiten, ein anderer Controller verarbeitet Eingaben mit der Maus. Beide Controller konfigurieren dabei das gleiche Model über ein gemeinsames Interface. Das Model aktualisiert daraufhin seine Daten und benachrichtigt die Views, damit die Ausgabe auf den neuesten Stand gebracht werden kann. Eine View gibt die Daten als reinen Text aus, die andere View macht eine 3D Grafik draus. So erzeugt die eine View ein einfaches Textadventure während die andere View für Effekte wie bei Warcraft III sorgt.
Ok, prima alles klar. Aber wenn das MVC Konzept so wichtig ist, sollte es sich dann nicht in der Aufgabenstellung etwas deutlicher niederschlagen?
Genau! Mit dem neuen Wissen im Hinterstübchen können wir gleich die Aufgabenstellung etwas verfeinern:
Die objektorientierte Applikation TowerSimulation soll das Problem "Die Türme von Hanoi" für eine bis zehn Scheiben mit Hilfe des rekursiven Algorithmus lösen und die Lösung grafisch ausgeben.
Die TowerSimulation muss nach dem MVC Design Pattern entworfen sein.
Bei "Türme von Hanoi" gibt es 3 gleiche Sockel. Zu Beginn befinden sich alle im Spiel befindlichen Scheiben am Startsockel. Diese Scheiben sollen vom Startsockel zum Zielsockel transferiert werden, wobei immer nur eine Scheibe auf einmal von einem Sockel zu einem anderen bewegt werden darf, und immer nur eine kleinere Scheibe auf einer größeren Scheibe zu liegen kommen darf.
Die Ausgabe soll grafisch erfolgen. Der User soll die Anzahl der Scheiben zwischen 1 und 10 variieren können, wobei 3 der Vorgabewert sein soll. Der User soll auch den Startsockel und den Zielsockel vorgeben können. Schließlich soll der User die Geschwindigkeit vorgeben können, mit der die Scheiben zwischen den Sockeln hin und her fliegen. Außerdem soll das ganze Geschehen mit angenehmer Hintergrundmusik ausgemalt werden. Wenn eine Scheibe von einem Sockel abhebt, soll das typische "Whoosh" Geräusch ertönen, und wenn sie wieder landet, soll sie es mit einem "Klonk" tun. Insgesamt soll alles ungefähr so aussehen, wie in der obenstehender Grafik. Und das Programm soll als Applet und als Applikation ausgeführt werden können!
Neu: Das TowerModel repräsentiert die Daten, hat also 3 Sockel (einen für jeden Schober), wobei sich zu Beginn alle Scheiben am Startsockel befinden sollen. Dem rekursiven Algorithmus folgend schickt das Model dann die Scheiben zwischen den Sockeln hin und her, bis alle Scheiben sich am Zielsockel befinden.
Wenn sich die Daten im TowerModel verändern, dann informiert das Model alle registrierten TowerViews, damit diese ihre Anzeige aktualisieren können.
Die View zeigt 3 Sockel an. Wenn eine Scheibe transferiert wird, soll sie mit dem "Whoosh" Sound abheben und mit dem "Klonk" Sound landen. Die Demonstration soll von angenehmer Musik begleitet werden.
Mit einem TowerController soll der User die Eigenschaften des Models vor der Demonstration und zum Teil auch während der Demonstration beeinflussen können:
| Attribut | Vor Demonstration | Während Demonstration |
|---|---|---|
| Anzahl der Scheiben | x | |
| Startsockel | x | |
| Zielsockel | x | |
| Speed | x | x |
| Start | x | |
| Stop | x |
Ok, prima alles klar. So soll es funktionieren. Aber wie kommt man jetzt von er Aufgabenstellung zu einem objektorientierten System, das vielleicht auch noch funktioniert?
Gute Frage. Genau dafür führt man ein objektorientiertes Design durch:
Nachdem oben das ganze System schon mal untersucht und analysiert wurde, können wir daran gehen, ein objektorientiertes System zu formen (design), das den Vorgaben entspricht. Das lässt sich auf vielerlei Arten machen. Eine einfache Möglichkeit, die hier auch ganz gut funktioniert, ist diese, die Substantiva (Hauptwörter, in der Aufgabenstellung farblich hervorgehoben) aus der Problemstellung raus zu dividieren, und zu testen, ob sie sich im System durch Klassen vernünftig repräsentieren lassen. Wollen mal einen Blick auf die Wörter werfen, in der Reihenfolge, wie sie im Text vorkommen:
Ok, prima alles klar! Damit haben wir schon mal die wichtigsten Klassen des Systems identifiziert. Vielleicht kommen wir nicht ganz damit durch und müssen noch ein paar weitere Klassen hinzunehmen. Aber fürs erste reicht es als roter Faden, an dem wir uns weiterhanteln können. Wie müssen die Klassen jetzt aber zusammengesteckt werden, damit ein richtiges System herauskommt? Gibt es da ein Hilfsmittel?
Genau das gibt es!
Das Klassendiagramm soll dem Designer helfen, sich einen Überblick über seine Klassen im System zu verschaffen und wie diese Klasse zueinander in Beziehung stehen. Die einzelnen Klassen werden im Diagramm durch Rechtecke repräsentiert, in denen zumindest der Name der Klasse steht. Das ist fürs erste ausreichend. Wenn das Systemdesign weiter vorgeschritten ist, weiß der Designer oft schon, welche Attribute und Operationen ein Typ aufweist und kann diese dann auch im Klassendiagramm festhalten, aber dazu später. Das Klassendiagramm für das ganze System ist etwas zu umfangreich. Daher werde ich hier mehrere Klassendiagramme für Teilsysteme wie das Model und die View präsentieren, um den Überblick zu wahren.
Das erste Klassendiagramm beschreibt das ganze System TowerSimulation, das getreu dem MVC Pattern aus drei Subsystemen besteht, dem TowerController, dem TowerModel und der TowerView.
Abbildung 4: Klassendiagramm TowerSimulation
Das erste Diagramm zeigt schon mal ganz klar die Konzeption des Gesamtsystems auf. Die TowerSimulation besteht also aus drei Teilen: TowerController, TowerModel und TowerView. Die Raute am Anfang der Verbindungslinie zwischen TowerSimulation und den anderen Klassen deutet an, dass TowerController, TowerModel und TowerView Teile der TowerSimulation sind und steht damit für den Begriff der Aggregation (oft wird auch der Begriff Komposition verwendet). Die kleine Zahl neben der Verbindungslinie bedeutet, dass eine TowerSimulation jeweils einen TowerController, ein TowerModel und eine TowerView hat.
Neben den Verbindungslinien wird angeschrieben, welche Nachrichten sich Objekte der Klassen senden. Ein kleiner Pfeil gibt an, in welche Richtung die Nachrichten geschickt werden. So informiert der TowerController das TowerModel über Usereingaben, indem er Nachrichten wie setSpeed () oder startDemonstration () sendet. Die Nachricht erzeugt () ist als Aufruf des Konstruktors zu verstehen, wenn ein neues Objekt erzeugt wird.
Die Kreise stehen noch für verschiedene Interfaces, die von den Klassen implementiert werden müssen. So muss die TowerView die Interfaces ScheibenListener und TabelModelListener implementieren, damit Objekte der Klasse Nachrichten zu allen wesentlichen Ereignissen im TowerModel empfangen können.
So verlangt das Interface ScheibenListener von einer implementierenden Klasse, dass die Methoden scheibeGestartet () und scheibeGelandet () definiert werden. Dann sind Objekte dieser Klasse ausreichend vorbereitet, um darüber informiert zu werden, wenn Scheiben im Model umgeschichtet werden.
Ok, prima alles klar. Aber so einfach kann es nicht sein. Wir haben vorhin gesehen, dass zum TowerModel noch die Klassen Sockel und Scheibe gehören, die kommen im obigen Klassendiagramm gar nicht vor. Und was ist mit TowerView und TowerController? Gibt es da auch noch einiges zu gestehen?
Tja, genau so ist es. Die einzelnen Komponenten, die im obigen Klassendiagramm angeführt sind, bilden zum Teil wieder recht komplexe Subsysteme. Durch die Abstraktion aller Teile, die sich mit der grafischen Ausgabe befassen, in eine TowerView, lässt sich das Gesamtsystem einfacher und in einem übersichtlichen Diagramm anschreiben. Die einzelnen Subsysteme müssen wir jetzt noch genauer betrachten. Fangen wir mit dem Model an:
Abbildung 4: Klassendiagramm TowerModel
Das TowerModel hat also 3 Sockel, und auf jedem Sockel können laut Aufgabenstellung 1 bis 10 Scheiben liegen. Das TowerModel hat kein besonders enges Verhältnis zu seinen Scheiben. Dem jeweiligen Startsockel befiehlt das Model, die eingestellte Anzahl an Scheiben zu erzeugen, vielleicht 3, indem es die Nachricht erzeugeScheiben () sendet. Den anderen Sockeln gibt das Model den Auftrag, etwaige vom vorigen Durchlauf übriggebliebene Scheiben zu vernichten, indem es die Nachricht vernichteScheiben () sendet. Während der Algorithmus abläuft, verfolgt das Model einen Plan, wann eine Scheibe von welchem Sockel zu welchen Sockel geschobert werden soll. Wenn gegebenenfalls die oberste Scheibe vom ersten Sockel auf den dritten Sockel muss, dann holt sich das Model mit getObersteScheibe () von Sockel eine Referenz auf die oberste Scheibe und schickte dann diese Scheibe zum dritten Sockel, indem es die Nachricht starten () an diese Scheibe sendet.
Wenn eine Scheibe die Nachricht start () erhält, dann verabschiedet sie sich von ihrem aktuellen Sockel, indem sie ihm die Nachricht scheibeGestartet () zukommen lässt. Der kann sie dann aus seiner Matrikel löschen und sonst seinen eigenen Geschäften nachgehen. Die Scheibe wartet dann die Flugzeit ab, die benötigt wird, um vom Startsockel zum Zielsockel zu gelangen. Ist die Flugzeit verstrichen, dann meldet sich die Scheibe beim Zielsockel an, indem sie ihm die Nachricht scheibeGelandet () sendet. Der Zielsockel nimmt sie dann freundlich in seine Matrikel auf. Damit ist die Sache aber noch nicht gegessen, da ja auch noch andere Teile des Systems über den Flug der Scheibe informiert werden müssen. Daher verarbeiten die Sockel ihre Nachrichten von der Scheibe nicht nur selbst, sondern geben sie auch ans Model weiter, das dann selbst nach Gutdünken weiter verfährt. In unserem Fall wird das Model diese Nachrichten vor allem an die View weitergeben, die dafür Zuständig ist, den Flug der Scheiben zu animieren und akustisch zu untermalen.
Wenn eine Scheibe startet oder landet wird das als Ereignis oder englisch als Event bezeichnet. Informationen über diese Events werden entsprechend in einer Klasse ScheibenEvent gespeichert. Die in diesem Zusammenhang wichtigen Informationen sind, um welche Scheibe genau es sich handelt (Source of Event), von welchem Sockel sie weg- und zu welchem Sockel sie hingeflogen ist. Die Art, solche Ereignisse mit Listener Interfaces und Event Objekten zu verarbeiten, beruhen auf dem Action Design Pattern.
Das Weitergeben von Events von der Scheibe über Sockel und TowerModel bis hin zur TowerView wird als bubble-up bezeichnet, weil die Events von einer Instanz zur nächsten aufsteigen wie kleine Bläschen im sagen wir mal Bier oder so.
Ok, prima alles klar. Getreu dem Spruch "... ein Bild sagt mehr als tausend Worte ..." zeigt das obige Diagramm schon sehr schön, wie das Model im Prinzip funktionieren wird. Aber haben wir nicht vorher von einer Klasse gesprochen, die den Algorithmus implementiert, und gehört die nicht auch zum Model dazu?
Stimmt auffallend! Ja ähm, ich würde sagen, da wird es Zeit für eine kleine praktische Übung! Einfach das Diagramm aus Abbildung 5 ausdrucken und noch eine Klasse Algorithmus einzeichnen, die ebenso zu TowerModel gehört wie Sockel (also mit Raute verbinden).
Ok, prima alles klar. Wer gut aufpasst lernt mehr. Wie sieht es jetzt eigentlich mit dem Controller Subsystem aus?
Das ist schön, wenn man immer die richtigen Fragen zum richtigen Zeitpunkt gestellt bekommt!
Wenn wir uns die Abbildung 1 genau ansehen, so stellen wir fest, dass der Controller aus zwei verschiedenartigen Komponenten besteht, nämlich zum einem aus Buttons, mit denen man die Demonstration starten und stoppen kann, und zum anderen aus einer komplexeren Komponente, mit der man mittels Slider und Textfeld den Wert von verschiedenen Modelattributen konfigurieren kann, nämlich Scheibenanzahl, Startsockel, Zielsockel und Animationsgeschwindigkeit. Die Buttons finden wir in der API (JButton), während wir die andere Komponente selbst basteln müssen. Dieses neue GUI Bean habe ich ConfigureFieldPanel getauft, weil man damit so schön einen ganzzahligen Wert konfigurieren kann. ConfigureFieldPanel ist eine abstrakte Klasse, in der das Interface vorgegeben wird, damit man dann verschiedene Implementierungen für verschiedene Designs vornehmen kann, zum Beispiel mit JScrollBar statt mit JSlider. Werfen wir zunächst einen Blick auf das Klassendiagramm für das GUI Bean SliderFieldPanel, das die abstrakte Klasse ConfigureFieldPanel erweitert.
Abbildung 5: Klassendiagramm SliderFieldPanel
Ein SliderFieldPanel ermöglicht es also dem User, einen Wert zu konfigurieren. Das alleine reicht aber noch nicht weit! Es hilft nicht viel, wenn der User am SliderFieldPanel den Wert 5 einstellt, denn was sich der User tatsächlich erwartet ist, dass im Model die Anzahl der Scheiben auf 5 eingestellt wird. Der Wert, den unser GUI Bean speichert, ist also an eine Eigenschaft von TowerModel wie die Scheibenanzahl gebunden. Daher spricht man in so einem Fall von einer "bound property". Wenn der User einen Wert einstellt, dann muss unser Bean alle betroffenen informieren, dass sich sein Wert verändert hat, sonst macht es keinen Sinn. Woher soll nun das arme Bean aber wissen, wer von seinen Zustandsänderungen alles betroffen ist? Nun, wenn sich das Model für Änderungen der Usereinstellungen interessiert, dann muss es sich beim SliderFieldPanel als Empfänger von Veränderungsnachrichten registrieren. Damit der Systementwickler nicht das gesamte Registrierungs- und EventHandling System selbst schreiben muss, gibt es in der API bereits eine vorgefertigte Klasse PropertyChangeSupport, die genau diese Funktionalität übernimmt. Daher verwendet unser SliderFieldPanel Bean ein PropertyChangeSupport Objekt, um alle Interessierten Listener zu registrieren und im Falle einer Veränderung des "bound value" zu benachrichtigen. In unserem Fall sendet das GUI Bean aber seine Nachrichten nicht direkt an das Model sondern an den hier zwischen geschalteten TowerController, der die Nachrichten aufarbeitet und etwas verfeinert ans Model weitergibt:
Abbildung 6: Klassendiagramm TowerController
Ok, prima alles klar. Der TowerController registriert sich beim ConfigureFieldPanel als PropertyChangeListener und wird im PropertyChangeSupport vom ConfigureFieldPanel gespeichert. Wenn sich jetzt der bound value im ConfigureFieldPanel ändert, dann informiert der PropertyChangeSupport alle registrierten Listener, in diesem Fall nur den TowerController. Der TowerController überprüft dann, vom welchem ConfigureField das Ereignis stammt, weil ja Scheibenanzahl, Speed, Startsockel oder Zielsockel verändert worden sein kann. Zu guter letzt sendet dann der Controller eine entsprechende Nachricht an das Model, damit die Systemdaten aktualisiert werden können.
Genauso ist es! Die Nachrichten, die der Controller an das Model senden kann, sind bereits in Abbildung 4 aufgelistet worden. Jetzt fehlt uns nur mehr das TowerView - Subsystem!
Auch TowerView erweitert JPanel, damit sie als am Fenster der Applikation angezeigt werden kann. Das TowerView Subsystem besteht aus zwei weiteren Klassen. Das eine ist ImagePanel, eine Subklasse von JPanel, und kann dafür verwendet werden, an einer bestimmten Bildschirmposition ein Bild anzuzeigen, das von ImagePanel als ImageIcon gespeichert wird. ScheibenPanel ist eine speziellere Version von ImagePanel mit der zusätzlichen Fähigkeit, sich über den Bildschirm zu bewegen. Objekte dieser Klasse werden von der View verwendet, um den Flug der Scheiben von einem Sockel zum anderen nachzustellen. Daher hat TowerView drei Objekte vom Typen ImagePanel (für die drei Sockel) und je nach Bedarf bis zu 10 Objekte vom Typen ScheibenPanel, eines für jede Scheibe im Spiel.
Die Aufgabe der View ist es, um das zu wiederholen, den Zustand des Models wiederzugeben. Dazu gehört auch, dass die View über jede Änderung im Model informiert werden kann. Darum implementiert die View auch das Interface TowerModelListener, um alle Methoden parat zu haben, die vom Model aufgerufen werden, sobald sich eines seiner Felder ändert. Ebenso wird das Interface ScheibenListener implementiert, welches die View in Stande setzt, alle Nachrichten, die von Scheiben abgesendet werden, zu empfangen.
Abbildung 7: Klassendiagramm TowerView
Ok, prima alles klar. Damit ist der Aufbau des gesamten Systems bis auf die letzte Klasse nachvollziehbar geworden. Und da leisten die Klassendiagramme wirklich einen tollen Dienst. Aber mir ist noch ziemlich unklar, wie jetzt die einzelnen Objekte genau interagieren, wenn es zum Ernstfall kommt, und der User den Startbutton klickt.
Genau. Die Klassendiagramme skizzieren das System zur Designzeit. Um zu beschreiben, wie sich die Objekte verhalten sollen, wenn das Programm gestartet wurde, brauchen wir eine neue Diagrammart:
Mit Interaction Diagrammen kann das Verhalten der Objekte eines Systems modelliert werden mit Schwerpunkt auf die Art und Weise, wie die Objekte miteinander umgehen, welche Nachrichten sie austauschen, und vor allem wann sie das alles machen. Weil sich das nicht nur sehr komplex anhört sondern tatsächlich auch sehr komplex und vielschichtig ist, gibt es zunächst zwei Arten von Interaction Diagrammen: das Collaboration Diagramm mit Schwerpunkt: welche Objekte arbeiten zusammen, und das Sequence Diagramm mit Schwerpunkt: welche Nachrichten werden wann an welches Objekt gesendet, und wie reagiert dieses Objekt darauf.
Da unser "Türme von Hanoi" System noch recht einfach ist, unterscheidet sich das Collaboration Diagram nicht sonderlich vom Klassendiagramm, bringt auf jeden Fall keine neuen Einsichten und wird in dieser Diskussion daher übergangen.
Schade.
Umso wichtiger ist für uns aber das Sequence Diagramm, in dem ganz detailliert und ohne Auslassung der gesamte Algorithmus aufgezeichnet wird, der durchexerziert werden muss, um die aktuelle Scheibe vom Sockel A zum Sockel B zu schicken.
Wie gesagt geht es hier jetzt mehr um die Objekte als um die Klassen, aus denen die Objekte erzeugt werden. Daher kommen in diesem Diagramm viele neue Symbole vor. Objekte werden ähnlich wie Klassen durch Rechtecke dargestellt. Damit man Objekte aber nicht mit Klassen verwechseln kann werden die Objekte unterstrichen. Außerdem sehen wir, das die Bezeichnung der Objekte durch einen Doppelpunkt in zwei Teile getrennt wird. Rechts steht der Typ des Objekts, der dem Namen der zugrunde liegenden Klasse entspricht. Links steht der Name der Referenz, die auf das jeweilige Objekt zeigt. Wenn der Referenzname unwichtig ist, zum Beispiel wenn von diesem Typen ohnehin nur ein Objekt im System existiert, dann kann man den Referenznamen auch getrost weglassen und nur den Doppelpunkt hinschreiben.
Das Sequenzdiagramm wird von oben nach unten gelesen. Die punktierten Striche, die von den Objekten abwärts ragen, zeigen an, welcher Bereich des Diagramms sich auf dieses Objekt bezieht. Wenn das Objekt gerade aktiv in den Prozess einbezogen wird, dann wird die punktierte Linie durch ein Rechteck ersetzt.
Die Pfeile in horizontaler Richtung symbolisieren Nachrichten, die Objekte untereinander versenden. Die Nachrichten werden in Pfeilrichtung gesendet, und um welche Nachricht es sich genau handelt kann man von der Beschriftung der Pfeile ablesen. Die punktiert gezeichneten Pfeile geben an, wann eine Methode an den Aufrufer zurückgibt, also die Programmkontrolle am Point of Call fortsetzt.
Abbildung 8: Sequenzdiagramm Scheibenflug
Wenn man das Sequenzdiagramm fleißig von oben bis unten durchliest, weiß man hinterher genau, was geschehen muss, damit eine Scheibe von Sockel A startet und auf Sockel B landet.
Ok, prima alles klar. Und das ganze Elaborat wird so oft wiederholt, bis alle Scheiben vom Startsockel am Zielsockel angelangt sind. Gesteuert wird das ganze wohl von der Klasse Algorithmus, die den rekursiven Algorithmus implementiert. Aber eines ist seltsam: anscheinend gibt die TowerView einfach wieder die Programmkontroller zurück, ohne irgendwas zu unternehmen, dabei sollte hier doch eine Animation zeigen, wie die Scheibe vom Startsockel zum Zielsockel fliegt!
Genau! Sehr gut beobachtet. Diesen Teil habe ich noch aus dem Diagramm rausgelassen, weil er für sich wieder ein eigenes Sequenzdiagramm verdient hat. Was also genau passiert, wenn die Nachricht scheibeGestartet () an die TowerView gesendet wird, schauen wir uns später im Abschnitt zur Animation an. Zuvor wollen wir aber noch einen raschen Blick darauf werfen, wie der rekursive Algorithmus umgesetzt werden kann:
Auf welchen Überlegungen der Algorithmus beruht, wurde oben schon dargestellt. Nach einer kurzen Zusammenfassung wollen wir das ganze in eine Methode zusammenpacken.
Voraussetzung: n Scheiben sollen vom Sockel A auf Sockel C gelegt werden, wobei Sockel B als Zwischenablage verwendet werden kann.
Abbildung 9: n Scheiben sollen von Sockel A über Sockel B nach Sockel C verlagert werden
Will man 3 Scheiben von A nach C legen, dann muss man zunächst die oberen 2 Scheiben von A nach B legen, dann die letzte und größte auf C legen, und dann die zwei übrigen von B nach C nach legen. Gut, da man nicht die oberen beiden Scheiben gleichzeitig von A nach B legen kann, muss man hier wieder die selbe Vorgangsweise wählen: Zuerst nur die oberste Scheibe von A nach C, dann die verbleibende mittlere Scheibe von A nach B und dann die kleine von C zurück auf B. Und das ist genau der rekursive Algorithmus, und hier kommt mal ein Pseudocode für die dazugehörende rekursive Methode, in der 3 Fälle unterschieden werden müssen:
Und hier das ganze nochmals, in kompilierbarer Form:
public void draw ( int n, int start, int ziel, int tmp ){
if ( n > 1 )
draw ( n-1, start, tmp, ziel );
move ( start, ziel );
if ( n > 1 )
draw ( n-1, tmp, ziel, start );
}
Dabei ist move eine Methode, die tatsächlich eine Scheibe von start nach ziel legt.
Ok, prima alles klar. Diese rekursive Methode sortiert also die Scheiben vom Startsockel auf den Zielsockel. Vorhin haben wir aber gesagt, dass die TowerView ein animiertes ScheibenPanel zeigen soll, das vom Startsockel zum Zielsockel schwebt. Hier haben wir eine rekursive Methode, die sehr schnell durchgeführt wird. Wie werden jetzt der rekursive Algorithmus des Models und die fliegenden Scheiben der TowerView synchronisiert?
Genau! Das ist so: Wenn der User die Demonstration startet, indem er den Startbutton klickt, sendet der Controller dem Model eine entsprechende Nachricht. Daraufhin startet das Model einen neuen Thread, also einen Programmteil, der selbständig und parallel zu den anderen Programmteilen abläuft. Dieser Thread führt den rekursiven Algorithmus durch und ruft von den jeweiligen Scheiben, die bewegt werden müssen, die Methode starten () auf. Was da passiert, können wir aus dem Sequenzdiagram in Abbildung 8 ablesen. Ein ScheibenEvent wird erzeugt und an den Sockel geschickt, auf dem die Scheibe ruht. Der gibt das Event weiter an das Model, von hier geht es weiter an die TowerView. Die View hat, wie wir sehen werden, einen eigenen Programmteil, der für die Animation verantwortlich ist. Wenn der Algorithmus Thread die View informiert hat, kehrt er sofort wieder zurück zu Methode starten () der Klasse Scheibe. Hier wird er in den Sleeping State versetzt, der genau solange dauert, wie ein ScheibenPanel Objekt der View braucht, um vom Startsockel zum Zielsockel zu fliegen. Dann wacht der Thread wieder auf, informiert alle, dass die Scheibe am Ziel angekommen ist, und führt weiter seinen Algorithmus aus.
Ok, prima alles klar. Steht eh alles im Sequenzdiagramm. Jetzt bin ich aber schon sehr darauf gespannt, wie die Animation in der View funktioniert.
Fein, das kommt jetzt nämlich gleich als nächstes dran.
Weiter oben wurde bereits angedeutet, dass die TowerView Objekte der Klassen ImagePanel und ScheibenPanel für die grafische Repräsentation der Daten verwendet. Das Wechselspiel zwischen TowerView und ihren Komponenten visualisiert Abbildung 10.
Eine Animation soll beim Betrachter die Illusion einer Bewegung erzeugen. Das wird erreicht, indem in rascher Folge Bilder gezeigt werden, die sich immer um ein weniges unterscheiden. Auf diesem Konzept beruht nicht nur der Kinofilm Matrix 3 sondern auch unserer TowerView hier. Alle 50 ms wird die Grafik der View neu gezeichnet. Ein ScheibenPanel, das gerade zwischen zwei Schobern unterwegs ist, wird dabei jedes Mal um ein kleines Stückchen weiter vorne gezeichnet, was den Eindruck ergibt, es würde über den Bildschirm flitzen. Zur Realisierung wird ein Objekt der Klasse Timer verwendet, das in ziemlicher Regelmäßigkeit alle 50 ms ein ActionEvent erzeugt und eine actionPerformed () Nachricht an alle registrierten ActionListener sendet, in unserem Fall ist das die TowerView. In Reaktion darauf sendet die TowerView die animate () Nachricht an das ScheibenPanel. Damit weiß das ScheibenPanel Objekt, dass es wieder an der Zeit ist, ein Stückchen weiter zu rücken, und berechnet seine neuen Koordinaten. Im nächsten Schritt wird die repaint () Nachricht an alle Beteiligten gesendet, die sich daraufhin neu zeichnen, was nur im Falle des gerade fliegenden ScheibenPanels zu einer Veränderung der Lage führt, derweil es auf seinen Zielsockel zurast.
Wie man in Abbildung 10 sieht, ist der Kern der Animation in den Methoden losfliegen () und animate () implementiert. Die Methode losfliegen () hat die Aufgabe, den Kurs für den gesamten Flug zu berechnen. Auf ihre Funktionalität werden wir gleich genauer eingehen. Die Methode animate () hat dann die Aufgabe, diesem Kurs zu folgen und in jedem Animationsschritt die neuen Koordinaten zu berechnen.
Die Methode animate () wird allerdings viel öfter ausgeführt, als im Diagramm angedeutet ist, nämlich etwa 20 mal pro Sekunde. Aber das würde zuviel Platz im Diagramm benötigen, daher musste es bei einem symbolischen Wert bleiben.
Wenn der Algorithmus Thread die Nachricht scheibeGestartet () sendet, such TowerView die entsprechende Scheibe aus ihrer Liste, indem sie die Größen der Scheibe vergleicht. Wenn also die kleinste Scheibe im Model gestartet ist, sucht die View ihre kleinste Scheibe raus, indem sie die Methode findScheibeBySize () ausführt. Als nächstes wird der typische "Whoosh" Sound abgespielt, den man immer hört, wenn eine Scheibe startet. Dann sendet die View die losfliegen () Nachricht an das ScheibenPanel, damit der Kurs berechnet werden kann. Sobald die Scheibe im Model gelandet ist, erhält die TowerView die scheibeGelandet () Nachricht, damit der typische "Klonk" Sound abgespielt werden kann, der untrennbar mit einer landenden Scheibe verbunden ist.
Wenn alle Scheiben am Zielsockel angelangt sind, sendet der Algorithmus Thread die demonstrationStopped () Nachricht, damit die View den Timer stoppen kann, der dann von seinen 50 millesekündlichen Benachrichtigungen ablässt.
Abbildung 10: Sequenzdiagram Animation
Ok, prima alles klar. Das Model informiert die View über alle möglichen Zustandsänderungen, und die View repräsentiert dann die Daten grafisch bzw. akustisch, wie es sich gehört. Wie berechnet das ScheibenPanel jetzt aber noch seinen Kurs?
Stimmt, das ist im Prinzip der letzte Punkt, der in der Diskussion noch fehlt. Sehen wir uns dafür mal genau an, welchen Weg die Scheibe zurücklegen muss:
Der Flug eines ScheibenPanels besteht aus drei Phasen:
Abbildung 11: Die Phasen des Scheibenfluges
Was das ganze etwas komplizierter macht, ist, dass das ScheibenPanel eine bestimmte Zeit für diesen Weg brauchen muss. Diese Zeit wird vom User vorgegeben, wenn er die verschiedenen Geschwindigkeiten am Controller einstellt. Die maximale Flugzeit beträgt 3000 ms, oder 3 Sekunden. Für jeden zusätzlichen Geschwindigkeitspunkt, den der User dem System abverlangt, verkürzt sich die Flugzeit um 250 ms. Und das muss die Methode losfliegen () ins Kalkül ziehen, wenn die Scheibenroute berechnet wird.
Zunächst wird der gesamte Flugweg berechnet, der sich aus den Wegen der Phase 1, 2 und 3 ergibt. Das könnte den Wert von 780 Pixel ergeben. Dann wird die Zeit berechnet, die zur Verfügung steht, das sind 3000 ms minus 250 ms multipliziert mit Geschwindigkeitspunkten. Haben wir zum Beispiel 2000 ms zur Verfügung, dann können wir uns ausrechnen, dass in dieser Zeit 40 Animationsschritte durchgeführt werden (20 pro Sekunden, alle 50 ms einer). Das ScheibenPanel muss also 780 Pixel in 40 Animationsschritten zurücklegen, das ergibt eine Geschwindigkeit von 19.5 Pixel pro Animationsschritt.
Damit ist die Geschwindigkeit klar, Startsockel und Zielsockel sind auch klar, und das ScheibenPanel kann auf den Weg geschickt werden, indem ihm die setMoving ( true ) Nachricht gesendet wird. Am Ende der Reise wird ihm die setMoving ( false ) Nachricht hinterbracht, damit es nicht durch den Fußboden abhaut.
Ok, prima alles klar. Das ist ja alles viel einfacher, als ich erwartet habe. Aber jetzt würde ich gerne das Programm antesten, um zu sehen, wie das Ergebnis der Planung aussieht. Und auch wenn alles sehr klar ist, würde ich gern noch einen Blick auf den Quelltext werfen, bevor ich selbst versuche, das objektorientierte Design zu implementieren.
Klar, kein Problem. Die Applikation wurde in Java implementiert. Daher gibt es das ganze Programm als jar Archiv zum Download. Im Archiv befinden sich die kompilierten Klassen zum Ausführen und die .java Dateien zum Schmökern. Außerdem gibt es den Quelltext noch gezippt zum Download. Zu finden ist das alles auf der Downloads Seite. Und hier ist ein Link zum Applet, um schon mal auszuprobieren, wie der Computer 10 Scheiben bei maximaler Geschwindigkeit vom Starksockel zum Zielsockel schicken und dabei auch noch tolle Musik abspielen kann. Das ganze System kommt übrigens auf 2141 Zeilen in 19 Klassen. Die Bilder für die Sockel und die Scheiben wurden mit der 2D API von Java erzeugt. Ich hoffe, der kleine Artikel hat allen viel Spaß gemacht und freue mich auf Feedback im TowerBook.