Nane Kratzke

Blog:

My 'favourite' Anti-Patterns

Published: 17 Apr 2020
Author: Nane Kratzke
Image: Pixabay

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

  1. Anti-Pattern: Abhängigkeiten zu spezifischen Klassen eingehen
  2. Anti-Pattern: Klassen ohne Verhalten
  3. Anti-Pattern: Dreiecke und Zyklen
  4. Anti-Pattern: Das magische Element
  5. Anti-Pattern: Assoziationen als Datenfelder doppeln
  6. Anti-Pattern: Alles in ein Klassendiagramm quetschen

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.

Anti-Pattern 1: Abhängigkeiten zu spezifischen Klassen eingehen

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:

UML

Wie dann?

Es wäre besser Sie würden die Klasse X wie folgt entwerfen:

UML

Warum?

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.

UML

Stellen Sie Ihre Software-Entwürfe also von Anfang an auf Erweiterbarkeit ein.

Anti-Pattern 2: Klassen ohne Verhalten

Häufig sind Entwürfe folgender Art zu finden.

UML

D.h. die Klassen Entity und PowerUp sind reine Datenkapseln. Beide Klassen haben keine Methoden und damit kein eigenständiges Verhalten.

Wieso ist das problematisch?

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.

Wie dann?

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:

  • “Mir fällt nichts besseres ein.”
  • “Das ging am schnellsten.”
  • Oder der Klassiker: “Es funktioniert doch!”

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.

UML

Anti-Pattern 3: Dreiecke und Zyklen

Häufig finden sich auch solche Entwürfe.

UML

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.

Was ist das Problem?

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.

Wie vermeidet man das?

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:

UML

Wenn Sie eher Kontrollfreak sind und dem Model zentral die Kontrolle geben wollen, präferieren Sie vermutlich eher folgenden Ansatz:

UML

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.

Achtung bei Zyklen

Noch schlimmer sind Zyklen, die schnell entstehen, wenn Kenntnisbeziehungen in beide Richtungen zwischen den Entities bestehen und Dreiecksbeziehungen eingegangen werden. Z.B. so:

UML

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.

Anti-Pattern 4: Das magische Element

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:

UML

Die Klasse X hat also die meisten Ein- und Ausgänge. Das an sich ist noch nicht problematisch (mal abgesehen von den Dreiecksbeziehungen).

Wann wird dieses Pattern problematisch?

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:

  • “Das müssen wir noch klären, die Klasse X ist aber nicht kompliziert.”
  • “Wir alle.”
  • “Ja, das macht Tom.” (Und Tom wird bleich, weil er gerade das Anti-Pattern 2 verstanden hat, dem die Klassen A bis F folgen, also Tom die ganze Arbeit hat, obwohl er ja nur eine Klasse betreut).

Wie vermeiden Sie dies?

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:

UML

Dies macht den Entwurf auch automatisch besser nachvollziehbar.

Anti-Pattern 5: Assoziationen als Datenfelder doppeln

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

UML

dann machen Sie sich das Leben doch einfacher und wenden Sie UML einfach konsequent an.

UML

Anti-Pattern 6: Alles in ein Klassendiagramm quetschen

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?

UML

Vom Überblick zu den Details

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.

UML

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!

Erklären Sie das Model

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.

UML

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).

UML

Ä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).

UML

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.

Erklären Sie den View

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.

UML

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?.

Erklären Sie zum Schluss den Controler

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.

UML

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.

Nicht alles ist durch Klassendiagramme effizient erklärbar

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.

UML

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.