Softwarearchitektur

Aus Das Sopra Wiki
Version vom 4. Mai 2019, 10:08 Uhr von Thomas (Diskussion | Beiträge) (Noch mehr Text zu Use Cases ergänzt)

Softwarearchitektur […] beschreibt die grundlegenden Komponenten und deren Zusammenspiel innerhalb eines Softwaresystems.[1]

Den Softwareaspekt mal außen vorgelassen, so weckt der Begriff „Architektur“ doch gewisse Vorstellungen in uns. Der Architekt – hochgebildet und erfahren – plant und entwirft Bauwerke nach praktischen und ästhetischen Gesichtspunkten und überwacht die korrekte Ausführung. Doch so sehr sich die Konstruktion von Bauwerken und die von Software auf den ersten Blick ähneln mögen und so sehr sich entsprechende Metaphern aufdrängen[2], so gibt es doch insbesondere einen entscheidenden Unterschied: Bauwerke sind vorwiegend statisch. Es sind Immobilien (lateinisch im-mobilis ‚unbeweglich‘). Software ist soft, weich, veränderbar. Und so sehr man sich auch einbilden mag, den Use Case zu kennen, allein das Wissen um diese Veränderbarkeit sorgt dafür, dass stets Bedarf für Veränderung gefunden wird.

Dies gilt umso mehr für Entwicklung von Videospielen. Hier ist das Ziel der Spielspaß, der schwer zu messen und höchst subjektiv ist.

Der Nutzen guter Architektur

Gute Architektur ist kein Selbstzweck! Es geht nicht darum, möglichst viele Design Patterns zu verwenden. Um nochmals die hinkende Metapher zum Bau zu ziehen: Gebäude sollen ihren praktischen Nutzen erfüllen (Schutz bieten und das Leben erleichtern) sowie ästhetischen Anforderungen genügen (gut aussehen und sich in die Umgebungsbebauung einfügen). Was heißt das für Software? Hier ist der tatsächliche Code meist vor dem Endanwender verborgen. Funktionale Software kann also durchaus mit schlechter Architektur gelingen. Die wahren Nutznießer guter Architektur sind also die Entwickler selbst. Eine gute und „ästhetische“ Architektur erleichtert die Entwicklung, vor allem in den Aspekten:

  • Verständnis vorhandener Funktionalität
  • Veränderungen vornehmen
  • Alle Arten des Testens (nicht nur Unit Tests, auch manuelle Tests durch den Entwickler, Integrationstests etc.)

Und eben diese Aspekte nehmen die meiste Zeit in Anspruch, je größer ein Softwareprojekt wird. Schlussendlich gilt jedoch: Der Erfolg gibt Recht. Und erfolgreich kann man mit verschiedenen Ansätzen sein.[3]

Bewährte Prinzipien

Robert C. Martin stellt in seinem Buch „Clean Architecture“ eine Reihe von Behauptungen auf:[4]

  • Architektur ist wichtiger als Funktionalität. Gute Architektur erleichtert das Vornehmen von Veränderungen. So kann Funktionalität bei guter Architektur einfach hergestellt werden. Eine funktionierende Software ohne gute Architektur funktioniert allerdings genau so lange, bis sie geändert werden muss.
  • Ein Softwarearchitekt sollte gleichzeitig auch ein Programmierer sein. Es ist schwer, gute Designentscheidungen zu treffen, wenn man von diesen nicht auch selbst betroffen ist.
  • Das Ziel guter Architektur ist es, so viele Entscheidungen wie möglich so lange wie möglich hinauszuzögern.

Anmerkung: Robert C. Martin's Bücher sind zwar stets empfehlenswert, jedoch nicht ohne Kontroversen.

Zur Umsetzung eben dieser Grundsätze gibt er folgende Ratschläge mit auf den Weg:

Grenzen abstecken (Boundaries)

Die Hauptaufgabe einer Architektur: Man hat eine Menge Komponenten – in unserem Fall vorwiegend Klassen – die bestimmte Funktionalität implementieren und wiederum von anderen Komponenten verwendet werden. Wenn nun also eine Klasse viele verschiedene Methoden unterschiedlichster Natur bereitstellt, dann wird sie auch von sehr vielen anderen Klassen verwendet. Es muss sehr viel an dieser einen "Über-Klasse" gearbeitet werden. Es gibt viele Konflikte und selbst einfache Änderungen betreffen die vielen anderen Klassen, die auf diese eine Komponente zugreifen. Damit ist die Software Veränderungen gegenüber sehr "instabil".

Wenn andererseits sehr viele kleine Klassen mit jeweils sehr wenigen Methoden verwendet werden, dann ist jede einzelne Klasse für sich genommen "nutzlos". Viele Klassen müssen für einfachste Dinge zusammenarbeiten, was nun wieder das Verständnis erschwert.

Schlussendlich geht es also darum, Komponenten (Klassen) und ihre Interaktion (Methodenaufrufe) so voneinander abzugrenzen, dass ein vertretbares Mittel zwischen "Instabilität" und "Nutzlosigkeit" gefunden wird. Oder positiv ausgedrückt: Es geht darum eine optimale Architektur zu finden unter den Gesichtspunkten der "Stabilität" und "Nützlichkeit" der einzelnen Komponenten.

In Use Cases denken statt in Implementierungsdetails

Funktionen und Komponenten können auf verschiedene Weisen gruppiert werden: einzelne Methoden werden in Klassen zusammengefasst. Klassen werden in Namespaces gehalten. Und eventuell wird all das in Bibliotheken verteilt (letzteres aber wohl nicht im Softwarepraktikum). Als Richtlinie gilt hier, dass diese Kategorisierung danach gewählt werden sollte, wie und von wem die Funktionen verwendet werden. Nicht danach, wie genau die Funktion implementiert wurde.

Selbiges gilt natürlich auch für Methoden, die von Klassen bereitgestellt werden ("public"). In der Praxis interessiert es den Nutzer einer Datenstruktur meist nicht, ob diese eine ausgeklügelte Baumstruktur hat, Hashing verwendet oder einfach nur ein Array durchläuft. Wichtig ist bei der Verwendung nur, dass die erwartete Funktionalität bereitstellt (und in diesem Fall ihr Laufzeitversprechen hält). Beim Design von Komponenten sollte also immer der spätere Nutzer im Blick behalten werden. Beim Softwarepraktikum sind das zuallererst die anderen Mitglieder des Entwicklerteams.

Trennung nach Funktionalität

Es hat sich herausgestellt, dass es eine gute Idee ist unterschiedliche Funktionalitäten (oder vielleicht besser: Funktionalitätsarten) voneinander zu treffen. Am einfachsten verständlich ist das an einem Beispiel:

Hat eine Komponente die Aufgabe Daten auf die Festplatte zu schreiben, dann tut sie auch nur das. In dieser Komponente ist keine Spiellogik enthalten. Auch kommt kein Code vor, der irgendwas mit Grafik oder Sound macht. Damit wird die Verwendung erleichtert und Fehler sind ebenfalls leichter zu finden. Ein weiterer ganz besonderer Vorteil ist außerdem, dass hier später auch Interfaces verwendet werden können. Haben wir erst eine stabile DiskWriter-Klasse, kann diese schnell mit einem WriterInterface ausgetauscht werden. So können später auch weitere Klassen einfach eingesetzt werden, die z.B. einen Netzwerksockel beschreiben (für Netzwerk-Kommunikation) oder einfach nur Dinge im temporären Speicher ablegen.

Besonderheiten bei der Entwicklung von Videospielen

Videospiele nehmen in der Softwarewelt eine Besonderheit ein. Nicht nur technologisch, da unterschiedlichste Subsysteme in Echtzeit zusammenarbeiten müssen. Auch die Entwicklung ist davon geprägt, dass es sich um an sich um ein verbrauchbares Konsumgut handelt (die meisten Videospiele hat man irgendwann ausgespielt). Entsprechend unwichtiger ist langfristige Wartbarkeit, da sich der Lebenszyklus des Spiels kaum vorhersehen lässt. Dies äußert sich natürlich auch in der Architektur:[5]

  • Auf Applikationsebene (also der Code, der unmittelbar für das Spielgeschehen verantwortlich ist) möchte man flexibel bleiben. Hier muss es nicht immer hübsch und korrekt zugehen. Man möchte schnell herausfinden ob Dinge funktionieren und ob es Spaß macht.
  • Auf Ebene der Engine sollte Stabilität im Vordergrund stehen. Damit ist nicht zwingend gemeint, dass der Code nicht verändert wird. Vielmehr geht es um das Interface und die Funktionsweise. Niemand will schließlich, dass neue Elemente plötzlich an den Anfang statt ans Ende einer Liste gehangen werden. Mehr als sonst lohnt sich hier die Beachtung der CleanCode Prinzipien.

Jedoch ist auch hier der Übergang fließend. Gerade Datenstrukturen (z.B. ein Quad-Tree) sollten definitiv so stabil wie möglich sein und entsprechender Aufwand rentiert sich durchaus. Sollten im Spiel viele verschiedene Einheitentypen vorkommen, ist die Nutzung von Design Patterns ebenfalls zu empfehlen, damit diese ohne viel Copy&Paste implementiert werden können. Einmalige Spezialfähigkeiten hingegen können auch gerne mal schnell reingepfuscht werden. Das wichtigste Clean Code Prinzip „Don‘t repeat yourself“ (DRY) darf also im Videospielbereich durchaus ab und zu ignoriert werden.

Vorsicht! Anforderungen ändern sich. Anfangs mögen z.B. wenige Einheitentypen geplant sein und entsprechend wird das irgendwie zurechtgehackt. Doch irgendwann wird doch beschlossen, dass man mehr möchte und dann werden Veränderungen schwierig. Ob und wo sich die Anforderungen verändern, lässt sich jedoch kaum vorhersehen.

Beispiel

Eine erste Architektur entsteht meist ganz natürlich, wenn es darum geht Code zu strukturieren und Dopplungen zu vermeiden. Am Beispiel eines beliebigen Videospiels könnte das so aussehen, dass wir erst mal allen Code in unserer Game-Klasse gerade herunterschreiben. So würde für jeden Schritt/Tick Folgendes ausgeführt werden:

  • Eingabe wird erfasst (eine großes If- oder Switch-Anweisung)
  • Update der Spiellogik
  • Aktualisierung der Anzeige (Bilder werden ausgewählt und an die entsprechende Grafik-Schnittstelle weitergereicht)

Nun bietet es sich an, diese verschiedenen Funktionalitäten in entsprechenden Klassen abzubilden, z.B. InputHandler, GameLogicUpdater, DisplayDrawer. Nun sind nur noch drei Zeilen Code in unserer Game-Klasse selbst nötig:

  • inputHandler.handle(...)
  • gameLogicUpdater.update(...)
  • displayDrawer.draw(...)

Als nächstes stellen wir fest, dass in unserer GameLogicUpdater-Klasse viele verschiedene Einheiten bewegt werden: King, Queen, Knight... Alle davon haben eine Methode move(), die immer etwas anders funktioniert. Damit man die aber alle in eine Liste stecken kann, bietet sich hier die Verwendung eines Interfaces "Movable" an. Statt für jeden einzeln move() aufrufen zu müssen lässt sich das nun in einem einfachen Loop machen:

foreach (Movable movableUnit in units) { movableUnit.move(); }

Ebenso lässt sich zum Beispiel wieder der InputHandler unterteilen, in einen KeyboardInputHandler und einen MouseInputHandler. Im DisplayDrawer könnte zwischen Hintergrund, Vordergrund und GUI unterschieden werden und so weiter. Irgendeine Architektur kommt also in jedem Fall zustande. Die Qualität dieser Architektur kann schlussendlich nur dadurch bestimmt werden, wie leicht sich damit arbeiten lässt.

 

So ein UML-Klassendiagramm sieht so zwar erst mal hübsch aus, aber natürlich braucht die Spiellogik Zugriff auf die Eingabe. Und die Bewegung der verschiedenen Einheiten muss natürlich auch angezeigt werden. Die Komponenten müssen irgendwie aufeinander zugreifen und so gehen die Probleme los. Außerdem: Wie kompliziert wäre eine Änderung, wenn nicht nur Vordergrund und Hintergrund sondern eine beliebige Anzahl von Ebenen angezeigt werden soll?

Referenzen

  1. https://de.wikipedia.org/wiki/Softwarearchitektur
  2. „Metaphern sind wie Lügen, nur ausschmückender.“ (Terry Pratchett: Wachen! Wachen!)
  3. Amy Brown, Greg Wilson, editors. The Architecture of Open Source Applications. Volume I and II. 2014. https://www.aosabook.org
  4. Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2017.
  5. How to make your dream game, publish it and not die in the process