Roles und Signaturen mit Moose: Moderne Erweiterungen für Perl 5

Seite 2: Rollen

Inhaltsverzeichnis

Doch Larry Wall hat noch weit größere Ambitionen. Zur diesjährigen YAPC::EU in Kiew stellte der Perl-Erfinder in seiner Keynote erneut klar, dass sich Perl 6 nur nennen dürfe, was möglichst alle grundlegenden Aufgaben der Programmierer besser löst als Perl 5, inklusive der Organisation von Abstraktion. In den 80er-Jahren kamen dafür Objekte in Mode. Sie wurden teils übertrieben als das Mittel zur Produktivitätssteigerung beworben. Denn wie die gefürchteten Polsterjacketts dieser Ära ist der Umgang mit Klassen, und vor allen ausufernden Klassenhierarchien, klobiger als erhofft. Der Aufstieg (teil-)funktionaler Sprachen wie Scala, F# oder Haskell und die Einführung funktionaler Mittel in Sprachen wie C# und Java (Lambdas) ist ein Symptom dafür. Andere Ansätze sind Aspekte und Traits. Besonders letztere finden gerade Einzug in zunehmend mehr Sprachen, da sie ein fundamentales Problem von Klassen angehen.

Da Klassen in eine strikte Vererbungshierarchie eingebunden sind, ist die Wiederverwendung ihrer Attribute und Methoden nur eingeschränkt möglich. Bisher stand man vor der Wahl, sich entweder mit einer Einfachvererbung schmerzlich einzuschränken und Codeduplikation quasi zu provozieren. Oder man lockerte die Schlinge etwas und fing sich Konflikte mit kollidierenden Namen ein.

Traits helfen hierbei aus, weil sie außerhalb der Klassenhierarchie stehen. Eine Klasse kann beliebig viele Traits konsumieren (ihre Methoden aufnehmen) und Konflikte durch Ablehnen oder Umbenennen auflösen. Das lässt sich mit Einfach- und Mehrfachvererbung kombinieren.

Die diesem Konzept ähnlichen Mixins können den Widerstreit zweier gleichnamiger Methoden nicht schlichten. Das war wohl der Hauptgrund für die mit Ruby 2.0 eingeführten Refinements, die das stille Überschreiben von Methoden lexikalisch begrenzen. Die Begriffe Mixin und Trait werden jedoch auch abweichend verwendet (wie bei D), da es keine offizielle Konvention dafür gibt.

Perl 6 (und Moose) vermied es, zu dieser Sprachverwirrung beizutragen und nannte seine Traits "Roles" (Rollen), da sie im Gegensatz zu bisherigen Entwürfen auch Attribute enthalten oder einfordern dürfen. Das macht Sinn, weil funktionell verwandte Methoden normalerweise über gemeinsam verwendete Attribute kommunizieren, die sonst in jeder, den Trait verwendenden Klasse neu zu schreiben wären. Konflikte zwischen gleichnamigen Attributen verschiedener Rollen lassen sich derzeit nur in Perl 6, nicht aber mit Moose lösen. Sie werden jedoch wie Methodenkonflikte zur Kompilierzeit erkannt und erzeugen Fehler.

Rollen sind (möglichst kleine) Einheiten der Wiederverwendung, Klassen (möglichst vollständige) Einheiten der Komposition und Instanziierung. Folglich lässt sich aus einer Rolle nicht direkt ein Objekt bilden, wohl vermögen Objekte zur Laufzeit Rollen zu konsumieren. Eine andere Konsequenz dieser Arbeitsteilung ist: Abstrakte Klassen werden kaum noch benötigt und Vererbungshierarchien wesentlich flacher oder entfallen ganz. Da Rollen Methoden anfordern können, die sie selbst nicht implementieren, erfüllen sie somit auch die Aufgabe eines Interfaces. Einzig wenn die Implementierung an eine abgeleitete Klasse zu delegieren ist, braucht es abstrakte Klassen. Denn wenn eine Rolle "Handbremse" die Methode "stop" fordert, dann muss sie von der Klasse oder einer ihrer Rollen zum Zeitpunkt des Einfügens implementiert sein, um den Compiler-Fehler zu vermeiden.

Weil Rollen durch die vorgestellten Methoden-Modifikatoren befähigt sind, Methoden zu erweitern, können sie auch als Aspekte fungieren.

Das letzte Beispiel des ersten Teils erwähnt die Klasse "Excalibur", die von "Raumschiff" und "White Star" erben müsste. Eine weitere Besonderheit gegenüber gewöhnlichen Raumschiffen ist ihre auf Vorlonentechnologie basierende Hauptkanone.

package VorlonenKanone;
use Moose::Role;

requires 'energielevel';

has 'ladezustand' => (is => 'rw', isa => 'Num');

sub feuer { ...

Äußerlich sieht es fast wie eine Moose-Klasse aus. Es ist kein base oder parent zu bemühen, da die Ableitung von Moose::Role hinter dem Vorhang geschieht, wie bei use Moose; in Klassen auch. Durch requires wird das Attribut energielevel verlangt, (das ja auch "nur" eine Methode ist), da es von der Methode feuer verwendet wird.

Da die Serie leider vorzeitig abgesetzt wurde, blieb die Excalibur ein Einzelstück, weswegen die Rolle nur einem einzelnen Raumschiff zugewiesen werden braucht.

use Moose::Util qw( apply_all_roles );

my $excalibur = WhiteStar->new;
apply_all_roles( $excalibur, 'VorlonenKanone' );

Doch in einem besseren Paralleluniversum gibt es sicherlich eine Klasse neuartiger Ranger-Schiffe, in die die Rolle mit with eingefügt wird:

package Excalibur;
use Moose;

extends 'Raumschiff', 'White Star';

with 'VorlonenKanone', 'Gravitationsgenerator';
...

Der von den Centauri bereitgestellte Gravitationsgenerator sollte im gleichen Arbeitsgang eingebaut werden, da sich sonst Namenskonflikte nicht erkennen lassen. Das liegt an der derzeit noch begrenzten Fähigkeit von Perl 5, das Ende des BEGIN {...}-Phasers zu erkennen und gegebenenfalls zu diesem Zeitpunkt nach Konflikten zu suchen. Durch das Einfügen der Rollen in mehreren Schritten kann man der Kontrolle bewusst aus dem Weg gehen und selbst ungewöhnliche Konstrukte erzeugen. Dann gilt nur die Methode, die zuerst eingefügt wurde. Im Regelfall werden Konflikte jedoch mit einer der beiden folgenden Techniken behoben: Besitzen beide Rollen zum Abschalten die Methode aus, wäre ein deklaratives Umbenennen sinnvoll.

with 'VorlonenKanone'       => {
-alias => { aus => 'kanone_aus' },
-excludes => 'aus'
},
'Gravitationsgenerator'=> {
-alias => { aus => 'gravitation_aus'},
-excludes => 'aus'
};

Da -alias nur einen Alias erzeugt, ist der Import des alten Namens zusätzlich zu unterdrücken. Der andere Lösungsweg wäre eine Methode aus in der Klasse Excalibur, die Zugriff auf VorlonenKanone::aus($self); und Gravitationsgenerator::aus($self); hätte, selbst wenn ihr Import unterdrückt würde. $self muss überreicht werden, da die Methoden sonst keinen Zugriff auf die Attribute hätten.

Mit MooseX::Declare bekommen die Beispiele den futuristischen Perl-6-Anstrich:

use MooseX::Declare;

role VorlonenKanone {

requires 'energielevel';

has 'ladezustand' => (is => 'rw', isa => 'Num');

method feuer { ... }

}


class Excalibur extends Raumschiff extends White Star
with VorlonenKanone with Gravitationsgenerator {
...
}

Hier dürfen die Namen der Klassen und Rollen nicht in Anführungszeichen gesetzt werden, bei einem nachträglichen extends oder with im Klassenblock müssen sie es schon. Da MooseX::Declare Devel::Declare verwendet, um die zusätzlichen Schlüsselworte einzufügen, hat es bis zum nächsten Semikolon volle Gewalt über den Parser und kann auf die leicht störenden Zeichen verzichten. Das gilt nicht für die normalen, aus Moose importierten Subroutinen wie extends, with oder has. Diese werden, wie erwähnt, zu Beginn der Laufzeit ausgeführt, weshalb deren Reihenfolge wundervolle Streiche spielen kann. Steht statt des entscheidenden with nur eine Zeile oberhalb der Methode, die von der Rolle verlangt wird, ist der Compiler unerbittlich. Deshalb ist ein with gut am Boden einer Klasse aufgehoben, noch besser, weil narrensicher, ist jedoch die eben gezeigte Form mit MooseX::Declare, die ebenfalls with am Ende aufruft und zudem deklarativer ist. Die wesentlichen Informationen zu einer Klasse sollten am Anfang stehen.

Diese gehen auch nicht verloren. Jederzeit lässt sich das Raumschiff zu seinen Bauteilen befragen:

# von dieser Klasse abgeleitet ?
$excalibur->isa('Raumschiff');
# enthält diese Rolle ?
$excalibur->does('VorlonenKanone');
# enthät diese Methode ?
$excalibur->can('feuer');

Die Antwort wäre jedes Mal "1", und auch die offizielle, mit Perl 5.10 in UNIVERSAL eingeführte Methode, um nach einer enthaltenen Rolle zu fragen, wird unterstützt.

$excalibur->DOES('VorlonenKanone');

does ist das Schlüsselwort in Perl 6 für diese Überprüfung und dient der Injektion von Rollen in eine Klasse oder ein Objekt.

Da wie gezeigt jede Klasse der Typ eines Attributs sein kann, lässt sich der Compiler auch hier für Fragen nach konsumierten Rollen einspannen.

class AntiDrakFlotte {

has 'flaggschiff' => {
is => 'rw',
isa => 'Excalibur',
does => 'VorlonenKanone',
}
...

Das ist nicht unwichtig, da besagte Rolle vielleicht über eine Elternklassen oder eine andere Rolle aufgenommen wurde.

So etwas wie die hohe Schule sind parametrisierte Rollen, die Perl 6 von sich aus kennt und Moose mit dem in MooseX::Declare enthaltenen MooseX::Role::Parameterized. Eigentlich sind es nur etwas flexiblere Rollen, die auf anfänglich überreichte Parameter reagieren. Verständlicherweise sind die Parameter vor der eigentlichen Rolle zu definieren.

package Antrieb;
use MooseX::Role::Parameterized;

parameter typ => (
isa => (enum ['Quanten', 'Antimaterie', 'Plasma']),
default => 'Plasma',
);

role {
my $p = shift;
if ($p->typ eq 'Quanten') {
method energie => \&QuantenStrahl::an;
method stop => \&QuantenStrahl::aus;
}
elsif ($p->typ eq 'Antimaterie') {
method energie => \&AMTriebwerk::fusion;
method stop => \&AMTriebwerk::trennen;
}
...
}

Ansonsten enthält das Beispiel wenig Neues, da parameter sich die Syntax mit has teilt. Dass benannte Parameter in einer Hashref ankommen, die mit shift aus @_ gezogen wird, ist ebenfalls tägliches Perl-5-Geschäft. Einzig bemerkenswert ist die Schreibweise zum Umleiten der Methodenaufrufe. In der Klasse wird der Antriebstyp wie bekannt weitergereicht.

package Excalibur;
...
with Antrieb => { typ => 'Antimaterie' };
...