Objektorientierte Programmierung

Geometrie-Editor

Als Beispiel für objektorientierte Programmierung dient das Erstellen eines dynamischen Geometrie-Editors. Um den Programmcode zu verstehen, müssen Sie mit den Grundlagen dieser Programmiertechnik vertraut sein. Schauen Sie sich das im Browser laufende Programm GeoGebra an, um zu verstehen, was unser Geometrie-Editor leisten soll.

Das folgende Bild zeigt einige einfache Anwendungsbeispiele:

Vier Bildschirmfotos des Geometrie-Editors: Dreieck mit Mittelsenkrechten und Umkreis, ein Pfad, sowie Parallele, Polygon, Ellipse und Winkelhalbierende

Bild links oben

Zunächst legt man durch drei Punkte (kleine Quadrate) ein Dreieck fest und zeichnet zu jeder Seite die Mittelsenkrechte. Die Dreieckspunkte kann man mit gedrückter Maustaste ziehen. So erkennt man, dass sich diese drei Geraden immer in einem Punkt schneiden.

Dann legt man durch den Schnittpunkt der Mittelsenkrechten den Mittelpunkt eines Kreises fest. Als Kreispunkt wird eine beliebige Ecke des Dreiecks gewählt. Wohin auch immer die Ecken des Dreiecks geschoben werden, immer geht der Kreis durch alle drei Eckpunkte.

Bild rechts oben

Hier wurde ein Pfad erzeugt. Durch Verschieben der Punkte des Pfades kann man, sofern genügend Punkte gesetzt sind, nahezu jede Form umschließen. Diese Technik verwendet man in der Bildbearbeitung, um ein Objekt freizustellen.

Bilder unten

Parallele, Polygon, Ellipse, Winkelhalbierende und so weiter.

Falls Sie das einmal ausprobieren wollen: Hier geht es zum Geometrie-Editor.

Die Klassenstruktur

Einen Überblick über die verwendeten Klassen und ihre Abhängigkeiten zeigt das nächste Bild:

Klassendiagramm des Geometrie-Editors mit der abstrakten Klasse Geometrie an der Spitze und den davon abgeleiteten Klassen

Die Pfeile zeigen an, von welcher Klasse die gegebene Klasse abgeleitet wurde. So wurde zum Beispiel die Klasse Mittelsenkrechte von der Klasse Strecke abgeleitet.

Die Klasse Geometrie ist eine abstrakte Klasse. Das heißt, von ihr können keine Objekte erzeugt werden. Verständlich, denn „Geometrie“ ist nichts, was man zeichnen kann. Die „Kennt-Beziehungen“ wurden, der besseren Übersicht wegen, weggelassen.

Wozu eine Klasse gut ist, von der es keine Objekte gibt

Auf den ersten Blick wirkt eine Klasse ohne Objekte nutzlos. Ihr Zweck ist aber gerade nicht, etwas darzustellen, sondern etwas zu versprechen. Geometrie legt fest, was jedes Objekt im Editor können muss – ganz gleich, ob es später ein Punkt, eine Ellipse oder eine Winkelhalbierende wird:

abstract class Geometrie {

  boolean sichtbar = true;
  color farbe = color(0);

  abstract void zeichne();
  abstract boolean getroffen(float mx, float my);
}

Die beiden mit abstract gekennzeichneten Methoden haben keinen Rumpf. Sie sagen nur: „Jede abgeleitete Klasse muss das können.“ Der Compiler achtet darauf – wer eine neue Geometrieklasse schreibt und zeichne() vergisst, erfährt es sofort und nicht erst beim Ausprobieren. Was dagegen für alle gleich ist, etwa Farbe und Sichtbarkeit, steht bereits in der abstrakten Klasse und muss nicht sechzehnmal wiederholt werden.

Polymorphie – der eigentliche Gewinn

Jetzt kommt der Punkt, an dem sich der ganze Aufwand auszahlt. Der Editor verwaltet alle Objekte in einer einzigen Liste, und das Zeichnen ist eine Schleife:

ArrayList<Geometrie> objekte = new ArrayList<Geometrie>();

void draw() {
  background(255);
  for (Geometrie g : objekte) {
    g.zeichne();          // jedes Objekt weiß selbst, wie
  }
}

In dieser Schleife steht nirgends, um welche Art von Objekt es sich handelt. Der Aufruf g.zeichne() landet trotzdem bei der richtigen Methode – bei einem Kreis in Kreis.zeichne(), bei einer Parallelen in Parallele.zeichne(). Diese Auswahl zur Laufzeit heißt Polymorphie („Vielgestaltigkeit“), und sie ist der Grund, warum objektorientierte Programme mit wachsender Funktionsvielfalt nicht unübersichtlich werden müssen.

Ohne diese Technik bräuchte man an jeder Stelle eine Fallunterscheidung über sämtliche Typen – beim Zeichnen, beim Anklicken, beim Verschieben, beim Löschen, beim Speichern. Jede neue Figur zwänge dazu, all diese Stellen wiederzufinden und zu erweitern. Mit Polymorphie gilt dagegen:

Eine neue Figur hinzuzufügen heißt: eine neue Klasse schreiben. Am bestehenden Code ändert sich nichts. Das ist keine Bequemlichkeit, sondern der Unterschied zwischen einem Programm, das man nach einem Jahr noch erweitern kann, und einem, das man lieber neu schreibt.

Die weggelassenen Kennt-Beziehungen

Das Klassendiagramm zeigt die Vererbung, und der Hinweis in Klammern verrät, dass noch etwas fehlt. Dieses Fehlende ist allerdings genau das, was einen Geometrie-Editor dynamisch macht, und deshalb lohnt sich ein zweiter Blick darauf.

Eine Mittelsenkrechte hängt in der Luft, solange sie nicht weiß, zu welcher Strecke sie gehört. Sie muss sich zwei Punkte merken:

class Mittelsenkrechte extends Strecke {

  Punkt a, b;               // die beiden Punkte, die sie kennt

  Mittelsenkrechte(Punkt a, Punkt b) {
    this.a = a;
    this.b = b;
  }

  void aktualisiere() {
    // aus a und b die eigene Lage neu berechnen
  }
}

Damit stehen zwei grundverschiedene Beziehungen nebeneinander, die man auseinanderhalten sollte:

Die beiden Beziehungsarten im Editor
Beziehung Sprachlich In Java Beispiel
Vererbung „ist ein“ extends Eine Mittelsenkrechte ist eine Strecke
Kennt-Beziehung „kennt ein“ Feld in der Klasse Eine Mittelsenkrechte kennt zwei Punkte

Die Vererbung bestimmt, was ein Objekt ist, die Kennt-Beziehung, woraus es sich berechnet. Anfänger verwechseln beides gern und leiten etwa Dreieck von Punkt ab, weil ein Dreieck ja Punkte enthält. Die Faustregel: Lässt sich der Satz mit „ist ein“ bilden, ist Vererbung richtig; passt nur „hat ein“ oder „kennt ein“, gehört es als Feld hinein.

Warum die Reihenfolge beim Neuzeichnen entscheidend ist

Verschiebt man im Bild links oben eine Ecke des Dreiecks, so muss eine ganze Kette von Objekten neu berechnet werden: erst die drei Mittelsenkrechten, dann deren Schnittpunkt, dann der Kreis, der diesen Schnittpunkt als Mittelpunkt hat. Rechnet man in der falschen Reihenfolge, so sitzt der Kreis noch am alten Platz, während das Dreieck schon am neuen ist.

Die Kennt-Beziehungen bilden also einen Abhängigkeitsgraphen, und in ihm muss stets zuerst das berechnet werden, wovon anderes abhängt. Zwei Wege führen zum Ziel:

Beide Wege setzen voraus, dass der Graph keine Kreise enthält. Ein Punkt, der – über mehrere Ecken – von sich selbst abhinge, brächte jede Neuberechnung in eine Endlosschleife. Ein Editor muss das daher schon beim Erzeugen verhindern: Bevor eine neue Abhängigkeit eingetragen wird, prüft er, ob das neue Objekt bereits unter den Vorgängern vorkommt.

Wo die Vererbung knirscht

Eine letzte Beobachtung, die über dieses Beispiel hinaus nützlich ist. Dass Mittelsenkrechte von Strecke erbt, ist praktisch: Das Zeichnen, die Trefferprüfung und die Farbverwaltung sind damit schon erledigt. Streng genommen gilt der Satz „ist ein“ aber nur zur Hälfte. Eine Strecke lässt sich frei verschieben; eine Mittelsenkrechte nicht, denn ihre Lage ergibt sich zwingend aus zwei Punkten. Die geerbte Fähigkeit „verschieben“ ist für sie also gar nicht sinnvoll.

Solche Stellen tauchen in fast jedem gewachsenen Klassenbaum auf, und sie sind kein Grund zur Beunruhigung – man sollte sie nur kennen. Wer sie vermeiden möchte, gibt der Basisklasse eine Methode istBeweglich(), die abgeleitete Klassen mit false beantworten können, oder trennt die beweglichen von den berechneten Objekten schon im Klassenbaum. Beides hat seinen Preis: mehr Klassen gegen weniger Sonderfälle.

Sketch Geometrie-Editor herunterladen

Menu