Was sind Traits und wie werden solche in Perl implementiert

Traits sind keine Erfindung von Perl sondern nur eine gute Idee. In Perl heißt Trait jedoch Role.

Programmieren ist nicht eine Frage der Entwurfsmuster sondern eine Frage der Kreativität!

Ein Trait ist ein Begriff aus der objektorientierten Programmierung und beschreibt eine wiederverwendbare Sammlung von Methoden und Attributen, ähnlich einer Klasse. Desweiteren können diese Methoden von Instanzen beliebiger Klassen benutzt werden. Ein Trait selbst ist nicht instanziierbar.

Diese Definition sagt eigentlich alles. Und so einfach wie sie sich anhört ist sie auch umzusetzen.

Perl Packages und Namespaces

In Perl bezeichnet eine Klasse den Namen einer Package:

package Foo;

# Class Foo erbt von Package main
use base 'main';

# Klassenmethode,
# statische Methode
sub hunter{
    my $classname = shift;
}

# Objektmethode
sub foo{
    my $self = shift;
}

ist also gleichbedeutend mit Class Foo. Jede Methode, in dieser Package definiert, ist entweder eine Klassenmethode die mit dem Namen der Klasse aufgerufen wird oder eine Objektmethode welche mit einer Instanz dieser Klasse aufgerufen wird. Die Instanz einer Klasse schließlich ist eine Referenz auf einen abstrakten Datentyp (Hash, Array, Filehandle) die mit dem Namen der Klasse gesegnet ist.

# rufe Klassenmethode zum Erzeugen einer Instanz
# mit dem Namen der Klasse auf
my $foo = Foo->new();

# rufe Objektmethode über die Instanz
$foo->foo();

Aufgund der mit use base 'main'; ausgewiesenen Erbschaft, kann die Instanz $foo aber auch sämtliche in der package main definierten Methoden nutzen. Jetzt sei einmal angenommen, es gäbe gäbe außer der Klasse Foo weitere Klassen wie Bar und Baz die ebenfalls von Class main erben:

package Bar;
use base 'main';

package Baz;
use base 'main';

So erben Instanzen dieser Klasse auch den Konstruktor, der als Klassenmethode in der package main definiert ist:

package main;

# Klassenmethode
sub new{
    my $classname = shift;
    bless{}, $classname;
}

Da der Konstruktor für alle Instanzen o.g. Klasse gleich ist, wird er einmalig in der Basisklase definiert. Wobei es sich hiermit um wiederverwertbaren Code handelt. Ebenso sind sämtliche in Class main definierte Ojektmethoden wiederverwendbar, es sei denn sie sind in einer abgeleitete Klasse anders definiert.

Nun ergibt sich irgendwann der Fall, daß sowohl in class Foo als auch in class Bar eine neue Methode gebraucht wird. Aber nicht nur das, diese neue Methode bindet weitere Klassen ein, deren Code zur Verwendung zu kompilieren ist. Beispielsweise soll die neue Methode den Namen encode_stash() bekommen und die package JSON einbinden:

Eine Methode als Trait, ein Trait als Methode

# Dateiname traits/encode_stash.pm

package UNIVERSAL;

use strict;
use warnings;

sub encode_stash{
    my $self  = shift; #  Beliebige Instanz
    my $stash = shift || $self->{STASH};
    my $algo  = shift || '';

    # Serializer festlegen
    if( $algo eq 'json' ){
        require JSON;
        return JSON->new->utf8(0)->encode($stash);
    }
    else{
        # nutze einen eigenen Algorithmus
    }
}
1;

Des Weiteren sei angemerkt, daß diese Methode nicht immer benötigt wird, genausowenig der JSON Algorithmus. Es bietet sich daher an, diese Methode nicht in der package main zu definieren sondern in eine eigene Datei auszulagern. Eine solche Vorgehensweise hat außerdem den Vorteil, daß die Datei in welcher die main definiert ist, nicht verändert werden muss. Wie in obenstehendem Beispiel weiterhin zu sehen ist, in der Datei traits/encode_stash.pm gibt es keine package-Deklaration. Damit ist diese Methode von Instanzen beliebiger Klassen benutzbar weil sie in jeden beliebigen Namespace importiert werden kann.

Charakteristisch ist auch, bei der Übergabe des zweiten Arguments eine Eigenschaft der Instanz als Default zu setzen. Das ist möglich weil die Instanz an dieser Stelle bereits vorliegt. Die Datei welche die Methode encode_stash() definiert, ist ein Trait. Eine aufrufende Instanz nutzt entweder das optionale zweite Argument oder bringt es als eine eigene Eigenschaft in die Methode ein. Die Wiederverwendbarkeit von Methoden in Instanzen beliebiger Klassen ermöglicht außerdem sogenannte Unit-Tests in denen Funktionen/Methoden über eine Attrappe (Mock) aufgefufen werden.

my $mock = bless{
    STASH => { foo => 'bar' }
};

# Pattern foo=bar
my $pattern = $mock->encode_stash();

Das Erbe von UNIVERSAL

Von dieser Klasse erben alle Klassen die es in Perl gibt und auch alle Klassen die in Perl erstellt werden. Methoden in einer Package mit diesem Namen sind also von Instanzen beliebig anderer Klassen nutzbar. Genau dieses Verhalten entspricht ja einer der hauptsächlichen Anforderungen eines Trait.

AUTOLOAD

Macht das Leben leichter: Es sorgt dafür, daß Code zu Laufzeit nachgeladen werden kann und zwar ganz automatisch wie von Zauberhand bewegt. Wird also eine Methode aufgerufen die es im eigenen Namespace nicht gibt, sucht der Perlinterpreter eine Methode mit dem Namen AUTOLOAD. In dieser Methode wiederum lässt sich die Variable $AUTOLOAD befragen, welche Methode namentlich aufgerufen wurde. Über diesen Mechanismus können also Traits recht komfortabel eingebunden werden.

Die Factory-Methode

... beschreibt, wie Methoden in Form einer Factory organisiert werden können. In meinen Framework ist die Factory lediglich ein Verzeichnis was Methoden in dedizierten Dateien vorhält. Gleichermaßen sind diese Dateien Traits.

Mehrere Methoden in einem Trait

Überlegen Sie sich das gut! Das Problem ist nämlich, daß früher oder später der Zeitpunkt kommt wo Sie nicht mehr wissen, welche Methode Sie in welchem Trait versenkt haben. Bei einem Interface jedoch sieht die Sache wieder anders aus, denn da sind es vom Namen her immer dieselben Methoden. So haben Interfaces, die als Trait gehalten werden den Vorteil, daß sie nicht an eine bestimmte Klasse gebunden sind. Eine Klassenbindung kann also später zur Laufzeit erfolgen. So werden sie auch hier im Framework verwendet.

Fazit

Methoden unterscheiden sich, außer daß sie verschiedene Dinge tun, hauptsächlich dadurch daß sie bestimmte Eigenschaften der übergebenen Instanz verwenden. Allein diese Kopplung qualifiziert eine Methode zu einer bestimmten Klasse -- Ohne daß die Klasse namentlich in der Methode benannt ist!

Sobald es gelingt, diese Kopplung von außen aufzuheben, ist eine Methode trait-geeignet bzw. klassenübergreifend wiederverwendbar.


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. s​os­@rolf­rost.de.