Moose: Eine (post-)moderne OOP-Erweiterung für Perl

Weit mehr als ein einfaches CPAN-Modul, ist Moose die vielleicht wichtigste Spracherweiterung für Perl der letzten Jahre. Einige Anwender fragen bereits scherzhaft, ob der Elch (englisch Moose) das neue Kamel (Maskottchen und Titelbild des ersten Perlbuches) sei.

In Pocket speichern vorlesen Druckansicht 52 Kommentare lesen
Lesezeit: 18 Min.
Von
  • Herbert Breunung
Inhaltsverzeichnis

Weit mehr als ein einfaches CPAN-Modul, ist Moose die vielleicht wichtigste Spracherweiterung für Perl der letzten Jahre. Einige Anwender fragen bereits scherzhaft, ob der Elch (englisch Moose) das neue Kamel (Maskottchen und Titelbild des ersten Perlbuches) sei.

Tatsächlich verändert Moose bei tieferer Nutzung den Entwurf und das Aussehen von Programmen weit stärker als alle großen Änderungen seit Version 5.10 der Skriptsprache Perl zusammengenommen. Es bietet deutlich mehr als "Objekte, wie in Java", sondern auch Typisierung, Constraints (Subtypen), Delegation und Teilklassen. Sogar das Objektmodell selber lässt sich ändern und bietet auf den ersten Blick kaum überschaubare Möglichkeiten.

Mehr Infos

Die Wurzeln von Moose

Stevan Little, Hauptautor von Moose und Class::MOP, war zuvor an Pugs beteiligt (mehr Infos zum System auch in der Wikipedia). Dieses vom Spieltrieb und Audrey Tang geprägte Projekt, aus dem viele weitere Systeme hervorgingen, führte zum ersten Interpreter und Compiler, der größere Teile der Perl-6-Syntax ausführen konnte.

Die frühen Perl-6-Programme machten Stevan Little die großen Unterschiede zwischen den Sprachversionen 5 und 6 schmerzlich bewusst, was ihn schließlich zu Moose motivierte.

Das mit dem System eingeführte Meta-Objekt-Protokoll ist von Common Lisp beeinflusst, Funktionalität und Syntax sind wiederum meist Perl 6 nachempfunden (mehr zu Perl 6 auf heise Developer), soweit es innerhalb der Grenzen von Perl 5 möglich ist.

Nun gibt es Objektorientierung (kurz OOP) in Perl bereits seit 1994, in einer an das ältere Python angelehnten Semantik, welche sich fast vollständig auf grundlegende Funktionalität stützt. Damit sind Objekte ein wenig mehr als Referenzen die mit einem Namensraum verknüpft sind, dessen Routinen die Methoden darstellen. Damian Conway, seinerzeit Lehrstuhlinhaber für OOP, zeigte 1999 in seinem Klassiker "Object Oriented Perl", dass die Sprache hier sehr vielseitig und mächtig ist. Jedoch widersprach der objektorientierte Ansatz dem eigenen Designprinzip, einfache Dinge auch einfach zu halten – manch üblicher Handgriff wie das Schreiben einer new-Methode verlangte (unübliche) Detailarbeit und Anfängern unnötig viel Wissen ab.

Rund 36 Module im CPAN, die das Problem zu lösen trachten, sind ein deutliches Symptom für Handlungsbedarf. Dass die Verbesserungsansätze kaum zueinander kompatibel sind, verschlimmert die Situation zusätzlich. Selbst Damian Conways Class::Std konnte sich nicht durchsetzen, da es die Objekte "inside-out“ implementiert und daher ebenfalls nicht kompatibel ist.

Moose scheint dieses Kapitel abzuschließen und entwickelte sich während der letzten drei Jahre zum beliebtesten Rahmenwerk für OOP in Perl. Selbst bei so manchem Veteranen hat Moose das Interesse an Perl neu entfacht und auf der letzten OSCON war es eines von zwei Themen, das gleich doppelt vertreten war. Moose wurde sowohl von der Perl Foundation gefördert als auch von der Enlightend Perl Organisation in den Kanon der wichtigsten Module gewählt. Auf Perls zentraler Website und im vielbeachteten Buch "Modern Perl" wird es als der empfohlene Weg für das Erzeugen von Objekten ausgewiesen. Selbst die offiziellen Kerndokumentation wird es ab Version 5.16 empfehlen.

Firmen wie Yahoo!, Cisco und die BBC setzten es ein und auch Catalyst, Perls derzeit wichtigstes Web-Framework für größere Anwendungen (etwa mit Rails 3 vergleichbar), stellte vor etwa drei Jahren mit der Version 5.8 auf Moose um. Die jetzt von Ricardo Signes geführten Perl Porter beraten sogar darüber, wie viel der Moose-Syntax mit die nächsten beiden Sprachversionen in den Kern aufgenommen werden soll. Steven Little arbeitet deshalb an einem kleinen MOP für Perl 5, welches mit 5.18 in den Kern einfließen soll.

Es gibt jedoch auch gute Gründe auf Moose zu verzichten. Ein bereits vorhandener Codeberg ist kein solcher Grund. Moose-Klassen lassen sich vollständig wie Klassen der alten Schule benutzen, inklusive Vererbung und aller UNIVERSAL-Methoden wie can und isa. Nur sollte man nicht vergessen, dass Moose-Akzessoren Methoden und keine Hash-Schlüssel sind, weshalb sie nun auch mit vererbt werden.

Für den umgekehrten Fall (Moose-Klasse erbt von einer Klasse im alten Stil) gibt es MooseX::NonMoose. Überhaupt ist das vielfältige und kreative Ökosystem an Erweiterungen im MooseX::*-Namensraum ein Argument für Moose. Das X am Wortende ist eine CPAN-Konvention für Module, die ohne Absprache mit dem Besitzer des Namensraumes (hier Moose) erstellt wurden.

Zum echten Showstopper kann der erhöhte Rechenaufwand zur Startzeit werden. Da dieser zum großen Teil von der Komplexität des Metaobjektprotokolls hervorgerufen wird, hat eine Gruppe vorwiegend asiatischer Programmierer mit Mouse ein fast kompatibles Moose ohne Class::MOP und die meisten anderen Abhängigkeiten geschaffen, weshalb man Mouse auch gerne als "Moose ohne Geweih" bezeichnet. Wer Moose::Any statt Moose verwendet, kann mit einer kleinen Änderung zwischen Moose und Mouse wechseln und von Fall zu Fall anhand eigener Benchmarks entscheiden.

Aber wenn selbst Mouse zu langsam ist oder weit weniger Funktionalität ausreicht, empfiehlt sich ein Griff zu Moo (minimalistische OO), das nur den Teil des "täglichen Bedarfs" von Moose mit einer leicht abweichenden Syntax implementiert.

Wesentlich länger gibt es die altehrwürdige und vielbenutzte Class::Accessor-Familie, mit der der Entwickler lediglich automatisch erzeugte Getter- und Setter-Methoden erhält. Die ungeschlagen schnellste und minimale Lösung ohne jegliche Abhängigkeiten stellt hier Object::Tiny dar.

Der Vorteil der autogenerierten Accessoren (Getter- und Setter-Methoden) war der Hauptentstehungsgrund der meisten OOP-Module, denn in nacktem Perl 5 braucht es mehrere kürzbare Zeilen, um Attribute zu erstellen. An private war ohne noch mehr "Boilerplate-Code" gar nicht zu denken.

Einem der neuen CPAN-Mottos folgend ("Hört auf die Räder neu zu erfinden — baut Raketen") würde eine kleine Klasse Rakete, die lediglich das Attribut Geschwindigkeit, sowie eine Methode zum Anhalten hat, wie folgt aussehen:

use strict;
use warnings;

package Rakete;

sub new {
my ($klasse, %parameter) = @_;
my $objekt = bless ({'v' => $parameter{geschwindigkeit} }, $klasse);
...
return $objekt;
}

sub geschwindigkeit {
my ($objekt, $v) = @_;
$objekt->{'v'} = $v if defined $v;
return $objekt->{'v'};
}

sub stop {
my ($objekt) = shift;
$objekt->{'v'} = 0;
}

Das Gleiche mit Moose:

package UFO;
use Moose;

has 'geschwindigkeit' => (is => 'rw');

sub stop {
my ($objekt) = shift;
$objekt->geschwindigkeit(0);
}

Die Ersparnis ist offensichtlich und geht weit über das Schreiben der Pragmas, des Konstruktors und der Accessoren hinaus. Oft genug täuschen Vergleiche minimaler Beispiele, da der Gewinn nur einen kleinen Teil der späteren Klassengröße ausmacht. Doch je mehr Funktionalität von Moose man verwendet, desto drastischer schrumpfen die Quelldateien.

Das 'rw' steht wie bei Datenträgerrohlingen auch für "read and write", 'ro' dementsprechend für "readonly" — der Wert solcher Attribute ließe sich nur einmalig während der Initialisierung setzen.

Sämtliche hier gezeigten Beispiele machen reichlich Gebrauch vom fetten Pfeil (=>). Das hat zwar auch visuelle Vorteile, erspart aber vor allem, das Vorangehende in Anführungszeichen zu setzen. Um sich Erklärungen zu sparen, gehört es mancherorts zum guten Ton, genau das aber trotzdem zu tun. Benutzt werden beide Klassen identisch.

   my $objekt = Klasse->new(geschwindigkeit => 20);
$objekt->stop;
say $objekt->geschwindigkeit; # gibt 0 aus
$objekt->geschwindigkeit(30);
say $objekt->geschwindigkeit; # 30

Da meist benannte Paramter in einer Hashreferenz übergeben wurden, die der Konstruktor mit my ($klasse, $parameter) = @_; aufnimmt, sieht man in altem Code jedoch häufig:

   my $arv = Rakete->new({geschwindigkeit => 20});

Moose reagiert auf beide Schreibweisen gleich.

Um die Syntax noch mehr nach bekannter OOP aussehen zu lassen, steht Entwicklern die Erweiterung MooseX::Declare zur Verfügung. Damit heisst eine Methode method und eine Klasse class, was darüber hinaus das use Moose; spart, das sonst nach jeder Package-Deklaration zu stehen hat, um aus dem Namensraum eine Moose-Klasse werden zu lassen. Manchmal fügt man auch no Moose; ein, um Befehle wie has zu deaktivieren.

   use MooseX::Declare;
class UFO {

has 'geschwindigkeit' => (is => 'rw');

method stop {
my ($objekt) = shift;
$objekt->geschwindigkeit(0);
}
}

# alternativ für kleine Methoden:
method stop { shift->geschwindigkeit(0) }

MooseX::Declare verlangt die Klasse in geschweifte Klammern zu legen und führt im Hintergrund noch ein paar andere Dinge aus. Es verhindert versehentliche Verschmutzung von Namensräumen mit namespace::autoclean und ruft das notorische

   __PACKAGE__->meta->make_immutable;

auf, das ansonsten am Ende der meisten Moose-Klassen steht. Mit dieser Zeile verzichtet die Klasse auf ihr Recht, sich zur Laufzeit zu verändern und gewinnt dafür an Geschwindigkeit. Ist das nicht gewünscht, sollte bei Verwendung von MooseX::Declare dem Klassennamen ein is mutable folgen.

Das vorangegangene Codebeispiel kommt dem Vorbild der Perl-6-Syntax bereits sehr nahe. Dort hieße es kurz und knapp:

   class UFO {

has Numeric $.geschwindigkeit is rw;

method stop { $.geschwindigkeit = 0 }
}

Das hinzugekommene Numeric stellt sicher, dass nur Zahlenwerte für die Geschwindigkeit angenommen werden. Da Perl 5 im Gegensatz zu Perl 6 keine Datentypen kennt, emuliert Moose dieses Verhalten mit Routinen, die eingehende Daten prüfen. Der Entwickler gibt das wie folgt in Auftrag:

   use Moose::Util::TypeConstraints;
has geschwindigkeit => (is => 'rw', isa => 'Num');

Eine Übersicht aller verwendbaren Datentypen befindet sich auf einem Spickzettel von Oliver Gorwits (PDF), weitere sind in MooseX::Types zusammengefasst. Aber wenn schon eine Prüfung, dann ein vollständige. Im dreidimensionalen Raum gibt es eine Höchstgeschwindigkeit und eine Rakete besitzt keinen Rückwärtsgang. Deswegen wäre für das Programmbeispiel ein Untertyp von Num angebracht, der die nicht anwendbaren Werte abweist.

   subtype 'NormalSpeed'
=> as 'Num'
=> where { $_ >= 0 and $_ <= 299792458 };

Auch wäre es praktisch, die Geschwindigkeit als Vektor angeben zu können. Moose sollte ihn automatisch in einen skalaren Wert umwandeln:

   coerce 'NormalSpeed'
=> from 'HashRef[Num]'
=> via { sqrt( $_->{'x'}**2 + $_->{'y'}**2 + $_->{'z'}**2) };

Diese Umwandlung muss man schließlich noch für jedes Attribut separat erlauben:

   has 'geschwindigkeit' => (... isa => 'NormalSpeed', coerce => 1);

Dann darf man:

   my $herz_aus_gold = UFO->new( );
$herz_aus_gold->geschwindigkeit( {x => 10000, y => 200, z => 300} );

Selbstverständlich wird die Prüfung, ob die Geschwindigkeit im erlaubten Bereich ist, nach der Umwandlung vorgenommen. Für einen interstellaren Kurzurlaub ist dieses Limit leider weniger praktisch, doch es gibt ja noch den Hyperraum. In ihm zu navigieren ist nicht ohne, da man sich in mehrere Richtungen in unterschiedlichen Geschwindigkeiten zur gleichen Zeit bewegt. Es soll hier aber nicht um die technischen Details, sondern nur um die Arrayreferenz (ArrayRef) gehen, die die einzelnen Geschwindigkeiten speichert. Der Typ wurde hier gewählt, um die hochdimensionalen Geschwindigkeitsangaben von denen im dreidimensionalen Raum unterscheiden zu können (die als Hashreferenz ankommen). Hier wäre es überflüssig, die Arrayreferenz aus Zahlen vom Datentyp "HyperSpeed" zu definieren. Damit das Attribut beide Datentypen annimmt, braucht es:

   has 'geschwindigkeit' => (... isa => 'NormalSpeed|ArrayRef[Num]');

Natürlich darf jede mit Standardobjekten kompatible Klasse auch Typ eines Attributes sein. Ein Beispiel dafür ist DateTime, dass die Zeitanzeige in den schicken Holoarmaturen der Rakete speisen kann, oder zusätzlich den Vermerk über den Zeitpunkt des Baus enthält, der sich trotz relativistischer Effekte nicht ändert und deshalb "readonly" ist:

   has 'erbaut' => (
is => 'ro',
isa => 'DateTime',
default => sub {DateTime->now},
);

default bestimmt den Standardwert für das Attribut, der hier durch einen Aufruf der anonymen sub bei der Objekterzeugung vorgegeben wird, falls kein eigener Wert zugewiesen ist. Wäre erbaut als lazy => 1, markiert, geschähe die Zuweisung erst vor der ersten Abfrage.

Den Typ DateTime erstellt das Moose-System nebenbei. Falls Subtypen abgeleitet werden sollen, müsste er explizit erzeugt werden:

   subtype 'DateTime'
=> as 'Object'
=> where { $_->isa('DateTime') };

Natürlich dürfen alle Methoden von DateTime benutzt werden:

   say $herz_aus_gold->erbaut->year;

Da das im Einzelfall weniger schön ausfallen kann, als in diesem Beispiel, lassen sich auch automatisch Methoden erzeugen, die dann an Attribute weitergeleitet werden können. In der Literatur ist diese Technik als Delegation bekannt:

   has 'erbaut' => (... handles => 'year');
# oder gleich
... handles => [qw/day month year/]);

Das ermöglicht ein $herz_aus_gold->day, oder ...->month und ...->year. Effektiver ist es, Methoden mit einem regulären Ausdruck gleich im Dutzend umzuleiten:

   ... handles => qr/^y.*r$/

In diesem Fall betrifft es diejenigen Methoden, die mit y anfangen und r enden. Auch das handles => 'year' lässt sich noch optimieren, beispielsweise mit einem treffenden Namen:

   ... handles => { baujahr => 'year' }

Damit ruft $herz_ausgold->baujahr letztlich $herz_aus_gold->erbaut->year auf. Für Entwickler eröffnen sich noch weit mehr Möglichkeiten, wenn man die Delegation mit Currying kombiniert, dem Aufrufen von Funktionen mit feststehenden Paramterwerten. Wenn die Methode year einen Parameter annehmen würde, der das Ausgabeformat bestimmt, dann könnte man festlegen, dass das Baujahr in voller Länge gefragt ist:

       handles => { baujahr => [ year => 'long' ] },

Die möglichen Parameter für $obj->baujahr(...) folgen nach long in sub year.

Da der Getter/Setter auch "nur" eine automatisch generierte Methode ist, lässt sich sein Name ebenfalls frei wählen. Mit reader und writer steht er sogar als separate Methode für das Lesen und Schreiben zur Verfügung. Dabei richtet sich writer nicht danach, ob es ein schreibgeschütztes Attribut werden sollte. Möchte man lediglich das übliche und von Conway empfohlene Duo get_name/set_name verwenden, reicht ein use MooseX::FollowPBP;.

Mittels predicate kann der Entwickler eine Prädikat-Methode erzeugen lassen, die den Wert 1 zurückliefert, wenn das zugehörige Attribut einen Inhalt hat. Es ist Konvention, die Prädikat-Methode mit has_ + (Name des Attributes) zu benennen. Sie macht selbst dann Sinn, wenn das Attribut notwendig (bei new anzugeben) oder schreibgeschützt ist, da eine mit clearer erzeugte Methode das Attribut in einen unbenutzten Zustand zurückführen kann. Die mit trigger bestimmte Subroutine wird nach jeder Änderung des Attributwertes ausgeführt und bekommt das Objekt und den neuen Wert als Parameter.

Interessant sind noch zwei weitere Attribut-Modifikatoren: builder und lazy_build. Ersterer erinnert an default, bestimmt jedoch eine Routine, die Startwerte für mehrere Attribute "baut". Der zweite Modifikator sorgt nicht nur dafür, dass das so spät wie möglich geschieht, sondern aktiviert gleich fünf der beliebtesten Voreinstellungen. Ein

   has farbe => ( lazy_build => 1 )

entspricht damit:

   has farbe => (
lazy => 1,
required => 1,
builder => '_build_farbe',
clearer => 'clear_farbe',
predicate => 'has_farbe',
)

Ob das Raumschiff schon gestrichen wurde kontrolliert somit ein:

   if ($ufo->has_farbe) { …

War die Malerkollone bisher nur mit den Pausenbroten beschäftigt, ermuntert sie folgendes zur Arbeit:

   $ufo->_build_farbe;

Mitunter haben vererbte Attribute ungewollte Eigenschaften, die sich mit einem + vor den Namen in der erbenden Klasse jedoch überschreiben lassen:

   has '+farbe' => {
lazy => 0,
builder => 'farbgenerator',
}

In der neues Raumschiffklasse verhält sich farbe wie bekannt, nur wird $ufo->farbgenerator anstatt $ufo->_build_farbe; schon während der Objekterzeugung aufgerufen.

Erwartungsgemäß lassen sich auch die erhaltenen Methoden überschreiben, sogar mit Routinen anderen Namens:

   override 'stop' => \&passagierfreundlicher_halt;

Die abgeleitete Klasse kennt somit keine Methode stopsondern etwas wesentlich komfortableres. Oft genügt es auch schon, nur ein wenig Funktionalität hinzuzufügen:

   before 'stop' => sub {
shift->bordfunk('Bitte schnallen sie sich an!');
};

und vielleicht sogar:

   after 'stop' => sub { shift->standlicht(1); };

Diese beiden Erweiterungen kann man nun zusammenfassen:

   around 'stop' => sub {
my $original = shift;
my $self = shift;
$self->bordfunk('Bitte schnallen sie sich an!');
super();
$self->standlicht(1);
}

super() ruf die Orginalmethode auf, mit den gleichen Argumenten. Wer mehr Kontrolle benötigt, greift zu $self->SUPER::stop() oder $self->$original(). Auf diese Art lassen sich Methoden beliebig oft "einwickeln" (der Perl-6-Befehl dafür heisst wrap). Der umgekehrte Weg steht Entwicklern ebenfalls offen. In der Variante schreibt man in die Elternklasse:

   sub stop {
$self->bordfunk('Bitte schnallen sie sich an!');
inner();
$self->standlicht(1);
}

Die Erben können dann entscheiden, ob sie hart oder sanft bremsen wollen, indem sie stop
individuell implementieren. Bordfunk und Standlicht wird davon unabhängig auf jeden Fall betätigt, wobei Erben die Ausführung mit inner() beliebig weiterreichen können:

   augment 'stop' => sub {
my $self = shift;
my $a = shift; # Beschleunigung
$self->geschwindigkeit( $self->geschwindigkeit - $a)
while $self->geschwindigkeit > 0;
$self->geschwindigkeit(0);
};

Da die Beschleunigung nur numerisch sein darf, bietet sich auch hier eine Typenprüfung an. Der Autor empfiehlt den Einsatz von MooseX::Method::Signatures statt MooseX::Params::Validate. Letzteres wirkt ein wenig umständlich und weniger leistungsfähig. Zudem wird MooseX::Method::Signatures zusammen mit dem empfohlenen MooseX::Declare geladen. Es nähert die Syntax noch einmal ein gutes Stück an Perl 6 an und spart das notorische my $self = shift;

   method stop (Num :$a! = 1, DateTime $wann? where {$_ >= DateTime->now}) { ...

Der Doppelpunkt markiert benannte Parameter. Durch das Ausrufezeichen wird eine Anwesenheit eines Argumentes erzwungen. Per Default sind benannte Parameter optional, positionale (ohne Doppelpunkt) sind es nicht. Auf diesem Weg lassen sich selbst Subtypen und Default-Werte gleich in der Signatur ausweisen. Sie funktionieren auch noch nach der Vererbung, selbst wenn der Erbe kein MooseX::Method::Signatures einsetzt.

Moose-Objekte erben mit extends:

   class UFO extends Excalibur { ...

Der Befehl extends steht normalerweise in der Klasse, sofern nicht das angetragene MooseX::Declare zum Einsatz kommt. Mit der beliebten Erweiterung darf extends auch in der Klassendefinition stehen. Eine Mehrfachvererbung erfolgt immer innerhalb der Klasse nach dem Muster:

   extends 'Excalibur', 'White Star';

Ein erneuter Aufruf von extends würde diese Definition überschreiben. Zur Vererbung und weiteren Moose-Themen ließe sich noch weit mehr sagen, was den Artikel zu epischer Länge dehnen würde. Ein zweiter Artikel wird deshalb das interessante Konzept der Rollen (Roles) vorstellen, das über Moose in die Perl-5-Welt Einzug hielt.

Herbert Breunung
schreibt regelmäßig Artikel über Applikationsentwicklung in Perl und Perl 6 und spricht auf Konferenzen im In- und Ausland. Er führt seit Jahren ein freies Softwareprojekt und ist am Aufbau der Perl-6-Dokumentation beteiligt.
(rl)