Table of Contents

Objektorientierte Programmierung

Diese Lesson soll das Konzept des Objektorientierten Programmierens (kurz OOP) erläutern, sowie die dafür nötigen Code-Elemente Vorstellen.

Grundgedanke

Ein modulares Programm besteht aus einzelnen Modulen, von den jedes eine bestimmte Aufgabe erfüllen soll. Von Variablen kennen wir den Ansatz der Wiederverwendbarkeit. Sie ermöglichen es einem einzelnen Wert mehrfach in einem Programm aufzutauchen, so dass dieser einfach geändert werden kann. Funktionen abstrahieren eine bestimmte Aufgabe und ermöglichen ebenfalls eine Wiederverwendbarkeit dieser Aufgabe. Dabei ist immer entscheidend, was eine Funktion tut und nicht wie sie im Detail Funktioniert (Abstraktion). Diese Herangehensweise ermöglicht es, sich auf die Ziele eines Programms zu kümmern anstelle sich in Details zu verlieren.

Der OOP-Ansatz erweitert diese Modularität, in dem Variablen und Funktionen zu Objekten gruppiert werden. Das Ziel dabei ist das Gestalten von regelbasiertem Verhalten in Form von Objekten. Die Verhaltensweisen eines Objektes sind formulierte Prozesse, die auf einen bestimmten Input einen bestimmten Output liefern. Man spricht bei diesen speziellen Funktionen von Methoden. Ein Objekt kann weiterhin erst definiert werden, wenn wir seine Eigenschaften kennen. Durch diese Eigenschaften wird das Objekt unterscheid- und vergleichbar mit anderen Objekten.

Dabei ist es durchaus möglich Analogien zwischen Software-Objekten und realen Objekten zu bilden.

Formulierung

Im Texteditor wurde der Code des Sketches bis dato nur in einem Tab formuliert. Dieser Tab trägt den Namen unter welchem der Sketch abgelegt wurde - bzw. einen temporären, kryptischen wenn dies noch nicht passiert ist. Wie bereits angesprochen ist ein Vorteil von oop die Weiterführung des Modularisierungsansatzes von Programmen.
Jede sog. Klasse, die als Grundlage für die Objektgenerierung dient, wird in einem neuen Tab geschrieben. Um einen neuen Tab anzulegen, Klickt man im rechten Teil der Tableiste auf den quadratischen Button (→) und wählt 'New Tab'. Über der Konsole erscheint ein Eingabefeld in dem Processing der Name der Klasse mitgeteilt werden muss. Nach dem Bestätigen der Eingabe mit 'Ok' erhält man ein neues Tab und kann mit der Ausformulierung der Klasse beginnen.

Klasse

Basis für eine oop-Konstruktion ist die Klasse. Ihr Inhalt dient als Bauplan für das spätere Erstellen von Objekten – auch Instanzen oder Klasseninstanzen genannt. Der Begriff class leitet die Formulierung ein, worauf der Klassenname folgt. Dieser muss mit einem Großbuchstaben beginnen und taucht beim Arbeiten als Datentyp bzw. direkt hinter dem Wort new auf. Innerhalb der geschweiften Klammern folgen die Attribute und Methoden der Klasse. Alles was außerhalb des Klassenblocks steht, gehört nicht zur Klasse.

class Butterfly {

  // Der Klassenblock

}

Attribut

Attribute sind die Eigenschaften die eine Klasse aufweist. Sie geben den Objekten die Möglichkeit sich voneinander zu unterscheiden, bzw. eine Vergleichbarkeit untereinander herzustellen. In der Formulierung der Klasse tauchen die Attribute an erster Stelle auf. Da es sich bei ihnen um Variablen handelt, folgt nach der Angabe des Datentyps (String, int, float, …) der bezeichnende Name. Neben dem Datentyp können weitere oop-spezifische Kriterien vergeben werden. Da es sich dabei aber um keine Notwendigkeit handelt, belassen wir es in diesem Abschnitt bei der kompakten Version.

class Butterfly {

  String species;
  String gender;

}

Methode

Die als Fähigkeiten aufgeführten Methoden werden ebenfalls im Klassenkörper platziert. Direkt unter den Attributen formulieren sie Prozesse in von Funktionen bekannter Schreibweise. Das Zurückgeben von Variablen als Result des Aufrufs (Rückgabewerte) und die Angabe von Parametern ist wie bei normalen Funktionen möglich. Durch Einsatz dieser Bausteine kann eine hohe Varianz in das Verhalten unterschiedlicher Objekte gelegt werden.

class Butterfly {
   
   void fly () {
     // Prozess 'fliegen'
   }

   void land () {
     // Prozess 'landen'
   }
}

Konstruktor

Beim Arbeiten mit Capture wurden Ausdrücke wie Capture c = new Capture (…); verwendet (siehe Lesson 14). Da Informationen wie Bildgröße, Bilder pro Sekunde und Kameranamen für Processing notwendig sind, mussten dafür Werte zwischen den Klammern (…) angegeben werden. Beim Anlegen einer Klasse, in dem Fall Capture, können solche Daten mit der Festlegung eines sog. Konstruktors erzwungen werden.
Der Konstruktor ist eine spezielle Funktion einer Klasse und hat keinen Rückgabewert. Im Klassenaufbau findet er zwischen den Attributen und Methoden Platz. Durch Parameter im Funktionskopf wird definiert, welche Informationen beim Ausdruck new KlassenName (…) zwischen den Klammern angegeben werden müssen. Innerhalb des Konstruktors erfolgt eine Zuweisung, wobei jeder Parameter seinem Attribut zugeordnet wird.

class Butterfly {

  String species;
  String gender;

  Butterfly (String theSpezies, String theGender) {
    species = theSpezies;
    gender = theGender;
  }
}

Arbeiten mit Klassen

Instanzen/Objekte erzeugen

Mit einer Klasse haben wir gleichzeitig einen Datentyp erstellt. Beim Anlegen von Instanzen der Klasse taucht der Klassenname vor dem Instanznamen zum ersten Mal auf. Diese Schreibweise ist bereits vom Erzeugen von Variablen bekannt. Nach dem = folgte bei Variablen die Zuweisung des Wertes, z.B. “Text” oder Zahlen. Da es sich bei Klassen automatisch um komplexe Datentypen handelt, muss eine Instanz der Klasse mit dem Wörtchen new erstellt werden. Dadurch nimmt sich Processing den 'Objektbauplan' und strickt uns ein Abbild der Klasse – eine Instanz.

KlassenName InstanzName = new KlassenName (Parameter des Konstruktors);

Bezogen auf unser Schmetterlingsklasse sieht das Erstellen von Instanzen wie folgt aus:

Butterfly bfW = new Butterfly ("Zitronenfalter", "weiblich");
Butterfly bfM = new Butterfly ("Zitronenfalter", "männlich");

Die Instanzen tragen die Bezeichnungen bfW und bfM. Beide sind von der Gattung 'Zitronenfalter' – unterscheiden sich jedoch im Geschlecht. Die Anzahl und Reihenfolge der übergebenen Parameter beim Erzeugen muss mit Definition im Konstruktor übereinstimmen.

Ansprechen von Instanzen

Zugreifen und Ansprechen von Instanzen funktioniert über die Punktnotation. Instanz- und Attributs bzw. Methodenname werden dabei durch einen Punkt voneinander getrennt.

Attribute

Wo bei der Wertzuweisung von Variablen nur der Variablenname links neben dem = stand, taucht bei Attributen eine Kombination aus Instanz- und Attributsname auf. Das gleiche Prinzip gilt für das Auslesen von Attributen (siehe println im Beispiel).

InstanzName.AttributName = "WERT";
println (InstanzName.AttributName);

Methoden

Genau wie Attribute spricht man die Fähigkeiten von Klasseninstanzen durch eine Kombination aus Instanz- und Methodenname an. In der zweiten Zeile des Pseudocodes gibt die Methode der Instanz einen float Wert zurück, welcher in der Variable val abgelegt wird. Der Aufruf gestaltet sich demnach wie bei Funktionen, nur das vor dem Methodennamen explizit eine Instanz angegeben werden muss, auf welche diese ausgeführt werden soll.

InstanzName.MethodenName (Parameter der Methode);
float val = InstanzName.MethodenName (Parameter der Methode);

Die Schmetterlingsinstanz btW kann also auf diese Weise fliegen und landen:

bfW.fly ();
bfW.land ();

Da die Methoden in der Klasse keinen Rückgabewert haben und keine Parameterangaben zulassen, kann der Aufruf nur in dieser Form erfolgen.

Beispiele

Die Klasse »Ball«

Bsp.: Anlegen der Klasse

b15_ball_1.jpg Im ersten Beispiel legen wir die Klasse Ball in einem neuen Tab an. Dieses hält neben dem Klassenkörper die Attribute x, y und diameter für Position und Durchmesser. Im Sketch (L15_01_oop_ball1) legen wir die Instanz b global fest. Nachdem wir im setup()-Block das Sketchfenster eingerichtet haben, Erzeugen wir die Ballinstanz mit b = new Ball(); und füllen die Attribute mit Werten. Das weitere Programm macht nichts anderes, als uns eine Ellipse mit der Position und Größe des Balls abzubilden.

L15_01_oop_ball1 (Tab)
// Instanz 'b' der Klasse 'Ball'
Ball b;
 
void setup () {
  size (320, 240);
  smooth ();
 
  // Erzeugen der Instanz
  b = new Ball ();
  // Füllen der Attribute
  b.x = 120;
  b.y = 140;
  b.diameter = 90;
}
 
void draw () {
  background (0);
  // Zeichnen des Balls durch Auslesen
  // der Instanzeigenschaften
  ellipse (b.x, b.y, b.diameter, b.diameter);
}
Ball (Tab)
class Ball {
  // Attribute der Klasse
  float x;
  float y;
  float diameter;
}

Bsp.: Anlegen mittels Konstruktor

b15_ball_2.jpg Da das 'manuelle' Befüllen der Instanz mit Werten eine relativ umständliche Angelegenheit ist, legen wir in der Klasse einen Konstruktor dafür an. Dieser wird automatisch bei der Erzeugung der Instanz von uns abgefragt. Visuell bestehen keine unterschiede zwischen diesem und dem ersten Beispiel.

L15_02_oop_ball2 (Tab)
// Instanz 'b' der Klasse 'Ball'
Ball b;
 
void setup () {
  size (320, 240);
  smooth ();
  // Erzeugen der Instanz und gleichzeitiges 
  // Füllen der Attribute durch den Konstruktor
  b = new Ball (120, 140, 90);
}
 
void draw () {
  background (0);
  ellipse (b.x, b.y, b.diameter, b.diameter);
}
Ball (Tab)
class Ball {
  // Attribute der Klasse
  float x;
  float y;
  float diameter;
 
  // Konstruktor der Klasse 'Ball'
  Ball (float theX, float theY, float theDiameter) {
    x = theX;
    y = theY;
    diameter = theDiameter;
  }
}

Bsp.: Bewegen durch die Methode »move«

b15_ball_3.jpg Momentan besteht die Klasse nur aus Eigenschaftsdefinitionen. Sie ist nur brauchbar um Werte/Charakteristika abzulegen bzw. auszulesen. In diesem Schritt wird eine Methode (Fähigkeit) zum Bewegen des Balls festgelegt. Platziert im Klassenkörper hat sie den Namen 'move' und besitzt keinen Rückgabewert und keine Parameter. Innerhalb dieser Methode wird der Wert von x um 1 erhöht und wenn notwendig auf 0 zurückgesetzt. Bei jedem draw() Durchlauf wird b.move(); aufgerufen, was eine Bewegung des Ball von links nach rechts zur Folge hat.

L15_03_oop_ball3 (Tab)
Ball b;
 
void setup () {
  size (320, 240);
  smooth ();
  b = new Ball (120, 140, 90);
}
 
void draw () {
  background (0);
  b.move ();
  ellipse (b.x, b.y, b.diameter, b.diameter);
}
Ball (Tab)
class Ball {
  float x;
  float y;
  float diameter;
 
  Ball (float theX, float theY, float theDiameter) {
    x = theX;
    y = theY;
    diameter = theDiameter;
  }
 
  void move () {
    x = x + 1;
    if (x > width) {
      x = 0;
    }
  }
}

Bsp.: Array von Bällen

b15_ball_5.jpg Beispiel Nummer vier demonstriert die Klasse Ball unter Verwendung eines Arrays. Zwei for-Schleifen dienen dabei das Array im setup() zu füllen und draw() auszulesen. Innerhalb der ersten Schleife ändert die Zählvariable i die Startpositionen der einzelnen Bälle.

L15_05_oop_ball5 (Tab)
// Anlegen des Ball-Arrays
Ball b[] = new Ball[20];
 
void setup () {
  size (320, 240);
  smooth ();
  // Erzeugen aller Ballinstanzen
  for (int i=0; i < b.length; i++) {
    b[i] = new Ball (i * 15, 20 + i * 10, 10);
  }
}
 
void draw () {
  background (0);
  // für jede Ballinstanz
  for (int i=0; i < b.length; i++) {
    // Bewegen des Balls
    b[i].move ();
    // Darstellen im Sketchfenster
    ellipse(b[i].x, b[i].y, b[i].diameter, b[i].diameter);
  }
}
Ball (Tab)
class Ball {
  float x;
  float y;
  float diameter;
 
  Ball (float theX, float theY, float theDiameter) {
    x = theX;
    y = theY;
    diameter = theDiameter;
  }
 
  void move () {
    x = x + 1;
    if (x > width) {
      x = 0;
    }
  }
}

Gizmo Tierchen

Der folgende Beispielkomlex soll die schrittweise Entwicklung von lebewesenartigen Vehicels verdeutchlichen. Unter Hinzunahme der Vektor Klasse PVector werden Bewegungen im Sketchfenster simmuliert, ohne das die Objekte dabei die Zeichenfläche verlassen. Im ersten Schritt folgen die Gizmos einem selbstgesetzten Ziel. Kurz vor dem Erreichen wird dieses verschoben – sie bleiben in ständiger Bewegung. Da die erzielte geradlinige Positionsänderung unnatürlich wirkt, verwirft die zweite Version den Gedanken des festen Ziels. Stattdessen werden die Gizmos mit jeweils einer Richtung ausgestattet. Ebenfalls ein Vektor, variieren wir x und y Wert von Bild zu Bild, um ein konfuses Verhalten zu erzeugen.

Bsp.: zielstrebig

b15_gizmo1.jpg Durch eine non-lineare Animation streben 30 Gizmos über die Zeichenfläche. Sie folgen einem selbstgestecktem Ziel. Kurz vor dem Zielpunkt wird dieser neu definiert. Start- und Zielpositionen sind zufällig festgelegt.

L15_06_oop_gizmo1 (Tab)
Gizmo giz[] = new Gizmo[30];
 
void setup () {
  size (320, 240);
  background (0);
  stroke (255);
 
  for (int i=0; i < giz.length; i++) {
    float x = random (width);
    float y = random (height);
    giz[i] = new Gizmo (x, y);
  }
}
 
void draw () {
  for (int i=0; i < giz.length; i++) {
    giz[i].move ();
    point (giz[i].position.x, giz[i].position.y);
  }
}
Gizmo (Tab)
class Gizmo {
 
  PVector position;
  PVector target;
 
  Gizmo (float theX, float theY) {
    position = new PVector (theX, theY);
    target   = new PVector ();
    newRandomTarget ();
  }
 
  void move () {
    PVector step = new PVector ();
    step.set (position);
    step.sub (target);
    step.div (40);
    position.sub (step);
 
    if (position.dist (target) < 3) {
      newRandomTarget ();
    }
  }
 
  void newRandomTarget () {
    target.x = random (width);
    target.y = random (height);
  }
}

Bsp.: wandern

b15_gizmo2.jpg Im zweiten Teil wird das feste Ziel gegen eine Richtung getauscht. Diese Verändert sich von Bild zu Bild um einen Bereich von -0.15 bis 0.15 und wird auf die Position addiert. Damit die Geschwindigkeit der Gizmos konstat ist, führen wir normalize() vor der Addition auf die Richtung aus (damit Länge von 1, siehe Normalenvektor). Zum Schluss wird die aktuelle Position auf ein Verlassen der Zeichenfläche überprüft – bei Eintritt kehren wir die Richtung auf der entsprechenden Achse um.

L15_07_oop_gizmo2 (Tab)
Gizmo giz[] = new Gizmo[10];
 
void setup () {
  size (320, 240);
  background (0);
  stroke (255);
 
  for (int i=0; i < giz.length; i++) {
    float x = random (width);
    float y = random (height);
    giz[i] = new Gizmo (x, y);
  }
}
 
void draw () {
  noStroke ();
  fill (0, 2);
  rect (0, 0, width, height);
 
  stroke (255);
  for (int i=0; i < giz.length; i++) {
    giz[i].move ();
    point (giz[i].position.x, giz[i].position.y);
  }
}
Gizmo (Tab)
class Gizmo {
 
  PVector position;
  PVector direction;
 
  float spin = 0.15;
 
  Gizmo (float theX, float theY) {
    position    = new PVector (theX, theY);
    direction   = new PVector ();
    direction.x = random (-1, 1);
    direction.y = random (-1, 1);
  }
 
  void move () {
    direction.x += random (-spin, spin);
    direction.y += random (-spin, spin);
    direction.normalize ();
    position.add (direction);
 
    if (position.x < 0 || position.x > width) {
      direction.x *= -1;
    }
    if (position.y < 0 || position.y > height) {
      direction.y *= -1;
    }
  }
}

Bsp.: wandern (farbig)

b15_gizmo3.jpg Sketch drei färbt die Gizmos in vollem Spektrum ein. Der HSB-Farbraum eignet sich für dieses Anliegen besonders (colorMode (HSB, 255);), da wir nur den Farbton Ändern und Sättigung und Helligkeit konstant beleiben. Klasse 'Gizmo' wird für die Farbtonberechnung um die Methode getHue() erweitert. In ihr berechnet sich die Farbe aus der Lage des Vektors direction. Demnach erhält jede Richtung ihren eigenen Farbton.

L15_08_oop_gizmo3 (Tab)
Gizmo giz[] = new Gizmo[10];
 
void setup () {
  size (320, 240);
  colorMode (HSB, 255);
  background (0);
  stroke (255);
 
  for (int i=0; i < giz.length; i++) {
    float x = random (width);
    float y = random (height);
    giz[i] = new Gizmo (x, y);
  }
}
 
void draw () {
  noStroke ();
  fill (0, 2);
  rect (0, 0, width, height);
 
  for (int i=0; i < giz.length; i++) {
    giz[i].move ();
    stroke (giz[i].getHue (), 255, 255);
    point (giz[i].position.x, giz[i].position.y);
  }
}
Gizmo (Tab)
class Gizmo {
 
  PVector position;
  PVector direction;
 
  float spin = 0.15;
 
  Gizmo (float theX, float theY) {
    position    = new PVector (theX, theY);
    direction   = new PVector ();
    direction.x = random (-1, 1);
    direction.y = random (-1, 1);
  }
 
  void move () {
    direction.x += random (-spin, spin);
    direction.y += random (-spin, spin);
    direction.normalize ();
    position.add (direction);
 
    if (position.x < 0 || position.x > width) {
      direction.x *= -1;
    }
    if (position.y < 0 || position.y > height) {
      direction.y *= -1;
    }
  }
 
  int getHue () {
    PVector v = new PVector (0, 1);
    float a = PVector.angleBetween (v, direction);
    a /=  TWO_PI;
    return int (255 * a);
  }
}

Bsp.: wandern (Bild freilegen)

b15_gizmo4.jpg In diesem Sketch bestimmt ein zu Begin im setup() gelandenes Bild, ob die 'Gizmos' im draw() abgebildet werden. Mittig im Bild ist schwarz/weiß der Schriftzug 'gizmo' platziert. Nach dem Auslesen des Farbwertes im Bild an der Gizmo-Position mit get(), prüft eine if-Bedingung ob sich der Gizmo über einem Schriftzeichen befindet. Wenn 'ja' wird er gezeichnet – anderenfalls nicht. Das Bild wird nicht abgebildet.

L15_09_oop_gizmo4 (Tab)
Gizmo giz[] = new Gizmo[700];
PImage typo;
 
void setup () {
  size (320, 240);
  stroke (0);
  background (255);
  typo = loadImage ("background.jpg");
  for (int i=0; i < giz.length; i++) {
    float x = random (width);
    float y = random (height);
    giz[i] = new Gizmo (x, y);
  }
}
 
void draw () {
  noStroke ();
  fill (255, 30);
  rect (0, 0, width, height);
  stroke (0);
  for (int i=0; i < giz.length; i++) {
    giz[i].move ();
    int x = int (giz[i].position.x);
    int y = int (giz[i].position.y);
    color pixel = typo.get (x, y);
 
    if (brightness (pixel) < 40) {
      point (x, y);
    }
  }
}
Gizmo (Tab)
class Gizmo {
 
  PVector position;
  PVector direction;
 
  float spin = 0.15;
 
  Gizmo (float theX, float theY) {
    position    = new PVector (theX, theY);
    direction   = new PVector ();
    direction.x = random (-1, 1);
    direction.y = random (-1, 1);
  }
 
  void move () {
    direction.x += random (-spin, spin);
    direction.y += random (-spin, spin);
    direction.normalize ();
    position.add (direction);
 
    if (position.x < 0 || position.x > width) {
      direction.x *= -1;
    }
    if (position.y < 0 || position.y > height) {
      direction.y *= -1;
    }
  }
}