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

Seite 3: Typisierung in Moose

Inhaltsverzeichnis

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;