Login
Newsletter
Werbung

Do, 12. April 2012, 15:00

Objektorientierte Programmierung: Teil 1 – OOP in der Praxis

Ein erstes Beispiel

Als (anfangs schlechtes) Beispiel soll der vierte Programmierwettwerb von freiesMagazin herhalten. Für diesen wurde ein Beispiel-Bot geschrieben, der den Teilnehmern als Sparringpartner diente. Dieser soll hier etwas vereinfacht nachprogrammiert werden, sodass er folgende Anforderungen erfüllt:

  • Spielverwaltung und Bot getrennt
  • Einlesen von Benutzereingaben (Zahlen zwischen 0 und 1000)
  • Antwort des Bots nach einer bestimmten Strategie, die im Programm per Kommandozeile ausgelesen wird
  • Drei vordefinierte Strategien:
    1. immer annehmen
    2. immer ablehnen
    3. Annahme, wenn größer gleich 200. Wenn dreimal nacheinander kleiner 200, dann immer Ablehnung; wenn dreimal nacheinander größer als 700, dann immer Annahme.
  • Bot soll angenommene Punkte zählen und am Ende ausgeben

Da bei der Entwicklung eher das Software-Design im Vordergrund steht, und damit die Beispielprogramme nicht zu lang werden, wird weniger Gewicht auf ausführliche Dokumentation oder Fehlerbehandlung gelegt.

Wenn man sich keine großen Gedanken über eine Umsetzung macht bzw. einfach sehr faul ist, erstellt man zwei Klassen. Eine Klasse Game, welche die Spielverwaltung und Erstellung des Bots übernimmt, und eine Klasse Bot, welche die Strategie bestimmt und die Punkte zählt. Dies war auch der erste Ansatz für den Beispielbot des Programmierwettbewerbs, woraus dann auch die Idee für die Artikelreihe entstanden ist.

Das Software-Design der naiven Umsetzung

Dominik Wagenführ

Das Software-Design der naiven Umsetzung

Um sich klar zu machen, welche Aufgabe eine Klasse hat, kann man sogenannte CRC-Karten einsetzen (Class-Responsibility-Collaboration-Karten). Diese enthalten den Klassennamen, die Verantwortlichkeit und alle anderen Objekte, die die Klasse benötigt. Für das erste Beispiel sähen diese so aus:

Klasse:
Bot
Benötigt:
Verantwort.:
nimmt Angebote an oder lehnt sie je nach Strategie ab; zählt die angenommenen Punkte

Klasse:
Game
Benötigt:
Bot
Verantwort.:
erstellt Bot und setzt die Strategie (aus Konsolenparameter); liest Benutzereingabe und fragt Bot nach Annahme oder Ablehnung

Abhängigkeitsanalyse

Wieso sind Abhängigkeiten überhaupt wichtig? Stark vernetzte Software (das heißt eine mit vielen Verbindungen zwischen den Klassen) führt früher oder später zu einem nicht mehr überschaubaren Wust an Code. So kann man irgendwann nicht mehr sagen, welche Teile einer Software unabhängig voneinander arbeiten können – zum Beispiel in Form von Bibliotheken. Daher bemüht man sich in der Regel, die Abhängigkeiten zwischen einzelnen Softwareteilen (Komponenten) gering zu halten.

Nicht in allen Sprachen wirken sich die Abhängigkeiten der Software unbedingt negativ aus. C++ ist aber ein sehr schönes Beispiel, da hier die Abhängigkeiten direkten Einfluss auf die Generierung nehmen. Hat man eine Software, die aus 50 Bibliotheken und Hunderten von Klassen besteht, die alle kreuz und quer miteinander vernetzt sind, führt die Änderung einer einzigen Header-Datei oft dazu, dass alle Klassen und alle Bibliotheken neu übersetzt werden müssen. Dies versucht man normalerweise zu vermeiden, da es unnötigen Zeitaufwand bedeutet (was mit hinausgeworfenem Geld gleichzusetzen ist).

Zusätzlich ist eine Software mit vielen Abhängigkeiten nur noch schwer zu verstehen, da es bei kleinen Änderungen bereits unerwünschte Seiteneffekte geben kann. Auch unerwünschte zyklische Abhängigkeiten können so leichter auftreten. Ebenso werden Unit-Tests (oder Modul-Tests) durch zu viele Abhängigkeiten erschwert.

Hinweis: Mit Abhängigkeit ist jede Verbindung einer Klasse zu einer anderen gemeint und nicht eine Abhängigkeit im strengen UML-Sprachgebrauch.

Welche Abhängigkeiten ergeben sich also aus den zwei Klassen?

Die Klasse Bot hat nur Abhängigkeiten zu Standard-Datentypen wie string und int, die nicht ins Gewicht fallen.

Die Klasse Game ist direkt von Bot abhängig, da der Bot von ihr erstellt und danach benutzt wird. Im UML-Modell wird dies dann mittels einer Abhängigkeitsbeziehung vom Stereotyp instantiate dargestellt.

Wie man an den CRC-Karten sieht, haben die zwei Klassen Bot und Game nicht nur eine Aufgabe, sondern mehrere. Dies ist nicht immer, aber oft ein Hinweis darauf, dass man seine Klassen falsch »geschnitten« hat.

Man hätte es aber sogar noch schlimmer machen und die Benutzereingabe auch direkt in Bot auslesen können, ebenso wie die komplette Spielverwaltung. Dann wäre die Klasse aber ein echter Gemischtwarenladen an Aufgaben und es gäbe gar keine klare Verantwortlichkeit der Klasse Bot mehr. (So ein Objekt nennt man gemeinhin Gott-Objekt.)

Der Nachteil des obigen Designs ist also, dass die Klassen zu viele Aufgaben übernehmen. So ist es eher seltsam, dass die Klasse Game den Bot erstellt. Ebenso kann man überlegen, ob es sinnvoll ist, dass Game die Interpretation der Benutzereingaben übernimmt. In der Artikelreihe soll sich aber mehr auf den Bot und dessen Strategien konzentriert werden. Aber auch dieser hat mehrere Verantwortlichkeiten, wie man sieht.

Ein weiterer Nachteil ist, dass die Übersicht in der Klasse Bot leidet, wenn es sehr viele Strategien gibt. Diese werden durch zig if-Abfragen anhand der Zeichenkette mStrategy in der Operation acceptOffer unterschieden. Wenn man also 30 Strategien hat, die mitunter um einiges komplexer sind als die Beispielstrategien, wird das extrem unübersichtlich. Daneben muss man z.B. für die Strategie, dass man ein Angebot immer annimmt, das Attribut mNumAcceptInRow speichern, auch wenn man es in dem Fall gar nicht benötigt.

Der dritte Nachteil ist, dass die Bibliothek libgame neu übersetzt werden muss, wenn sich in der Deklaration von Bot etwas ändert. Dies ergibt sich aus der direkten Abhängigkeit der Klassen. (Dies spielt aber in manchen Sprachen, wie oben geschrieben, keine große Rolle.) Als Folge davon wäre auch ein Unit-Test der Klasse Game nur schwer machbar, d.h. wenn man die Klasse Game testet (z.B. die Operation start), testet man automatisch auch immer die Klasse Bot mit. (In C++ hätte man nur die Möglichkeit, den Include- und Library-Pfad anzupassen, um eine neue Implementierung unterzuschieben. Bei anderen Sprachen kann man die Klasse Bot und dessen Methoden zur Laufzeit überschreiben.)

Der Vorteil der Implementierung ist natürlich ihre Einfachheit. Es gibt nur zwei Klassen und alles ist schön übersichtlich. Leicht erweiterbar oder wartbar ist es auf Dauer aber nicht.

Die C++-Implementierung der zwei Klassen kann als Archiv heruntergeladen werden: oop1-beispiel.tar.gz.

Pro-Linux
Pro-Linux @Facebook
Neue Nachrichten
Werbung