OO Objects
Einleitung
Wie in unserem letzten Bericht beschrieben, haben wir unser Ökosystem in mehrere Schichten unterteilt. Aus technischer Sicht war es eine Herausforderung, dass mehrere Instanzen der Schwarmschicht auf die statische, von allen nebenläufig verwendete Zonenschicht zugreifen können. Der folgende Artikel erläutert, welche Möglichkeiten es gibt, solche getrennten Schichten in Max/MSP überhaupt aufzubauen, wie diese miteinander kommunizieren, welche Probleme mit den von Max/MSP mitgelieferten Werkzeugen entstehen und wie diese mit der Softwarekomponente OO Objects von Mattijs Kneppers und John Pitcairn gelöst werden. Vorausgesetzt sind Kenntnisse in Max/MSP sowie in den Grundlagen der objektorientierten Programmierung.
Möglichkeiten der Objektorientierung in Max/MSP
Ein beliebtes Werkzeug der objektorientierten Programmierung ist die Kapselung von Funktionalitäten zu eigenständigen Objekten. Max/MSP bietet dazu zwei Möglichkeiten: patcher-Objekte und Abstractions.
In einem Patch können jederzeit durch die Erzeugung eines patcher-Objekts (oder kurz p) Funktionen ausgelagert werden. Die Möglichkeit, sie wiederzuverwenden, ist jedoch eingeschränkt: Man kann patcher-Objekte zwar beliebig oft vervielfältigen, aber jede Instanz steht für sich. Eine Änderung an einer Kopie wirkt sich weder auf das ursprünglich kopierte Objekt noch auf dessen andere Kopien aus.
Abstractions sind Patches, die im Gegensatz zu patcher-Objekten separat auf der Festplatte in einer eigenen Datei gespeichert werden. Sie gliedern sich nahtlos in die bereits bestehenden Objekte aus der Max-Bibliothek ein, da sie als eigene Objekte in jedem Patch ergänzend bereitgestellt werden. Ein solches Objekt versteht man am besten als eine Instanz dieser Abstraction: Änderungen an der Abstraction wirken sich direkt auf alle ihre Instanzen aus. Instanziierte Objekte können daher nicht bearbeitet werden, lassen sich aber trotzdem einzeln lesend öffnen, schließlich haben sie unterschiedliche Identitäten und Zustände.
Beide Möglichkeiten können durch eine beliebige Anzahl von Inlets beliebige Werte von außen entgegennehmen und diese durch die ebenfalls beliebige Anzahl von Outlets nach der Berechnung wieder zurückgeben. In der Objektorientierung entspricht das einem Methodenaufruf – die Implementierung von Max/MSP ist jedoch nicht besonders mächtig: Es gibt weder Sichtbarkeiten noch nebenläufigen Zugriff. Besonders der letzte Punkt schmerzt, da es unmöglich ist, gemeinsam genutze Funktionen durch ein einmalig erzeugtes Max-Objekt statisch für alle Beteiligten zur Verfügung zu stellen. Stattdessen muss an jeder Stelle, an welcher dieser Programmteil genutzt werden soll, eine Kopie des patcher-Objekts oder eben eine neue Instanz einer Abstraction erstellt werden. Letzteres führt bei häufiger Verwendung zu langen Ladezeiten des Patches und sollte daher vermieden werden.
Ein weiteres Problem resultiert aus der Verwendung von send- und receive-Objekten: Wenn man ein Objekt kopiert, das intern diese beiden Mechanismen verwendet, landet ein einmal versendeter Wert bei allen receive-Objekten, die den Zielnamen tragen, also bei allen Kopien gleichzeitig – dieses Verhalten ist in der Regel unerwünscht. Eine manuelle Umbenennung der jeweiligen Quell- und Zielnamen in den einzelnen patcher-Objekten ist untragbar aufwändig, daher bleibt kaum eine andere Wahl, als Strippen zu verwenden, was je nach Komplexität sehr unübersichtlich werden kann. Für Abstractions gibt es einen besseren Mechanismus: Ein vor die Namen gesetztes “#0″ wird bei der Instanzierung durch einen einzigartigen Wert ersetzt. Somit können alle Abstractions unabhängig voneinander arbeiten. Diese Möglichkeit schließt sich aber aufgrund der oben gestellten Forderung nach möglichst wenig Abstractions jedoch wieder aus.
OO Objects
Die beiden Max-Entwickler Mattijs Kneppers und John Pitcairn haben diese Probleme ebenfalls erkannt und eine einfache und performante Lösung geschaffen: Die OO Objects, welche man unter http://www.cycling74.com/twiki/bin/view/Share/MattijsKneppers herunterladen kann. Die aktuelle Version 0.41 gibt es für Max5 sowohl für Mac OS als auch für Windows – Max4 wird derzeit jedoch nur unter Mac OS unterstützt.
Wer bislang mit dem Namen OO Objects ein ganzes Arsenal neuer Objekte verbunden hat, wird vielleicht enttäuscht sein, dass nach der Installation lediglich zwei neue Objekte verfügbar sind: oo.method und oo.call. Diese beiden Objekte reichen allerdings aus, um unsere Ziele zu erreichen. Mit oo.method deklariert man Methoden, mit oo.call ruft man diese auf.
Instanzen von oo.method können an beliebiger Stelle erzeugt werden. Damit man die Methode aufrufen kann, benötigt sie einen Namen, welcher als Parameter übergeben wird.
Eine Methode lässt sich nicht durch Strippen oder send- und receive-Objekte aufrufen; der Aufruf findet durch das Objekt oo.call statt. Als Parameter wird der Name der Methode übergeben, welche aufgerufen werden soll.
Das Objekt oo.call hat zwei Inlets und zwei Outlets. Für uns spielen im Moment nur das erste Inlet und das erste Outlet eine Rolle. Wird ein Wert von einem beliebigen Typ an das erste Inlet gesendet, wird die entsprechende Methode mit diesem Wert als Parameter aufgerufen – ein Bang entspricht hier einem Methodenaufruf ohne Parameter. Mit dem ersten Outlet wird der optionale Rückgabewert der Methode empfangen.
oo.method hat nur ein Inlet und ein Outlet. Durch das Outlet wird der Parameter von oo.call geleitet. Was danach kommt, bleibt ganz dem Entwickler überlassen. Um einen Wert zurückzugeben, muss dieser an das erste Inlet von oo.method weitergeleitet werden. Für Max-Verhältnisse sieht das etwas eigenartig aus, drängt sich doch der Verdacht auf, eine Endlosschleife zu produzieren. Dem ist aber nicht so: Man sollte die vermeintliche Rückkopplung als Abschluss eines Methodencontainers begreifen. OO Objects kümmert sich darum, dass jeder Aufrufende seinen individuellen Rückgabewert erhält.
Es folgt ein einfaches Beispiel, das drei Methoden m1, m2 und m3 in unterschiedlichen Situationen deklariert. m1 ist direkt in unser Patch eingebettet, m2 in einem patcher-Objekt p1 und m3 in einer Abstraction. Unabhängig davon lassen sich alle drei Methoden an beliebiger Stelle aufrufen: Dazu wird entweder ein Bang oder ein konkreter Wert an das jeweilige oo.call-Objekt gesendet. Am Beispiel m2 sieht man, dass Rückgabewerte optional sind.

Dass sich alle drei Methoden an beliebiger Stelle aufrufen lassen können, ist eher nachteilig. Die Methoden stehen ohne Ordnung und Struktur frei im Raum. Bei komplexen Patches mit vielen unterschiedlichen Komponenten geht nicht hervor, zu welcher Teilkomponente die jeweilige Methode gehört.
Aus diesem Grund führt OO Objects ein neues Attribut @oo ein, welches der Strukturierung von Methoden dient. In der klassischen objektorientierten Programmierung gehört eine Methode immer zu einem bestimmten Objekt. In Max/MSP wäre das jedoch durch die zahlreiche Verwendung von patcher-Objekten und Abstractions, die zwar einerseits für sich alleine stehen, aber auch zu einer bestimmten Sammlung von Variablen und Methoden gehören können, eher unvorteilhaft – das Scoping-Prinzip eignet sich in diesem Kontext wesentlich besser.
Ein Scope fasst Methoden mit ihrem Namen unter einer gemeinsamen Adresse zusammen und stellt diese nach außen zur Verfügung. In diesem gemeinsamen Adressraum darf keine Methode namentlich doppelt vorhanden sein. Auch wenn man nicht explizit einen Scope deklariert, gibt es standardmäßig immer einen: Er heißt oo_top und wir haben ihn im obigen Beispiel bereits implizit kennengelernt. Er bildet den Wurzelknoten einer Hierarchie aus unterschiedlichen Scopes. Unterhalb des Wurzelknotens können beliebig viele weitere Scopes erzeugt werden. Um einen neuen Scope zu erzeugen, muss man bei patcher-Objekten oder Abstractions nach dem jeweiligen Objektnamen das Attribut @oo setzen. Weist man dem Attribut @oo einen Wert zu, wird ein neuer Scope erzeugt, der genau dem Namen des übergebenen Wertes entspricht.
Alle Methoden, die innerhalb des Patchers myPatch und somit innerhalb des neuen Scopes myScope deklariert werden, werden nun unter diesem Scope zusammengefasst und sind nach außen nur noch über einen eindeutigen Pfad erreichbar. Befindet sich eine Methode nicht im selben Scope wie der Methodenaufruf, muss über den Pfad darauf zugegriffen werden. Im obigen Beispiel konnten wir die Methoden direkt mit dem Namen ansprechen, da sich Aufrufer und Methode im selben Scope befinden. Jetzt ist das nicht mehr der Fall. Die Syntax des oo.call-Objekts lautet also folgendermaßen:
Der Pfad zur jeweiligen Methode ergibt sich aus der aktuellen Position in der Scope-Hierarchie: Wird auf eine Methode zugegriffen, die sich innerhalb des selben Scopes befindet, genügt es, den Namen der Methode anzugeben. Soll eine Methode aufgerufen werden, die sich im übergeordneten Scope befindet, muss dem Namen der Methode ein “parent.” vorangesetzt werden (oder kurz “.”, was Platz spart). Eine im untergeordneten Scope deklarierte Methode erreicht man über das Voranstellen des Scope-Namens. In dieser Pfadsprache werden Scopes durch einen Punkt voneinander getrennt.
Versuchen wir nun ausgehend vom Standard-Scope oo_top eine Methode m1 des Scopes s1 aufzurufen, lautet der Pfad s1.m1 – also oo.call s1.m1. Lässt sich die Methode jetzt aufrufen? Nein! Man kann zwar problemlos eine Methode aufrufen, die im gleichen Scope wie der Aufrufende deklariert ist, der Aufruf einer sich in einem anderen Scope befindlichen Methode scheitert jedoch. Dieses Verhalten erklärt sich aus der Sichtbarkeit einer Methode: Diese ist standardmäßig private, was bedeutet, dass Methoden nur innerhalb des selben Scopes aufgerufen können. Um eine Methode auch nach außen zur Verfügung zu stellen, muss diese ausdrücklich mit public gekennzeichnet werden:
Auch Scopes können eine Sichtbarkeit haben: Sie spielt dann eine Rolle, wenn über mehrere Scope-Grenzen hinweg agiert wird – z.B. wenn ausgehend von einem in oo_top deklarierten Scope s2 unsere Methode m1 aus Scope s1 aufgerufen wird, welcher ebenfalls in oo_top deklariert ist. Man muss nun den Umweg über oo_top gehen, der Pfad lautet also parent.s1.m1 (oder kurz .s1.m1). Die Sichtbarkeit von Scopes ist standardmäßig ebenfalls private.
Das folgende Beispiel entspricht dem obigen Beispiel, jedoch wurden die Methoden m2 und m3 in eigene Scopes s1 und s2 ausgelagert.

Beachten sollte man, dass sich eine Änderung an den Methoden und Scopes in der aktuellen Version 0.41 nicht unmittelbar auswirkt. Dazu müssen die Objekte neu initialisiert werden. Das funktioniert am besten, indem man diese kurz löscht und diesen Schritt danach rückgängig macht. Erst dann werden die Änderungen berücksichtigt.
Fazit
Mit der beschriebenen Herangehensweise haben wir unsere Schwarmschicht so aufgebaut, dass sie in einem Scope mehrere Methoden zur Verfügung stellt, auf welche die Instanzen der Schwarmschicht nebenläufig und threadsicher zugreifen können. Die angesprochenen Grundlagen über die OO Objects sollen es jedem Max-Entwickler ermöglichen, eine komplexe Anwendung wesentlich zu vereinfachen und besser zu strukturieren – also wartbarer und verständlicher zu machen. Um mehr über das Thema zu erfahren, lohnt sich ein Blick in die Helpfiles: Dort werden fortgeschrittene Themen wie z.B. Referenzen behandelt.