faecher:informatik:oberstufe:modellierung:warum:start

Warum betreiben wir modularen Klassenentwurf?

An dieser Stelle kann man sich mit zwei Fragestellungen befassen:

  1. Warum macht man das überhaupt? Könnte man nicht einfach alles Funktionalität in einer Klasse unterbringen, anstatt das Programm auf viele einzelne Klassen(dateien) zu verteilen?
  2. Wenn die OO-Modellierung eine Problems nicht eindeutig ist - woran erkennt man dann, ob man es "gut" gemacht hat?

Wenn man ein Problem sinnvoll modularisiert und modelliert, hat das viele Vorteile:

  • Lesbarkeit des Quellcodes → Etwas stimmt mit dem Tor nicht? Also muss man in der "tor"-Klasse schauen und nicht 5000 Zeilen Code durchscrollen, bis man zu dem Teil kommt, der das Tor erzeugt. Es erleichtert Änderungen an der Funktionalität, wenn stets klar ist, wo bestimmte Eigenschaften und Fähigkeiten festgelegt sind.
  • Wenn man Klassen geschickt modelliert, kann man Sie in anderen Programmen wiederverwenden - nicht umsonst spricht man von "Klassenbibliotheken".
  • Neue Objekte können durch neue Klassen ein ein Modell eingefügt werden - du willst Hindernisse auf dem Spielfeld? Kein Problem mit der zusätzlichen "hindernis"-Klasse.

Ein Klassenentwurf ist also "gut", wenn er die oben genannten Vorteile maximal unterstützt - hierfür kann man zwei Eigenschaften des Entwurfs betrachten:

Die Kohäsion einer Klasse definiert ihren logischen Zusammenhalt als Einheit. Erkennen kann man das daran, wie deutlich Sie sich von anderen Klassen des Modells abgrenzt, aber auch daran, dass ihre Methoden für klar umrissene Aufgaben zuständig sind. Das Single-Responsibility-Prinzip besagt, dass jede Klasse nur genau eine fest definierte Aufgabe zu erfüllen hat. Diese Aufgabe wird durch das Zusammenspiel aller Attribute und Methoden dieser Klasse erfüllt. Das Zusammenspiel der Attribute und Methoden dieser Klasse ist dadurch also sehr eng - es liegt eine starke Kohäsion vor. Man möchte also eine hohe Kohäsion der Klassen des Modells haben.

Die Eigenschaft "Kopplung" beschreibt die Bindung zwischen den Klassen. Die Kopplung soll möglichst gering sein, das erreicht man dadurch, dass die Abhängigkeiten zu anderen Klassen möglichst klein gehalten werden sollten.

Abhängigkeiten bezieht sich damit sowohl auf die Zahl der Assoziationen (Kontrollkopplung) als auch auf die Komplexität der Bindung (Datenkopplung)1).

Schnittstellen zwischen Klassen sollten also klar und mit möglichst wenig bedeutungsvollen Parametern definiert sein. Anstatt eine get-Methode zu schreiben, die unter Umständen mehrere Parameter benötigt, die ihr mitteilen, welche(s) Attribut(e) zurückgegeben werden soll(en), bekommt jedes Attribut einen eigenen Getter mit sprechendem Namen - und ohne Parameter. Die Aufrufende Klasse kann dann ohne weitere Kenntnisse ihre Aufgaben erfüllen.

Klassenvariablen niemals öffentlich (public) deklarieren. Zugriff auf Attribute von anderen nur über sondierende und verändernde Methoden (get- und set-Methoden) möglich. Änderungen am internen Aufbau der Klasse haben keine Auswirkungen auf andere Klassen, welche mit dieser assoziiert sind.

Die Verwaltung der Position der Münzen in unserem Beispiel ist in Verantwortung der Muenzen-Klasse. Sollte die Verwaltung intern später auf ein Array mit zwei Feldern für x- und y-Koordinate umgestellt werden, so müssten bei direktem Zugriff von außen alle zugreifenden Klassen mitverändert werden - wenn der Zugriff über Getter- und Setter-Methoden gekapselt ist, müssen die assoziierten Klassen nichts über den internen Aufbau der Muenzen-Klasse wissen. Man spricht vom Geheimnisprinzip.

Entlang Zuständigkeiten zu modellieren bedeutet, dass eine Klasse einen logisch sinnvollen und klar abgegrenzten Aufgabenbereich besitzt. Im Münz-Schnipsspiel nimmt das Tor zwar Münzen auf, verwaltet aber nicht deren Koordinaten, ebensowenig wie das Spielfeld: Die Koordinaten gehören logisch zu den Münzen, darum werden sie auch von den Münz-Objekten selbst verwaltet.

Dasselbe, was für die Aufteilung des Problems in Klassen gilt, gilt innerhalb der Klassen für die Methoden: Jede Methode sollte eine klare Aufgabe haben - und einen Namen, der diese Aufgabe auch verdeutlicht.

Die Klasse Spieler hat zwei Methoden - muenzeEinwerfen und muenzeSchnipsen, die beiden Methoden sind funktional sehr ähnlich, es macht jedoch Sinn, die beiden zu trennen, da sie logisch zwei Abläufe des Modells umsetzen.

Man sollte es tunlichst vermeiden, identischen, also redundanten, Code an mehreren Stellen eines Programms zu verwenden. Dieses Prinzip wird oft auch das DRY-Prinzip (Don't repeat youself) genannt. So spart man Schreibaufwand, das Programm ist leichter zu verstehen und zu warten, denn man muss den Code nur einmal ändern und nicht noch alle Redundanzen.

In unserem Beispiel gibt es eine Methode setPosition() des Münz-Objekts, diese wird von allen assoziierenden Klassen aufgerufen, wenn die Position der Münze verändert werden soll. Wäre es an mehreren Stellen des Programmcodes möglich die entsprechenden Attribute des Münz-Objekts zu beeinflussen, müssten bei einer internen Änderung alle diese Stellen angepasst werden.


1)
Und noch auf ein paar Aspekte mehr…
  • faecher/informatik/oberstufe/modellierung/warum/start.txt
  • Zuletzt geändert: 26.10.2021 10:56
  • von sbel