Just another OOP Tutorial for Perl

Objektorientiert Programmieren mit Perl ist ganz einfach und unpragmatisch

In Perl ist ein Objekt eine Referenz die mit dem Namen einer Klasse gesegnet ist. So erzeugen Sie mit my $obj = bless{}; eine Instanz derjenigen Klasse bzw. __PACKAGE__ in welcher diese Zeile notiert ist, wobei diese Instanz eine Referenz auf einen Hash darstellt. Gewöhnlich jedoch wird zum Erzeugen von Instanzen eine Klassenmethode mit dem Namen new() notiert und mit dem Namen einer Klasse aufgerufen wenn die Instanz erstellt werden soll:

    package MyClass;

    # Konstruktor
    sub new{
        my $class = shift;
            # Name der Klasse
        return bless {}, $class;
    }

    # eine beliebige Methode
    sub anymethod{
        my $self = shift;
    }

    # Klasse einbinden, Objekt erstellen
    package main;
    require MyClass;
    my $mc = MyClass->new();

Dieses Grundgerüst werden Sie immer wieder vorfinden, wenn Sie sich den Code anderer Autoren, beispielsweise auf CPAN anschauen. Der Pfeil Operator -> übergibt das links von ihm stehende Literal als erstes Argument in die Methode, beim Aufruf des Konstruktors ist das stets der Name der Klasse. Aber auch beim Aufruf der Methoden der erstellten Instanz wird der Pfeil-Operator notiert, in diesem Fall wird die Instanz als erstes Argument übergeben: $obj->methode();

Innerhalb der für die Instanz definierten Methoden wird die Instanz/Objekt als erstes Argument extrahiert mit der shift()-Funktion. Sie tun gut daran, innerhalb einer jeden Methode die Instanz mit $self oder $this zu bezeichnen und genauso ist es zweckmäßig, diese Instanz immer einzeln aus der Argumentenliste @_ herauszuziehen. Der Grund hierür ist eine saubere Trennung der Instanz vom Rest der an die Methode übergebeben Argumente.

Eigenschaften und Methoden anhand eines praktischen Beispiel

In Webanwendngen immer wieder gebraucht ist die Möglichkeit, zur Laufzeit bestimmte HTTP-Header hinzufügen zu können. Hieraus ergibt sich, wofür die inneren Eigenschaften eines Objektes dienlich sind: Zum Puffern von Daten. Da bei jedem Aufruf einer Methode stets die Instanz übergeben wird, stehen innerhalb einer jeden Methode sämtliche Eigenschaften der Instanz zur Verfügung. Die Klasse ist recht einfach, Erläuterungen siehe weiter unten:

package HTTPHeader;

use strict;
use warnings

# Konstruktor
sub new{
    my $class = shift;

  # Stelle den Default Header sicher
    my %headers = (
        'Content-Type' => 'text/html; Charset=UTF-8',
    @_);

  # Headers als Hash-Referenz gepuffert
  # in einer Eigenschaft der Instanz
    return bless {
        HEADERS => \%headers
    }, $class;
}

# Public Method
sub header{
    my $self    = shift;
    my %headers = @_;

  # Vorhandene Header werden gemischt
  # mit Hinzugefügten
    %{$self->{HEADERS}} = (%{$self->{HEADERS}}, %headers);

  # Return Value abhängig vom Kontext
    return wantarray ? %{$self->{HEADERS}} : do{
        my @meta = ();
        while( my($head, $val) = each %{$self->{HEADERS}} ){
            push @meta, sprintf("%s: %s", $head, $val);
        }
        join("\n", @meta)."\n\n";
    };
}

1; # ein wahrer Wert in der letzten Zeile

Das Besondere am Konstruktor ist durch die Übergabe eines Hash in @_ gegeben: Die Möglichkeit der Vorbelegung eines Default-Headers Content-Type mit text/html; Charset=UTF-8, dieser Header wird nämlich immer benötigt. Beim Aufruf des Konstruktors können weitere Header übergeben werden und gleichermaßen beim Aufruf der header()-Methode. Beim Mischen werden bereits vorhandene Header namentlich überschrieben und neue Header hinzugefügt.

Die Public-Method header() hat einen Rückgabewert abhängig vom Kontext in dem sie aufgerufen wird: Im Listenkontext wird das Array bzw. der Hash mit den bisher gespeicherten Headers zurückgegeben, im scalaren Kontext hingegen ist es ein String der direkt an den Webserver übergeben werden kann. Beispiel:

package main;

my $h = HTTPHeader->new('Set-Cookie' => 'Foo=Bar123', 'x-login' => 'fuzzi');
print scalar $h->header('x-login' => 'admin');

# Ausgabe im scalaren Kontext
x-login: admin
Content-Type: text/html; Charset=UTF-8
Set-Cookie: Foo=Bar123
===Leerzeile===

Vererbung -- Inheritance

Zum Demonstrieren der Vererbung deklarieren wir für unsere Package main die frischgebackene Basisklasse HTTPHeader und erstellen eine Instanz unserer eigenen Package. Der Aufruf der geerbten Methode funktioniert wie erwartet und gibt in untenstehendem Beispiel den Default-Header aus:

package main;

use base qw(HTTPHeader);
use strict;
use warnings;

# Instanz der Package main
my $m = main->new;

# Aufruf der geerbten Methode
print scalar $m->header;
  # Content-Type: text/html; Charset=UTF-8
  # und die Leerzeile

Hinweis: Neben Methoden der Basisklasse wird auch das Erbe der Eigenschaften angetreten. Sie sollten eine Klasse sehr genau kennen, wenn Sie vorhaben von dieser zu erben, damit es zu keinen Konflikten mit den eigenen Eigenschaften kommt. Von daher ist es oftmals besser anstelle der Vererbung nur einzelne Methoden einer anderen Klasse zu delegieren und das geht so:

Aggregation, Delegation, Dependency Injection

package main;

use strict;
use warnings;

# Instanz der Package main
# Eigenschaft HEADERS bekommt eine
# Instanz der anderen Klasse
my $m = bless{
    HEADERS => HTTPHeader->new
}, 'main';

# Delegation der benötigten Methode
# nunmehr als eine eigene Methode
sub header{
    my $self = shift;
    return $self->{HEADERS}->header(@_);
}

print scalar $m->header('Set-Cookie' => 'Foo=123');

Lassen Sie sich nicht verblüffen, wenn sie für derart einfache Vorgehensweisen gelegentlich andere Fachbegriffe finden.

Fehlersuche in Modulen

Ihre Begeisterung für OOP mit Perl wird wachsen mit der Anzahl der eigens entwickelten Klassen. Wenden wir uns daher einem weiteren wichtigen Kapitel zu, der Fehlersuche und Diagnostik. Wichtigstes Instrument ist der Data::Dumper, er wird wie folgt eingebunden und benutzt anhand des letzten Beispiel:

use Data::Dumper;

print Dumper $m;
$VAR1 = bless( {
     'HEADERS' => bless( {
       'HEADERS' => {
          'Content-Type' => 'text/html; Charset=UTF-8'
       }
     }, 'HTTPHeader' )
}, 'main' );

Ein solcher Dump zeigt den inneren Aufbau eines Perl-Objekts oder allgemein einer beliebigen Datenstruktur sehr anschaulich. Gut zu sehen ist, dass im Objekt $m eine weitere Instanz eingebaut ist und zu welcher Klasse die gehört.

Error Tracking: use Carp;

Bevor wir auf das Carp-Modul eingehen eine kurze Anmerkung zu Perl's Exception-Modell: Dem in anderen Programmiersprachen üblichen try{}-Block entspricht in Perl ein eval{}-Block. Ein Solcher lässt sich auch in einer print()-Anweisung anbringen, fällt irgendwo in diesem Block eine Exception, kommt nicht die letzte Zeile zur Ausgabe sondern die notierte Alternative wobei der Fehlertext in $@ zu finden ist:

print eval{
    die "Fehler beim Berechnen der Zeit!";
    time;
} || "Fehler: $@";

# Fehler: Fehler beim Berechnen der Zeit! at ... line 59.

Betrachte untenstehenden Code zum Demonstrieren der Wirkunsweise von $Carp::Verbose:

use Carp;
$Carp::Verbose = 1;
use strict;
use warnings;

sub new{
    my $class  = shift;
    my $number = shift || 0;
    return eval{
        croak "Zahl nicht numerisch!" unless $number =~ /^\d+$/;
        bless{}, $class;
    };
}

my $m = main->new('asdf') or die $@;

Sie sehen sämtliche an der Fehlermeldung beteiligten Zeilen in der Fehlerausgabe, also sowohl die Zeilen in denen sich der Fehler auswirkt als auch die eigentliche (aufrufende) Zeile welche den Fehler verursacht.

Private, Protected, Public ...

... werden Sie eventl. vermissen. Es gibt jedoch eine ungeschriebene Vereinbarung, dass in Perl-Klassen eingesetzte Private Methoden mit einem Unterstrich beginnend notiert werden. Das wird einen Aufruf von außerhalb zwar nicht verhinderen, aber derjenige, der davor nicht zurückschreckt, sollte sich nicht wundern, wenn nach dem Einspielen einer neueren Version des Moduls seine Anwendung nicht mehr erwartungsgemäß funktioniert, weil der Autor der Klasse den inneren Aufbau geändert hat. Aus demselben Grund verbietet sich ein direkter Zugriff auf die Eigenschaften einer Instanz, verwenden Sie daher immer eine Methode, auch dann, wenn das Modul ein Eigenentwicklung ist.

Sie können jedoch eine Methode AUTOLOAD im Namespace der jeweiligen Klasse/Package definieren, die beim Aufruf einer Eigenschaft als Methode, den Inhalt dieser Eigenschaft zurückgibt:

my $m = bless{ Number => 123 };
# betrachte die Eigenschaft als
# gleichnamige Methode
print $m->Number();

sub AUTOLOAD{
    my $self = shift;
    my $prop = our $AUTOLOAD =~ /(\w+)$/ ? $1 : '';

    # Eigenschaft nur als Literal zulässig
    return $self->{$prop} && ! ref $self->{$prop} ? $self->{$prop} : '';
}

Somit ergibt sich ein einfacher Getter für Objekteigenschaften, sofern der Wunsch besteht, diese außerhalb der Klasse verwenden zu wollen. Ganz anders verhält es sich mit sogenannten Setter's, ob diese immer sinnvoll sind, hängt davon ab inwieweit interne Eigenschaften einer Instanz voneinander abhängig sind. Das im nächsten Abschnitt vorgestellte Klassen-Beispiel macht deutlich, dass die Überlagerung des ++Operators(Increment) sinnvoller ist, als dem Anwender den direkten Zugriff per Setter auf eine Numerische IP-Adresse zu ermöglichen -- denn wer kennt schon diese Zahl für eine bestimmte IP-Adresse in gewohnter Oktettenschreibweise ;)

Operatoren überladen -- Overload

Untenstehender Code zeigt die Funktionsweise, überladen werden der ++Operator und die Stringbegrenzer womit eine String-Representation des Objekts möglich wird. Im Konstruktor wird bereits ein eval{}-Block eingesetzt, was den späteren Ausbau der Fehlerbehandlung ermöglicht. Beispielsweise eine Prüfung ob der richigen Schreibweise der übergebenen IP-Adresse, im Fehlerfall zeigt der mit croak() erzeugte Backtrace auf die Zeile in welcher der Konstruktor mit einer ungültigen IP-Adresse aufgerufen wurde.

package IP;

use strict;
use warnings;
use Carp;

sub bool{shift};
use overload
    'bool' => \&bool,
    '++' => sub{
        my $self = shift;
        $self->{IPNUM}++;
    },
    '""' => sub{
        my $self = shift;
        return join(".", unpack("CCCC", pack("N", $self->{IPNUM})));
    };


sub new{
    my $class = shift;
    my $ineta = shift; # 0.0.0.0 Schreibweise
    return eval{
        croak "Übergebene IP-Adresse ungültig!"
            unless $ineta =~ /^\d+\.\d+\.\d+\.\d+$/;
        my $self = bless{
            IPNUM => unpack("N", pack("CCCC", split( /\./, $ineta)))
        }, $class;
        $self;
    };
}

my $ip = IP->new('127.0.0.1') or die $@;
$ip++;
print $ip; # 127.0.0.2

Klassen-Hierarchien und deren Abbild im Dateisystem

Oft sehen Sie Klassenbezeichner der Art Admin::User, also mit Doppelpunkt getrennte Namen. Zum Verständnis dieser Schreibweise müssen wir uns kurzerhand mit dem @INC-Array anfreunden, hierin sind alle Pfade zu Perl-Libraries aufgeführt, auf die der Interpreter zugreift, Beispiel für Perl auf einem Win32-Rechner:

C:/Perl/site/lib
C:/Perl/lib
.

Wenn Sie also eine Perl-Klasse mit use Admin; einbinden, erwartet der Perl-Interpreter, dass sich eine Datei mit dem Namen Admin.pm in einem der o.g. Verzeichnissen befindet. Darauf ausbauend weisen zwei Doppelpunkte den Perl-Interpreter an, das Verzeichnis zu wechseln:

# Anweisung
use Admin::User;

# Pfade
C:/Perl/site/lib/Admin.pm
C:/Perl/site/lib/Admin/User.pm

Hinweis: use erwartet die Bezeichner stets als Text-Literal, Variable Modulnamen sind hierbei nicht zuläsig, Der Grund dafür ist, dass sämtliche use-Anweisungen im BEGIN{}-Block ausgeführt werden zu einem Zeitpunkt in welchem Variablen noch gar nicht aktiviert sind. Die Dateierweiterung .pm wird bei dieser Art der Einbindung grundsätzlich immer vorausgesetzt. Falls Sie davon abweichen möchten, verwenden Sie statt use require "LfdNr.pl"; wie das Beispiel zeigt, sind da auch andere Dateierweiterungen ebenso möglich wie variable Modulnamen (require "$classfile.pm";). Um auf den doppelten Doppelpunkt zurückzukommen, dieser suggeriert dem Anwender, dass die Admin::User-Class von der Basisklasse Admin erbt. Es gibt jedoch Abweichungen von dieser praktischen Regelung, weil die nicht zwingend vorgeschrieben ist.

Schließlich können Sie mit use lib qw(/dir /verz); auch Ihre eigenen Pfade dem @INC-Array hinzufügen. Es empfiehlt sich immer, nämlich aus Gründen der Übersichtlichkeit, dass aus dem Dateinamen der Name der Package hervorgeht und umgekehrt. Außerdem ist es ein schöner Brauch, in Klassennamen Großbuchstaben oder CamelCode zu verwenden, also nicht alles kleinzuscheiben. Grundsätzlich sind all diese Angaben Case-sensitive.

Der Destruktor -- Cleanup

    sub DESTROY{
        my $self = shift;
    }

Eine solche Methode können Sie wie obenstehend definieren, wenn unmittelbar vor dem Entfernen der Instanz aus dem Hauptspeicher noch bestimmte Dinge zu erledigen sind, beispielsweise Daten zurück in Dateien zu schreiben oder Verbindungen zu trennen.

Zuallerletzt -- Die letzte CODE-Zeile einer Moduldatei

1;

Eins-Komma hat sich eingebürgert, weil es i.d.R. nur darum geht, feststellen zu können, ob diese Zeile beim Kompilieren erreicht wurde. So ist dieser wahre Wert der Return-Value einer mit use, require oder do eingebundenen Datei. Wenn Sie jedoch einen von 1; abweichenden Scalar, z.B. eine Referenz zurückgeben möchten, beachten Sie, dass dies unter der Verwendung von use bedeutungslos ist. Und wenn Sie mit einem my $rv = require myClass; den Rückgabewert auffangen, bedenken Sie, das ab dem zweiten $rv = require myClass; in $rv stets eine 1 zu finden ist, unabhängig davon, was Sie in der letzten Zeile Ihrer Library notiert haben. Der Perl-Interpreter merkt sich nämlich, welche Datei bereits kompiliert wurde und tut das deswegen nur einmal. Beliebig oft jedoch kompiliert Perl eine Datei, die mit do LfdNr; eingebunden wurde, da wird bei jedem do genau das zurückgegeben was in der letzten Zeile steht, z.B. eine fortlaufende Nummer.

__DATA__

Unterhalb dieses Token kann auch noch was stehen. Zum Beispiel eine Bedienungsanleitung oder ein HTML-Template. Über den Handler <DATA> können Sie auf diese Texte zugreifen. Beachten Sie obenstehende Anmerkungen zur Art und Weise der Einbindung mit use, require oder do was die Verwendung der unter __DATA__ notierten Texte betrifft..

Nächster Artikel: Perl: Dependency Injection als Design Pattern


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.