Blog:
In Softwaretechnik I und Programmieren II haben Sie von Entwurfsmustern (Pattern) gehört. Pattern sind bewährte Templates für Softwareentwürfe, die wiederkehrende Probleme der Softwareentwicklung auf bewährte Art und Weise lösen. Z.B. ist das Command-Pattern ein bewährtes Verhaltenspattern, dass sich auch häufig in Spielen anwenden lässt.
Viele Teams versuchen daher möglichst viele solcher Patterns in Software-Entwürfen zu berücksichtigen. Obwohl gut gemeint, tut man dabei manchmal etwas des Guten zu viel. Ich nenne das auch manchmal - nicht ganz Ernst gemeint - in “objektorientierter Schönheit” sterben. Aus meiner Erfahrung ist es für Unerfahrene oft wesentlich zielführender bewusst Anti-Pattern zu vermeiden. Die resultierenden Software-Entwürfe folgen dann häufig automatisch solchen Pattern, die Sie insbesondere in Softwaretechnik I kennen gelernt haben.
Insbesondere die folgenden Anti-Pattern
tauchen im Modul Webtechnologie immer wieder in Softwareentwürfen auf. Diese sollten Sie - wenn irgendwie möglich - von Anfang an vermeiden. Es kommen dann meist ganz ansehnliche Softwarentwürfe dabei heraus.
Dies ist vielleicht der Kardinalsfehler schlechthin in objektorientierten SW-Entwürfen. Vermeiden Sie Abhängigkeiten zu spezifischen Klassen. Also eine Klasse X
die z.B. wie folgt entworfen wurde:
Es wäre besser Sie würden die Klasse X
wie folgt entwerfen:
Dann können Sie das System nämlich einfach durch zusätzliche Klassen erweitern, ohne die Klasse X
für jede Erweiterung anpassen zu müssen. Dafür wurde Objektorientierung ja erfunden! In diesem Beispiel hier erweitert die Klasse Z
den ursprünglichen Entwurf. Dies können z.B. weitere Spielfiguren oder Power-Ups sein.
Stellen Sie Ihre Software-Entwürfe also von Anfang an auf Erweiterbarkeit ein.
Häufig sind Entwürfe folgender Art zu finden.
D.h. die Klassen Entity
und PowerUp
sind reine Datenkapseln. Beide Klassen haben keine Methoden und damit kein eigenständiges Verhalten.
Das Verhalten von Objekten der Klasse Entity
und PowerUp
muss also in der Klasse Model
zentral realisiert werden. Ein Objekt (das Model) macht dann für ein Objekt etwas. So etwas sollten Sie tunlichst vermeiden. Weniger objektorientiert geht kaum. Der Gameloop der Methode loop()
wird dadurch unnötig komplex, lang und schwer zu erweitern. Erweitern Sie das Model um zusätzliche Klassen, müssen Sie deren Verhalten dann nämlich auch immer zentral im Gameloop anpassen. Der Weg zum “Spaghetticode” ist dann nicht mehr weit. OK, Sie entwickeln ja objektorientiert. Kein Spaghetticode, sondern Komponentenlasagne.
Vermeiden Sie Klassen, die reine strukturierte Datentypen sind (d.h. keine Methoden haben). Das geht nicht immer, aber hinterfragen Sie solche Klassen in einem Entwurf immer kritisch. Finden Sie kein gutes Argument, machen Sie es anders. Gute Argumente sind übrigens nicht:
Versehen Sie daher Entity-Klassen immer mit einem Verhalten, dass sich aus dem Spielkonzept ableiten lässt. Benennen Sie diese Verhaltensmethoden auch entsprechend. Es ergeben sich auf diese Weise sehr gut lesbare und nachvollziehbare Software-Entwürfe.
Häufig finden sich auch solche Entwürfe.
Ein Spiel besteht also aus mindestens einem Spieler (Player
) und Bällen (Ball
). Ein Spieler interagiert mit einem Ball
. Das lässt sich vermutlich problemlos aus dem Spielkonzept so ableiten. So weit so gut.
Stellen wir uns folgende Frage: Ist die Klasse Game
oder die Klasse Player
nun die primäre und verantwortliche Instanz mit dem Ball zu interagieren? Beides ist möglich und vermutlich auch gut aus dem Spielkonzept ableitbar und somit gut begründbar. Man sollte sich daher auf eine einheitliche Vorgehensweise im Projekt festlegen. Da dies aber häufig unbewusste Entscheidungen sind, passiert gerne folgendes: Ein Teil der Spiellogik wird durch die Klasse Game
und der Rest durch die Klasse Player
abgewickelt. Insbesondere dann, wenn die Klassen verantwortlich von unterschiedlichen Personen implementiert werden. Die einen haben eben eine Präferenz dafür das der Spieler mit dem Ball eigenverantwortlich interagiert und die anderen, dass das Model vorgibt wie der Spieler mit dem Ball zu interagieren hat. Laisez-faire vs. Kontrolle. Beides ist grundsätzlich möglich.
Versuchen Sie immer nur einen Weg entlang von Assoziationen vom zentralen Model zu allen Spielentities vorzusehen. Wenn Sie eigenverantwortlich interagierende Entities bevorzugen, kann man das bspw. so machen:
Wenn Sie eher Kontrollfreak sind und dem Model zentral die Kontrolle geben wollen, präferieren Sie vermutlich eher folgenden Ansatz:
In beiden Fällen ist jedoch klar in welchen Klassen die Logik zu implementieren ist und beide “Stile” vermischen sich nicht. Im oberen Fall hätten Sie aufgrund der zwei Pfade vom Model zum Ball
allerdings zwei Möglichkeiten gehabt. Schöner Nebeneffekt dabei: Sie erhalten automatisch weniger sich kreuzende Assoziationen und die Darstellung des UML-Klassendiagramms wird automatisch übersichtlicher.
Nicht immer lassen sich Dreiecke und Zyklen vermeiden. Aber hinterfragen Sie solche Dreiecke und Zyklen in einem Entwurf kritisch. Finden Sie kein gutes Argument, machen Sie es anders. Gute Argumente sind übrigens nicht die bereits im Anti-Pattern “Klassen ohne Verhalten” genannten.
Noch schlimmer sind Zyklen, die schnell entstehen, wenn Kenntnisbeziehungen in beide Richtungen zwischen den Entities bestehen und Dreiecksbeziehungen eingegangen werden. Z.B. so:
In solchen Fällen, können Sie vom Game
zum Player
navigieren und vom Player
zum Ball
und vom Ball
wieder zum Ausgangspunkt Game
. So haben Sie schnell Endlosschleifen gebaut. Auch dies lässt sich durch die Vermeidung von Dreicksbeziehungen konstruktiv vermeiden.
Auch dieses Pattern ist häufig anzutreffen und eigentlich auch nicht komplett zu vermeiden. Es tritt vermutlich bei Ihrer zentralen Game
Klasse auf (also hier die Klasse X
). Trotzdem sollten Sie sich dessen bewusst sein. Sehen Sie sich folgendes Klassendiagramm an:
Die Klasse X
hat also die meisten Ein- und Ausgänge. Das an sich ist noch nicht problematisch (mal abgesehen von den Dreiecksbeziehungen).
Wenn ich so etwas sehe, frage ich normalerweise wer für die Klasse X
verantwortlich ist? Folgende Antworten will ich dann nicht hören, weil diese untrüglich dafür sind, dass das Team in Probleme rennen wird:
X
ist aber nicht kompliziert.”A
bis F
folgen, also Tom die ganze Arbeit hat, obwohl er ja nur eine Klasse betreut).Wie gesagt, komplett lässt sich dies nicht vermeiden, aber es sollte eine klare Verantwortlichkeit für die Klasse X
im Team existieren. Und derjenige oder diejenige Verantwortliche sollte massiv darauf drängen, dass der Entwurf etwas abgeändert wird und Entities eigenverantwortlicher miteinander interagieren dürfen (siehe Anti-Pattern 3). Also z.B. so:
Dies macht den Entwurf auch automatisch besser nachvollziehbar.
Häufig werden Assoziationen als Datenfelder gedoppelt. Dies ist (meist) ein reines Dokumentationsproblem und beeinträchtigt den eigentlichen Software-Entwurf nicht weiter. Es tritt meist dann auf, wenn Sie nicht berücksichtigen, dass Assoziationen im Code als Datenfelder umgesetzt werden müssen. Wenn Sie also so etwas hier modellieren
dann machen Sie sich das Leben doch einfacher und wenden Sie UML einfach konsequent an.
Auch dies ist ein reines Dokumentationsproblem, aber extrem häufig anzutreffen. Viele hängen der irrigen Annahme an, dass ein kompletter Software-Entwurf in einem einzigen UML-Klassendiagramm abzubilden ist. Das ist ab einer gewissen Größe kaum sinnvoll möglich und vor allem nur noch mit abstrus kleinen Schriftgrößen realisierbar.
Sehen Sie sich bitte folgendes UML-Klassendiagramm an (dieses Beispiel ist eigentlich noch harmlos im Vergleich zu dem was in Vorjahrgängen so alles abgegeben wurde!) und stellen Sie sich die Frage, wie übersichtlich Sie dies finden? Wie häufig mussten Sie das Diagramm ansehen, um den Model, den View und den Controler-Teil zu identifizieren? Haben Sie das auf Anhieb erkannt oder brauchten Sie mehrere Anläufe? Mussten Sie etwa zoomen?
Wäre es für Sie nicht einfacher, wenn Ihnen jemand den Sachverhalt in kleinere Häppchen unterteilt? Z.B. indem man Ihnen einleitend erklärt, dass das Spiel grundsätzlich nach dem MVC-Prinzip aufgebaut ist und Ihnen erläutert wird, welche grundsätzlichen Verantwortlichkeiten die Klassen Game
, View
und Controler
haben.
Genau. Dieser jemand sind Sie! Unterteilen Sie einen Softwareentwurf immer in kleine, nachvollziehbare und für sich einzeln verständliche Häppchen. Gelingt Ihnen das nicht, ist vermutlich der Entwurf “murks”. Überarbeiten Sie dann den Softwareentwurf, so ärgerlich das sein mag!
Sie könnten dann erklären, dass sich das Model
grundsätzlich aus Spielern (Player
) und Bällen (Ball
) bildet, deren Gemeinsatzkeiten in der Klasse Entity
gebündelt werden.
Hilfreich ist sicher auch zu wissen, dass unterschiedliche Arten von Spielern vorgesehen sind. Nämlich HumanPlayer
und KI-Gegner (AIPlayer
), die gegebenfalls Zugriff auf eine Tensorflow-Engine haben, um komplexe Verhaltensweisen mit neuronalen Netzen abbilden zu können (OK, wir fangen hier etwas an zu spinnen, aber es ist ja nur ein Beispiel).
Ähnlich aufgebaut sind die Bälle mit denen gespielt wird. Sie leiten sich von einer Basisimplementierung Ball
ab und können unterschiedliches Verhalten zeigen (z.B. Brennen, oder etwas Verfolgen).
Damit haben Sie nun ganz gut die aktiven Elemente der Spiellogik erfasst und beschrieben. Es fehlt nun nur noch das Zusammenspiel mit dem View und dem Event-Handling.
Machen Sie bspw. an folgendem UML-Klassendiagramm deutlich, dass der View
mittels seiner update()
-Methode die Aufgabe hat, den Modellzustand so in einen DOM-Tree abzubilden, dass dieser eine sinnvolle Repräsentation des logischen Spielzustands für den Nutzer darstellt.
Sie können dabei auch darauf hinweisen, dass das Model seinen Zustand mittels einer bislang noch nicht erwähnten Methode getState()
lesend als JSON-Zeichenketten zur Verfügung stellt. Sie müssen also Methoden nicht immer und in allen UML-Klassendiagrammen angegeben. Wenn eine Methode nur in einem Erläuterungskontext benötigt wird, müssen Sie eine Methode auch nur in diesem Kontext in einem Diagramm auswerfen. Auch wenn Methoden oder Datenfelder in anderen Kontexten natürlich noch vorhanden sind. Aber wenn Klassen-Komponenten in einem spezifischen Kontext nichts zum Verständnis beitragen, wieso sollten Sie diese dann aufführen?.
Machen Sie nun deutlich wie der Nutzer mit dem Spiel interagieren kann. Z.B. dass der Controler
für alle Eventklassen einen entsprechenden Handler vorsieht, die aus der observeEvents()
Methode aufgerufen wird. Und das das Spiel Eventhandler für Gyroskop (Gyro
), Tastatur (Keys
) und Maus (Mouse
) vorsieht.
Ihnen wurde nun nicht ein großes Diagramm mit allen Details auf einmal präsentiert, sondern dieselben Informationen in sechs Teilmodellen Stück für Stück aufbereitet.
Häufig ist aber das auch noch zu viel. Bedenken Sie, dass ein Modell (und ein UML-Klassendiagramm ist nichts anderes als ein Modell einer Software) nur die Realität vereinfacht abbilden soll und nicht exakt. Verstehen Sie ein UML-KLassendiagramm daher nicht im Sinne eines Fotos, sondern mehr im Sinne einer Prinzipskizze. In einem Klassendiagramm müssen also nicht immer alle Details (z.B. alle Datenfelder) auftauchen, sondern Sie können sich auch nur auf die wesentlichen Eckpunkte des Entwurfs fokussieren, die für einen Aspekt von besonderer Bedeutung sind.
Sie dürfen in einem Modell also immer etwas weglassen! Konzentrieren Sie sich auf das für das Verständnis eines Außenstehenden Wesentliche und Hilfreiche! Alle Details lassen sich notfalls auch immer noch im Code nachvollziehen. Diese Diagramme dienen dazu, diese Stellen im Code schnell zu finden. Was Code allerdings grundsätzlich fehlt ist ein Überblick gebendes “Inhaltsverzeichnis”. Genau hier kommen die Architektur und UML-Klassendiagramme ins Spiel.
Klassendiagramme sind eine effiziente Art schnell einen Überblick über statische Codestrukturen zu geben und deren Beziehungen zueinander deutlich zu machen. Aber nicht alles lässt sich statisch erläutern. Z.B. kann der Kontrollfluss durch beteiligte Entities im Rahmen des Event Handlings und des Game Loops nicht durch UML Klassendiagramme erläutert werden.
Hilfreicher ist da bspw. ein Sequenzdiagramm, dass die Wechselwirkungen und die Call-Abfolge zwischen den beteiligten Entities veranschaulicht.
Aus diesem Sequenzdiagramm wird bspw. deutlich, dass der Controler
beim Event-Handling mit seiner handle()
-Methode die loop()
Methode des Models aufruft. Im Rahmen der Berechnung des nächsten Spielzustands werden u.a. die strategy()
Methoden der Spiel Entities
aufgerufen (hier wird die Entity-spezifische Logik implementiert). Nachdem der Game Loop komplett berechnet wurde, geht die Kontrolle an den Controler
zurück, der wiederum die update()
-Methode des View
aufruft, um den neu berechneten Spielzustand darzustellen.
Dieses dynamische Verhalten und die Sequenz der Wechselwirkungen zwischen beteiligten Objekten lassen sich nicht wirklich gut mit Klassendiagrammen darstellen, aus Sequenzdiagrammen geht dies aber sehr schnell und intuitiv erfassbar hervor.