Segensreich

Perl gilt vielen immer noch vorrangig als prozedurale Skriptsprache, obwohl es etliche Techniken der objektorientierten Softwareentwicklung unterstützt, beispielsweise Polymorphie und Vererbung. Da die meisten Module als Klassen implementiert sind, lohnt sich ein genauerer Blick auf die OO-Fähigkeiten.

In Pocket speichern vorlesen Druckansicht 5 Kommentare lesen
Lesezeit: 13 Min.
Von
  • Oliver Fischer
Inhaltsverzeichnis

Seit der vor einigen Jahren erschienen Version 5 kann man mit Perl objektorientiert programmieren [1], [2]. Die wesentlichen Aspekte dabei sind:

  • Eine Klasse ist nichts weiter als ein Package, dessen Funktionen als Methoden fungieren.
  • Ein Perl-Objekt ist eine Referenz auf eine Datenstruktur, in der der Interpreter vermerkt, zu welcher Klasse sie gehört.
  • Klassen- beziehungsweise Instanzmethoden sind Funktionen, die als erstes Argument den Klassennamen beziehungsweise eine Objektreferenz erhalten.

Listing 1 implementiert eine solche Klasse Animal samt Konstruktor, Destruktor und Methoden. Anders als C++ und Java stellt Perl keinen Default-Konstruktor bereit. Bei der Klasse Animal erledigt die Funktion new() als minimaler Konstruktor das Nötige. Der Name ist zwar beliebig, ‘new’ aber üblich. Für den Aufruf von Klassen- und Instanzmethoden bietet Perl zwei Formen:

Klasse_Objektreferenz->methode (Parameterliste)

und

methode Klasse_Objektreferenz (Parameterliste)
Mehr Infos

Listing 1

Eine Klasse ist nichts weiter als ein Package. bless ordnet der benutzten Datenstruktur den Klassennamen zu.

package Animal;

use strict;

sub new {
my $type = shift;
my $self = {};

bless $self, $type;

$self->{species} = q(Tier);
$self->{name} = q(Es);

return $self;
}

sub name {
my $self = shift;

@_ ? $self->{name} = shift
: return $self->{name};
}

sub species {
my $self = shift;

return $self->{species};
}

sub run {
my $self = shift;

print "$self->{name} läuft.\n";
}

sub sleep {
my $self = shift;

print "$self->{name} schläft.\n";
}

sub wakeup {
my $self = shift;

print "$self->{name} ist wach.\n";
}

sub DESTROY {
my $self = shift;

print "$self->{name} ist weg.\n";
}

1;

Bei der ersten Form entscheidet sich erst zur Laufzeit, welche Klasse anzusprechen ist, bei der zweiten hingegen trifft Perl diese Entscheidung bereits während der Übersetzung. Unabhängig von der Schreibweise sorgt der Interpreter dafür, dass die aufgerufene Methode als ersten Parameter den Klassennamen beziehungsweise eine Referenz auf das aktuelle Objekt übergeben bekommt. Die im Aufruf angegebenen Parameter kommen erst danach. Das entspricht dem von C++ bekannten impliziten this.

Zurück zum Animal-Konstruktor. Als erstes Argument bekommt er den Klassennamen übergeben, den er in $type speichert. Anschließend weist er $self eine leere Hash-Referenz zu, die als Datenstruktur für die Speicherung der Objektattribute dienen soll. Noch ist $self nichts Ungewöhnliches, erst der Aufruf der Funktion bless mit $self und dem Klassennamen markiert es als Objekt der Klasse Animal. Abschließend gibt new die Objektreferenz mit return zurück, sodass sie dem aufrufenden Programmteil zur Verfügung steht (Listing 2). Über diese Objektreferenz ist der Aufruf der Methoden sleep(), wakeup() und run() möglich.

Mehr Infos

Listing 2

Animal-Objekte instantiiert der Konstruktor new, Objektmethoden ruft man mit dem ->-Operator auf.

#!/usr/bin/perl

use Animal;

package main;

$animal = new Animal();
$animal->sleep() ;
$animal->wakeup();
$animal->run();

Perl behält die Anzahl aller Objekte im Blick, indem es für jedes einen internen Referenzzähler führt. Erreicht dieser zur Laufzeit den Wert Null, setzt der Garbage Collection Mechanismus ein, der den belegten Speicher zurückgibt. Eventuelle Aufräumarbeiten erledigt DESTROY, das wie jede andere Methode definiert werden kann und kurz vor der Zerstörung des Objektes aufgerufen wird. Ist diese Methode nicht definiert, finden keine impliziten Aktionen statt. Die Klasse Animal benutzt den Konstruktor, um über das Verschwinden des Tieres zu informieren.

Perl-Entwickler können eine beliebige Datenstruktur benutzen, um das Objekt darzustellen, sei es ein Array, ein Hash, ein Scalar oder ein Typeglob. Wie für die Definition einer Klasse kennt Perl keine spezielle Syntax für die Festlegung von Objektattributen. Vielmehr sind diese in den Datenstrukturen zu finden, die das Objekt repräsentieren. Im Falle eines Arrays sind dies die einzelnen Elemente und bei einem Scalar dessen Wert. Die meisten Klassen verwenden jedoch einen Hash, der die Objektattribute mit ihren Werten unter beschreibenden Namen enthält - ebenso wie Animal. Für welchen Datentyp man sich entscheidet, hängt vom Anwendungsfall ab. Die Verwendung von Hashes trägt zur besseren Lesbarkeit der Quellen bei, hingegen sind auf Arrays basierende Objekte circa ein Drittel schneller als solche auf Hash-Basis.

Perl unterstützt Einfach- und Mehrfachvererbung. Dabei reicht es jedoch nur Methoden und nicht die Attribute einer Klasse weiter. Daher muss der Entwickler sicherstellen, dass die abgeleitete Klasse über dieselben Attribute verfügt wie die Basisklasse(n). Da Perl die entsprechenden Datenstrukturen beim Zugriff automatisch erzeugt, ist dies eigentlich nicht problematisch. Zu Komplikationen kann es kommen, wenn die Basisklasse ihre Attribute selbst initialisiert, beispielsweise über eine Datenbankabfrage. Allerdings lässt sich dieses Problem unter Verzicht auf Mehrfachvererbung umgehen. Später mehr dazu.

Für die Vererbung stellt Perl das spezielle Array @ISA zur Verfügung, in das alle vererbenden Klassen einzutragen sind. Listing 3 enthält beispielsweise die von Animal abgeleitete Klasse Cat. Dazu bindet das Skript zuerst die Klasse Animal durch use ein und schreibt ihren Namen in @ISA. Damit erfährt die Laufzeitumgebung, dass Animal eine Basisklasse von Cat ist.

Mehr Infos

Listing 3

Zum Erben von anderen Klassen dient das @ISA-Array.

package Cat;

use Animal;
use strict qw(@ISA);

@ISA = qw(Animal);

sub new {
my $type = shift;
my $self = {};

bless $self, $type;

$self->{species} = q(Katze);

return $self;
}

sub miaow {
print "miiiaaauuu!!\n";
}

1;

Da eine Klasse nicht automatisch die Attribute der Basisklasse erbt, stellt der Cat-Konstruktor sicher, dass es zumindest ein initialisiertes Attribut species gibt. name bleibt aber undefiniert. Ruft nun ein Programm eine Methode eines Cat-Objektes auf, die die Klasse nicht implementiert, durchsucht der Perl-Interpreter alle in @ISA erwähnten Klassen nach der betreffenden Methode. Schlägt diese Suche fehl, werden alle Klassen in derselben Reihenfolge nach der AUTOLOAD-Subroutine durchsucht. Ist sie in einer der Klassen vorhanden, reicht Perl den Aufruf der nicht gefundenen Methode samt deren Argumente an diese weiter und setzt intern den Wert der Variablen $AUTOLOAD auf den Namen der gesuchten Subroutine oder Methode. Enthält keine der Klassen eine AUTOLOAD-Methode, bricht Perl die Ausführung mit einer Fehlermeldung ab. Den Aufruf von $cat->name() reicht der Interpreter beispielsweise an die Klasse Animal weiter, da Cat keine Methode name() enthält. Allerdings liefert $cat->name() nur einen leeren String zurück, da ja der Katzen-Konstruktor nur das Attribut species, nicht aber name initialisierte.

Mit einem kleinen Trick kann die abgeleitete Klasse die Attribute ihrer Mutter erben - bei Mehrfachvererbung ist er jedoch nicht anwendbar. Dazu ruft man den Konstruktor der Basisklasse auf und ordnet die zurückgegebene Referenz mit bless der erbenden Klasse zu. Listing 4 zeigt dieses Verfahren an einer zweiten Version der Klasse Cat, die die Attribute species und name von ihrer Basisklasse übernimmt.

Mehr Infos

Listing 4

Bei Einfach-Vererbung kann der Konstruktor der übergeordneten Klasse direkt aufgerufen werden, um Attribute zu initialisieren. AUTOLOAD erzeugt fehlende Methoden automatisch.

package Cat2;

use Animal;
use strict qw(@ISA);

@ISA = qw(Animal);


sub new {
my $self = new Animal();
my $type = shift;

bless $self, $type;

$self->{species} = q(Katze);

return $self;
}

sub miaow {
print "miiiaaauuu!!\n";
}


sub AUTOLOAD {
no strict 'refs';

my $attr = $AUTOLOAD;
$attr =~ s/^.*:://;

*{__PACKAGE__ . "::$attr"} = sub {
my $self = shift;

@_ ? $self->{$attr} = shift
: return $self->{$attr};


};

goto &$AUTOLOAD;
}

1;

Basisklassen können Methoden ihrer Eltern überschreiben. Um den Zugriff auf eine solche verdeckte Methode zu ermöglichen, stellt Perl die Pseudoklasse SUPER zur Verfügung. So kann die abgeleitete Klasse unter Verwendung von $self->SUPER::methodenname() auf die Methode der Basisklasse zugreifen. Allerdings steht dieser Mechanismus nur in der Definition der abgeleiteten Klasse zur Verfügung. Außerhalb eines Klassenkontextes müssen alle Methoden der Klassenhierarchie beim direkten Einsatz durch ein Konstrukt der Art $objref->klassenname::methodenname() qualifiziert werden, unabhängig davon, an welcher Position der Hierarchie sich die jeweilige Klasse befindet. So überschreibt beispielsweise das Skript in Listing 5 die Methode run() aus Animal mit einer Version, die das Laufen der Katze verhindert. Der Aufruf der Methode run() in Listing 6 führt lediglich zur Ausgabe von

Ada will nicht laufen!
Mehr Infos

Listing 5

Methoden der vererbendenden Klasse kann man durch Neudefinition überlagern.

package Cat3;

use Animal;
use strict qw(@ISA);

@ISA = qw(Animal);

sub new {
my $self = new Animal();
my $type = shift;

bless $self, $type;

$self->{art} = q(Katze);

return $self;
}

sub miaow {
print "miiiaaauuu!!\n";
}

sub run {
my $self = shift;

print "$self->{name} will nicht laufen!\n";
}

1;
Mehr Infos

Listing 6

Vollständige Qualifizierung des Namens ruft die Methode in der Superklasse auf, hier durch Animal::run.

#!/usr/bin/perl

use Cat3;

$cat = new Cat3;
$cat->name(q(Ada));
$cat->run();
$cat->Animal::run();

Benutzt man, wie in der nächsten Zeile, die gleichnamige Methode der Basisklasse, hilft das dem Tierchen auf die Pfoten. Der Zugriff auf die Attribute ist weniger simpel, da deren Weitergabe nur mittels des oben beschrieben Tricks zu erreichen ist. Da die abgeleitete Klasse mit derselben Datenstruktur arbeitet wie die Basisklasse, überschreibt sie die Attribute einfach, statt sie wie in rein objektorientierten Sprachen zu verdecken: Ein Hash kann jeden Schlüssel eben nur einmal enthalten. Hier ist also die Aufmerksamkeit des Programmierers gefragt.

Generell kennt Perl keinen Unterschied zwischen privaten und öffentlichen Attributen, es sei denn, man erzwingt mit my() einen lexikalischen Kontext. So könnte man jederzeit auf das Attribut species eines Animal über $obj->{species} zugreifen und es ändern. Zum einen verstößt dies gegen das Prinzip der Datenkapselung, zum anderen ist der direkte Zugriff fehleranfällig, da sich hier schnell Tippfehler einschleichen. Schließlich sind $obj->{species} und $obj->{Species} voneinander verschieden. Der Interpreter bemerkt jedoch bei der zweiten Schreibweise keinen Fehler, sondern legt ein weiteres Hash-Element mit dem Schlüssel Species als Attribut an.

Während andere OO-Sprachen den Zugriff auf Attribute mit Schlüsselwörtern regeln, vertraut Perl darauf, dass Entwickler die Privatsphäre der Objekte respektieren. Programmierer neuer Klassen sollten also für alle Attribute, die von außen benutzbar sein sollen, Zugriffsmethoden erstellen. So lässt sich nicht nur das Innenleben der Klasse nach außen besser verbergen, sondern man kann auch die Implementierung unabhängig von der Schnittstelle variieren.

Auf den ersten Blick erscheint es recht aufwändig, für jedes Attribut Methoden für Lese- und Schreibzugriffe zu entwickeln. Perl wäre aber nicht Perl, wenn es nicht hierfür eine Lösung gebe, die der menschlichen Faulheit entgegenkommt. Sie beruht auf der Manipulation interner Symboltabellen des Interpreters zur Laufzeit.

Perl stellt für die Initialisierung und Zerstörung von Packages zwei spezielle Subroutinen zur Verfügung: BEGIN und END. Stößt es bei der Kompilierung eines Package auf eine oder mehrere Subroutinen namens BEGIN, führt es deren Code schon während des Übersetzens aus.

Listing 7 benutzt diese Technik, um der Klasse Cat4 weitere Attribute age, weight, owner samt Zugriffsmethoden hinzuzufügen. Dafür wird innerhalb von BEGIN das Array @attr mit den Namen aller Attribute angelegt, für die Zugriffsmethoden nötig sind. Die Anweisung no strict refs deaktiviert die Fehlermeldungen bei der Verwendung symbolischer Referenzen. Anschließend legt eine foreach-Schleife für jedes Element eine anonyme Subroutine an und weist sie einem Typeglob zu, dessen Name sich aus dem des Package und dem des Attributs zusammensetzt. So entsteht unter anderem der Eintrag age in der Symboltabelle *Cat. Als Subroutine $cat->age() aufgerufen, bekommt man den aktuellen Wert des Attributs age, den man mit $cat->age(15) setzen kann. Dazu prüft jede von foreach erzeugte Methode, ob sie ein Argument erhalten hat (@_ ?). Ist dies der Fall, trägt sie dieses Argument als Wert des Attributs ein ($self->{$attr}=shift), sonst liefert sie den aktuellen Wert mit return $self->{$attr} zurück.

Mehr Infos

Listing 7

Zugriffsmethoden für Attribute können durch Installation in der Symboltabelle erzeugt werden.

package Cat4;

use Animal;
use strict qw(@ISA);

@ISA = qw(Animal);

sub BEGIN {
my @attr = qw(age owner weight);

no strict 'refs';

foreach my $attr (@attr) {
*{__PACKAGE__ . "::$attr"} = sub {
my $self = shift;

@_ ? $self->{$attr} = shift
: return $self->{$attr};
}
}

}

sub new {
my $self = new Animal();
my $type = shift;

bless $self, $type;

$self->{art} = q(Katze);

return $self;
}

sub miaow {
print "miiiaaauuu!!\n";
}


1;

Nach Übersetzung des Quellcodes stehen die automatisch generierten Subroutinen wie alle anderen zur Verfügung. Wer sich für diesen Ansatz interessiert und sich nicht selbst um die Erzeugung seiner Methoden kümmern möchte, sollte sich das Modul Class::MethodMaker aus dem CPAN ansehen, das ein Interface für die Generierung von Zugriffsmethoden bereitstellt.

Ein alternativer Ansatz für die Generierung von Zugriffsmethoden ist die Verwendung der Subroutine AUTOLOAD, die der Perl-Interpreter automatisch aufzurufen versucht, wenn er eine Methode nicht finden kann. Das Vorgehen ist hier ähnlich dem gerade geschilderten, wie Listing 4 zeigt. Einziger Unterschied ist der Gebrauch von $AUTOLOAD, das den vollständigen Namen der fehlenden Methode enthält - also beispielsweise Animal::Cat::age. Ein regulärer Ausdruck entfernt daraus die Klassennamen, sodass nur age übrig bleibt. Anschließend geschieht in AUTOLOAD das vorher für BEGIN Beschriebene. Nachteilig wirkt sich hier im Gegensatz zum ersten Ansatz aus, dass Tippfehler bei Methodennamen nicht als Fehler erkannt werden, sondern stillschweigend zur Erzeugung einer neuen Methode führen.

Perl stellt noch weitere Mechanismen bereit, um zur Laufzeit Informationen über ein Objekt zu erfragen. So liefert ref, auf eine Objektreferenz angewandt, den Klassennamen des Objekts.

Jede Klasse erbt von UNIVERSAL. Damit stehen immer die drei Methoden isa(), can() und VERSION() zur Verfügung, die als Klassen- oder Objektmethoden aufrufbar sind. isa() liefert Informationen über die Klassenhierarchie. So würde in den Beispielen Cat->isa(‘Animal’) ‘wahr’, Cat->isa(‘Dog’) hingegen ‘falsch’ zurückliefern. Eine ähnliche Möglichkeit bietet can(), das feststellt, ob das übergebene Argument als Methodenname für die Klasse definiert ist. Cat->can(‘bark’) würde beispielsweise ‘falsch’ zurückliefern, da Katzen nicht bellen können. VERSION() schließlich unterstützt die Verwaltung von Versionsinformationen. Dazu muss das Package die Zeile

$VERSION = versionsnummer;

enthalten. Lädt man es dann mit einer use-Anweisung wie

use KLASSE versionsnummer;

bricht Perl die Kompilierung ab, falls $VERSION in der angeforderten Klasse kleiner ist, als in use verlangt.

Da Perl die Bindung zur Laufzeit beherrscht, kann es dynamisch die Klasse eines Objekts bestimmen und die entsprechenden Objekt- beziehungsweise Klassenmethode aufrufen. Listing 8 demonstriert dies noch einmal am Beispiel der Klassen Animal und Cat4.

Auch wenn Perl keine ‘reinrassige’ OO-Sprache ist, kann man die OO-Erweiterungen sinnvoll einsetzen, um bequem objektorientiert zu entwickeln und in kurzer Zeit viele Aufgaben zuverlässig umzusetzen. Beim verantwortungsvollen Umgang mit den gebotenen Mitteln lässt sich eine hohe Wiederverwendbarkeit des geschriebenen Codes erreichen. Voraussetzung hierfür ist allerdings ein Verständnis der Perl zu Grunde liegenden Techniken und Prinzipien und ihre bewusste Anwendung. Vieles beruht dabei eher auf Konventionen denn auf Sprachmitteln.

Oliver Fischer
ist bei Level_D als Datenbankentwickler beschäftigt.

[1] Michael Schilli; Gesegnet; Objektorientierte Programmierung mit Perl 5, iX 3/1996, S. 162

[2] Michael Schilli; Erben und erben lassen; Objektorientierte Anwendungen mit Perl 5, iX 4/1996, S. 198

[3] Damian Conway; Object Oriented Perl; Manning Publications 1999; ISBN: 1-884-77779-1

Mehr Infos

iX-TRACT

  • Perl-KLassen sind Packages, deren Funktionen als Methoden fungieren.
  • Ein- und Mehrfachvererbung sind möglich, Attribute werden nicht vererbt.
  • Es gibt keine Zugriffskontolle, Programmierer müssen sich verantwortungsbewußt verhalten.

(ck)