Processing: Objektorientierte Programmierung Teil 1

Objektorientierung – was ist das, und zu welchem Zwecke betreibt man sie? Das zu klären ist das Ziel dieses Beitrages. Er wendet sich an Leserinnen und Leser, die bereits mit Processing gearbeitet haben, aber mehr über Klassen und Objekte wissen wollen. Eine gute Einführung in Processing gibt es übrigens hier.

Processing ist also eine objektorientierte Programmiersprache. Objekte sind Datentypen, die Eigenschaften (also Variablen) und Verhalten (also Funktionen) zusammenfassen, um einen bestimmten Aufgabenbereich innerhalb eines Programms zu erledigen. Die Beschreibung von Objekten (ihrer Eigenschaften und Verhaltensweisen) nennt man Klasse. Eine Klasse ist sozusagen ein Bauplan für eine bestimmte Sorte von Objekten.

Projekt Pfandflaschensammler

Das klingt recht abstrakt, wird aber schnell verständlich anhand eines konkreten Beispiels: Stellt euch vor, ihr wollt ein Spiel entwickeln, bei dem es darum geht, Pfandflaschen einzusammeln und am Pfandautomaten anzugeben. Dann wäre der Flaschensammler ein heißer Kandidat für eine Klasse. Später würden vermutlich auch Flaschen und Pfandautomaten in eigenen Klassen abgebildet werden.

Bleiben wir aber beim Flaschensammler: Welche Eigenschaften sollte der haben? Neben einem Namen sicherlich die Anzahl der gesammelten Flaschen und das aktuelle Barvermögen in Euros. Welche Funktionen muß er besitzen? Zumindest das Einsammeln und das Abgeben von Flaschen sollte ein Flaschensammler sicher beherrschen! Zudem wäre eine Funktion schick, die Auskunft über den Sammler in Textform gibt, also: wie heißt er, und was hat er im Beutel?

Das folgende Listing zeigt den kompletten Code einer Minimalversion der Klasse Flaschensammler. Zur Konzentration auf das Wesentliche verzichten wir auf eine grafische Darstellung. Ein Flaschensammler wird sich allein auf der Textkonsole der Processing IDE bemerkbar machen. Wenn die Spielidee zündet, steht einer späteren Version mit hyperrealistischer 3D Grafik und 5.1 Sound nichts im Wege!

class Flaschensammler {

  String name;
  int anzahlFlaschen;
  float euroGuthaben;

  Flaschensammler(String name) {
    this.name = name;
    anzahlFlaschen = 0;
    euroGuthaben = 0;
  }

  void flaschenSammeln(int anzahl) {
    anzahlFlaschen += anzahl;
  }

  void flaschenAbgeben() {
    euroGuthaben += anzahlFlaschen * 0.25;
    anzahlFlaschen = 0;
  }

  String toString() {
    return "Flaschensammler " + name
      + " besitzt "+ anzahlFlaschen + " Flaschen und "
      + euroGuthaben + " Euros.";
  }
}

Die Klassendefinition

Es ist üblich und sinnvoll, jede Klassendefiniton in einer eigenen Datei (sprich: einem eigenen Reiter in der Processing IDE) zu schreiben. So ist es auch im Beispielprojekt Flaschensammler01 umgesetzt, das ihr hier herunterladen könnt.

Eine Klassendefinition beginnt immer mit dem Schlüsselwort class, gefolgt von dem Namen der Klasse. Der Name einer Klasse sollte immer mit einem Großbuchstaben beginnen.

Zeile 3 bis 5 deklarieren drei Variablen, die jedes Objekt der Klasse Flaschensammler besitzen soll: name, anzahlFlaschen und euroGuthaben.

Der Konstruktor baut Objekte

In Zeile 7 bis 11 steht eine sehr wesentliche Funktion, der sogenannte Konstruktor. Der Konstruktor wird immer dann aktiv, wenn ein neues Objekt der jeweiligen Klasse erzeugt wird. Diese Erzeugung eines Objektes der Klasse Flaschensammler sieht übrigens so aus: sammler = new Flaschensammler("Pitzi") – dazu später mehr!

Der Konstruktor heißt immer genau so wie die Klasse selbst und hat im Gegensatz zu allen anderen Funktionen keinen Rückgabewert. Konstruktoren sind immer für die Initialisierung von Objekten zuständig, sprich: Die zum Objekt gehörenden Variablen bekommen einen Wert. Im Falle des Flaschensammlers ist das zunächst in Zeile 8 der Name, der über ein Argument vom Typ String dem Konstruktor übergeben wird. Was hat es mit dem Ausdruck this.name = name auf sich? this verweist immer auf das konkrete Objekt. Entsprechend bedeutet this.name „die Variable name von diesem Objekt“. name hingegen verweist hier auf das Argument des Konstruktors.

Zeile 9 und 10 initialisieren anzahlFlaschen und euroGuthaben mit 0. Streng genommen wäre das nicht notwendig, da Variablen vom Typ float und int innerhalb von Klassendefinitionen automatisch mit dem Wert 0 initialisiert werden. So ist es aber klarer und sauberer.

Die Funktionen

Zeile 13 bis 15 definiert die Funktion flaschenSammeln(). Dazu gibt es nicht viel zu sagen: sie erhöht die Anzahl der Flaschen entsprechend dem übergebenen Argument.

Auch die Zeilen 17 bis 20 bergen keine Geheimnisse: Die Funktion flaschenAbgeben erhöht das Geldguthaben ensprechend der Anzahl der Flaschen – wir gehen der Einfachheit halber davon aus, dass alle Flaschen mit 25 Cent bepfandet sind. Anschliessend setzen wir die Anzahl der Flaschen auf Null.

Bleibt die gewünschte Darstellung als Text. Hierfür verwenden wir den von Java und Processing vorgegeben Funktionsnamen toString. Diese Funktion wird nämlich automatisch immer dann aufgerufen, wenn die Textfassung eines Objektes benötigt wird. Das ist zum Beispiel dann der Fall, wenn ein Flaschensammler per println in der Konsole ausgegeben werden soll. Würden wir (eben per eigener Definition der Funktion toString()) nicht für eine sinnvollere Ausgabe sorgen, käme dabei so etwas heraus:

Flaschensammler01$Flaschensammler@33b4968a

Jedes Objekt besitzt bereits eine toString()-Funktion. Unsere in den Zeilen 22 bis 26 selbst definierte Variante überschreibt die vorgegebene toString()-Funktion und sorgt für eine informativere und schönere Ausgabe in der Textkonsole, in etwa so:

Flaschensammler Pitzi besitzt 3 Flaschen und 2.5 Euros.

Flaschensammler in Aktion

Damit ist die Definition der Klasse Flaschensammler abgeschlossen. Schreiten wir zur Benutzung.

Hier gibt es eine kleine Ärgerlichkeit. Auch wenn wir die Klasse Flaschensammler nur in einem statischen Programm testen wollen – also einem, das nicht interaktiv oder animiert ist, und daher weder setup() noch draw() benötigt – müssen wir die setup() Funktion definieren. Andernfalls wird der Typ Flaschensammler nicht erkannt. Dieses Problem tritt nicht auf, wenn die Klasse in derselben Datei definiert und benutzt wird: das gewöhnen wir uns aber besser gar nicht erst an.

Flaschensammler sammler;

void setup(){
  sammler = new Flaschensammler("Pitzi");
  println(sammler);
  // Flaschensammler Pitzi besitzt 0 Flaschen und 0.0 Euros.

  sammler.flaschenSammeln(10);
  println(sammler);
  // Flaschensammler Pitzi besitzt 10 Flaschen und 0.0 Euros.

  sammler.flaschenAbgeben();
  println(sammler);
  // Flaschensammler Pitzi besitzt 0 Flaschen und 2.5 Euros.

  sammler.flaschenSammeln(4);
  println(sammler);
  // Flaschensammler Pitzi besitzt 4 Flaschen und 2.5 Euros.

}

Zeile 1 definiert eine Variable vom Typ Flaschensammler mit Namen sammler. Die Typbezeichnung Flaschensammler kann also genau so verwendet werden wie andere Typenbezeichnungen, zum Beispiel int, float oder String.

In Zeile 4 bekommt die Variable sammler einen Wert zugewiesen, und zwar ein Objekt der Klasse Flaschensammler, das per new erzeugt wird und den Namen "Pitzi" erhält.

In Zeile 5 kommt die Funktion toString() zum Zuge, diese wird im Hintergrund von der Funktion println() aufgerufen. Die Textausgabe steht hier zum besseren Verständnis direkt als Kommentar im Listing. Der Flaschensammler hat, frisch initialisiert, keine Flaschen und kein Geld – das war zu erwarten. Es folgen drei Aufrufe von Flaschensammler-Funktionen in den Zeilen 8, 12 und 16, auch diese arbeiten wie gewünscht.

Datenkapselung: Computer sagt Nein

Im vorliegenden Beispiel haben wir den Zustand des Flaschensammlers allein über die Funktionen flaschenSammeln() und flaschenAbgeben() verändert, und Auskunft über den Zustand allein über toString() erhalten.

Es ist aber auch möglich, folgendes zu machen:

sammler.anzahlFlaschen = 1000;
sammler.euroGuthaben = 10000000;
println(sammler.name)

Solche Aktionen widersprechen einem wichtigen Prinzip der objektorientierten Programmierung: der Datenkapselung. Wikipedia sagt hierzu: „Als Datenkapselung bezeichnet man in der Programmierung das Verbergen von Daten oder Informationen vor dem Zugriff von außen. Der direkte Zugriff auf die interne Datenstruktur wird unterbunden und erfolgt stattdessen über definierte Schnittstellen.“

Um dies zu erreichen, würde man in normalem Java die Flaschensammler-Variablen so deklarieren:

private String name;
private int anzahlFlaschen;
private float euroGuthaben;

Das Schlüsselwort private würde dann dafür sorgen, dass ein direkter Zugriff wie sammler.name nicht erlaubt ist, sondern nur über Funktionen der Klasse läuft. In Processing ist das Prinzip der Datenkapselung nicht umgesetzt. Das hat technische Gründe, auf die wir in Teil 2 dieser Einführung eingehen werden.

Schluss

Wir haben nun die Grundlagen der objektorientierten Programmierung in Processing behandelt. Das ist natürlich noch lange nicht alles. In einem weiteren, in Kürze erscheinenden Artikel werden wir auf das Prinzip der Vererbung eingehen – und damit auch genau verstehen, wie der Zusammenhang zwischen Java und Processing beschaffen ist.