Objekte in Perl 5 - Teil 1/2

Quelle

Diese Anleitung entstammt einem Artikel von Randal L. Schwartz für das Linux Magazine, und zwar seiner Ur-Fassung. Die Fortsetzung, Teil 2/2, wurde ebenfalls übersetzt.

Erläuterungen und Beispiele des Übersetzers sind in eckige [Klammern] oder kursiv gesetzt.

Artikel und Übersetzung stammen aus dem Jahr 2000 bzw. 2001, haben aber an Aktualität gar nichts verloren. Hier geht es sehr einfach in die objektorientierte Welt von Perl 5, der aktuellen Perl-Version. (Perl 6 ist auch 2011 noch nicht als stabile Version verfügbar.)

[Einleitung]

In den vorangehenden drei Ausgaben [für das Linux Magazine] wurde der Gebrauch von Referenzen betrachtet. Referenzen sind eine wichtige Komponente beim Erfassen und Darstellen von Datenstrukturen aus der realen Welt. Man betrachte z.B. eine Tabelle mit Angestellten, von denen jeder bestimmte Eigenschaften (Attribute) hat: Name, Position, Gehalt, etc. Die gesammte Belegschaft kann man als ein Array von Hash-Referenzen darstellen. Jedes Element dieses Arrays zeigt auf das Eigenschafts-Hash eines Mitarbeiters.

Das Array-Element für den Direktor wäre dann zum Beispiel eine Referenz auf ein Hash wie dieses: { 'Name'=>'Klaus Boss', 'Position'=>'Direktor', 'Gehalt'=>12000 } Die Belegschaft kann dann als Array solcher Hash-Referenzen dargestellt werden.

Versuchen wir nun, reale Prozesse in Form von Objekten zu begreifen und darzustellen. Objekte können uns folgendes bieten: Kapselung (um den Daten-Zugriff zu kontrollieren), abstrakte Datentypen (damit die Daten die reale Welt besser modelieren können) und Vererbung (damit wir unseren Code für ähnliche Aufgabenstellungen wiederverwenden können).

Die Perl-Distribution schließt perlobj ein, eine grundlegende Referenz für den Gebrauch von Objekten. Außerdem wird perltoot mitgeliefert. Bei letzterem handelt es sich um ein Tutorial, welches den Leser mit den Besonderheiten von Perls Objekt-System vertraut macht. Soweit so gut; aber ich [Randal Schwartz] habe festgestellt, dass diese beiden Dokumentationen etwas undurchsichtig erscheinen, wenn der Benutzer noch wenig Erfahrung im Umgang mit Objekten hat. Das scheint für die meisten derer der Fall zu sein, die aus den Bereichen System-Administration oder Web-Entwicklung / CGI kommen.

Also habe ich Kursmaterial für die Stonehenge-Perl-Lehrgänge geschaffen, welches den Programmierer auf anderen Wegen zu Objekten bringt. Dabei wird angenommen, dass man vorher noch nichts mit Objekten zu tun hatte. Das ganze funktioniert etwa so ...

Wenn wir zu den Tieren sprechen könnten ...

Wir wollen mal einen Moment lang die Tiere reden lassen:

sub Kuh::sprechen { print "Die Kuh macht muh!\n"; } sub Ziege::sprechen { print "Die Ziege macht mäh!\n"; } sub Ente::sprechen { print "Die Ente macht gack!\n"; } Kuh::sprechen; Ziege::sprechen; Ente::sprechen;

Die Ausführung dieses Skripts ergibt als Ausgabe:

Die Kuh macht muh! Die Ziege mach mäh! Die Ente macht gack!

Soweit nichts besonderes. Das sind ganz normale Subroutinen, wenngleich sie von separaten Packages und mit dem kompletten Package-Namen aufgerufen werden.

Dann bauen wir jetzt mal eine ganze Weide:

sub Kuh::sprechen { print "Die Kuh macht muh!\n"; } sub Ziege::sprechen { print "Die Ziege macht mäh!\n"; } sub Ente::sprechen { print "Die Ente macht gack!\n"; } @weide = qw(Kuh Kuh Ziege Ente Ente); # Und alle reden mit: foreach $tier (@weide) { &$tier."::sprechen"}; }

Ergebnis:

Die Kuh macht muh! Die Kuh macht muh! Die Ziege mach mäh! Die Ente macht gack! Die Ente macht gack!

Hossa! Diese Dereferenzierung einer Code-Referenz ist natürlich schon garstig. Auch kümmern wir uns hier nicht um "strict subs". Dies hier ist überhaupt für größere Programme sehr ungeeignet. War das nötig? Der Package-Name scheint untrennbar verknüpft mit dem Namen der Subroutine, die wir innerhalb des Packages aufrufen wollten.

Muss das so sein?

Ein Pfeil zum Methoden-Aufruf

Nehmen wir jetzt mal an, dass Klasse->methode die Subroutine methode im Package Klasse aufruft. Das ist nicht ganz akkurat, aber wir werden hier Schritt für Schritt vorgehen. Also benutzen wir es wie folgt:

# Kuh::sprechen, Ziege::sprechen, Ente::sprechen wie zuvor: sub Kuh::sprechen { print "Die Kuh macht muh!\n"; } sub Ziege::sprechen { print "Die Ziege macht mäh!\n"; } sub Ente::sprechen { print "Die Ente macht gack!\n"; } # [Und jetzt der Aufruf mit dem Pfeil-Operator:] Kuh->sprechen; Ziege->sprechen; Ente->sprechen;

Und erneut ist das Ergebnis:

Die Kuh macht muh! Die Ziege mach mäh! Die Ente macht gack!

Das ist immer noch nicht der Bringer. Was haben wir? Die gleiche Anzahl von Zeichen, alle konstant, keine Variablen. Aber jetzt kann man die Einzelteile voneinander trennen:

$a = "Kuh"; $a->sprechen; # Ruft auf: Kuh->sprechen

Aha! Jetzt, nachdem der Package-Name von dem Subroutine-Namen getrennt ist, können wir einen variablen Package-Namen verwenden. Und diesmal haben wir jetzt etwas gebaut, das auch bei eingeschaltetem use strict refs noch funktioniert.

Einen Bauernhof aufrufen

Setzen wir nun diesen neuen Aufruf mit dem Pfeil in das Bauernhof-Beispiel ein:

sub Kuh::sprechen { print "Die Kuh macht muh!\n"; } sub Ziege::sprechen { print "Die Ziege macht m‰h!\n"; } sub Ente::sprechen { print "Die Ente macht gack!\n"; } @weide = qw(Kuh Kuh Ziege Ente Ente); foreach $tier (@weide) { $tier->sprechen; }

So, nun haben wir alle Tiere zum Reden gebracht, ohne [unschöne] symbolische Code-Referenzen zu verwenden.

Bei genauem Hinsehen finden wir jede Menge Gemeinsamkeiten in den unterschiedlichen Teilen des Code. Alle Routinen namens sprechen haben ähnliche Strukturen: Einen print-Operator und eine Zeichenkette, die - wenn man von 2 Wörtern absieht - immer den gleichen Text enthällt. Hier scheint sich das Heraustrennen solcher Gemeinsamkeiten anzubieten. Vielleicht wollen wir später ja mal den Text "... macht ..." in "...sagt..." umwandeln. [Es wäre dann gut, dies nur an einer Stelle ändern zu müssen.]

In der Tat gibt es eine Möglichkeit, dies ohne viel Gedöhns zu tun. Doch zuvor müssen wir nocht ein bißchen mehr darüber lernen, was der Pfeil zum Methoden-Aufruf tatsächlich bewirkt.

Der Extra-Parameter beim Methoden-Aufruf mit Pfeil

Der Aufruf von

Klasse->methode(@argumente)

versucht, die Subroutine Klasse::methode so aufzurufen:

Klasse::methode("Klasse", @argumente);

(Falls es die Subroutine so nicht gibt, kommt Vererbung ins Spiel, aber dazu kommen wir erst später.)

Wir müssen also in unserer Subroutine den Klassen-Namen als ersten Parameter annehmen. Demnach kann ich die Sprechen-Subroutine für Enten so schreiben:

sub Ente::sprechen { my $klasse = shift; print "Die Ente macht gack!\n"; } # Und die anderen beiden Tiere entsprechend: sub Kuh::sprechen { my $klasse = shift; print "Die Kuh macht muh!\n"; } sub Ziege::sprechen { my $klasse = shift; print "Die Ziege macht m‰h!\n"; }

In diesem Fall bekommt $klasse den entsprechenden Wert für diese Subroutine. Aber wir haben immer noch diese gemeinsame Struktur. Können wir diese noch etwas weiter herauskürzen? Ja, wir können, und zwar indem wir eine andere Methode in der selben Klasse aufrufen.

Vereinfachung durch einen zweiten Methoden-Aufruf

Rufen wir also von sprechen aus eine Hilfs-Methode namens ton auf.

{ package Kuh; sub ton { "muh" } sub sprechen { my $klasse = shift; print "Eine $klasse macht ", $klasse->ton, "!\n"; } }

Wenn wir jetzt Kuh->sprechen aufrufen, kriegen wir eine $klasse von Kühen in sprechen.

Und wie sieht das nun für die Ziege aus?

{ package Ziege; sub ton { "mäh"; } sub sprechen { my $klasse = shift; print "Eine $klasse macht ", $klasse->ton, "!\n"; } }

Wir vererben Luftröhren

Wir werden ein Package mit gemeinsamen Subroutinen definieren, welches wir Tier nennen. In ihm soll das Sprechen in Form der Subroutine sprechen, definiert werden:

{ package Tier; sub sprechen { my $klasse = shift; print "Eine $klasse macht ", $klasse->ton, "!\n"; } }

Dann definieren wir für jede Gattung, dass sie von der Art (der Klasse) Tier erbt.
[Dadurch, dass eine Art (z.B. Kuh) das Sprechen (die Subroutine sprechen von Tier erbt, muss das Sprechen nicht für jedes einzelne Tier separat definiert werden. Wohl aber bekommt jede Gattung ihren spezifischen Ton:

{ package Kuh; @ISA = qw(Tier); sub ton { "muh" } }

Das hier eingeführte Array @ISA verdient Beachtung. [Zu lesen ist dieser Variablen-Name als englischer Text "is a", zu Deutsch "ist ein". Die Kuh soll also ein Tier sein.] Später werden wir uns @ISA noch genauer ansehen.

Was passiert nun, wenn wir Kuh->sprechen aufrufen?

Zuerst konstruiert Perl die Argumenten-Liste. In diesem Fall enthällt diese nur Kuh. Dann sucht Perl nach Kuh::sprechen. Dies ist aber nicht vorhanden. In diesem Fall wertet Perl das Vererbungs-Array @Kuh::ISA. Das gibt es, und es enthällt einen einzigen Eintrag, nämlich Tier.

Daher sucht Perl dann in der Definition von Tier nach sprechen. Das heißt, es wird überprüft, ob die Routine Tier::sprechen definiert ist. Und die gibt es ja wirklich.
[Wir haben sie im Package Tier definiert. Also ruft Perl diese Subroutine mit der bereits erstellten Argumenten-Liste auf.]

Innerhalb der Subroutine Tier::sprechen wird $klasse zu Kuh, dem Wert des ersten Arguments.

Ein paar Anmerkungen zu @ISA

[Dieser Abschnitt der Übersetzung enthält viele Abweichungen vom englischen Original, die nicht als solche gekennzeichnet sind.]

Die Zauber-Variable @ISA besagt hier lediglich, dass Kuh ein Tier ist: Das @ISA ("is a" oder "ist ein") von Kuh enthält den Wert Tier. Die Elemente der Klasse Kuh erben demnach Eigenschaften und Methoden von der Klasse Tier, über die man dann zusätzlich zu ihren eigenen, im Package Kuh selbst definierten Eigenschaften und Methoden, verfügen kann. Man beachte, dass es sich bei @ISA um ein Array handelt, nicht um einen einzelnen Wert. Gelegentlich kann es nämlich sinnvoll sein, dass eine Klasse mehr als eine Eltern-Klasse hat, in denen nach Methoden gesucht wird, die für die Klasse selbst nicht definiert sind.

Wenn Tier wiederum ein @ISA hätte, dann würde auch dort nach Methoden gesucht, die weder in Kuh noch in Tier vorkommen. Die Suche ist rekursiv und geht zunächst in die Tiefe. Ansonsten werden die Elternklassen wie in @ISA angegeben von links nach rechts durchsucht. Dies wird an folgendem Beispiel deutlich:

Wir nehmen an, es gäbe für Kuh zwei Elternklassen, Tier und Hofbewohner. Das heißt @ISA = qw(Tier Hofbewohner).

Wir nehmen weiter an, die Klasse Tier erbe von einer Elternklasse Lebewesen, habe also ein @ISA = qw(Lebewesen).

Wenn nun in Kuh nach einer Methode gesucht werden soll, dann geht das in folgender Reihenfolge:

  1. Erst wird in Kuh gesucht,
  2. dann in Tier (erstes Element in @ISA);
  3. dann in Lebewesen: Das Weitersuchen in der Tiefe hat höhere Priorität als das Durchsuchen von @ISA von links nach rechts.
  4. Erst wenn die Methode auch im Vererbungspfad von Tier nicht gefunden wurde, wird das nächste Element von @ISA, nämlich Hofbewohner, in Angriff genommen.

Wenn wir use strict benutzen, beschwert sich dieses über @ISA, weil es als Variable keinen explizit angegebenen Package-Namen hat, und auch nicht lexikalisch (my @ISA) ist. Wir können keine lexikalische Variable daraus machen, aber es gibt ein paar andere Möglichkeiten, damit umzugehen.

Das einfachste ist, den vollen Package-Namen auszuschreiben:
@Kuh::ISA = qw(Tier);

Wir können es auch als implizit benannte Package-Variable zulassen:
package Kuh; use vars qw(@ISA); @ISA = qw(Tier);

Wenn man die Klasse von außen über ein objektorientiertes Modul hereinbringt, ändert man nur:

package Kuh; use vars qw(@ISA); @ISA = qw(Animal);

in

package Kuh; use base qw(Tier);

Überlagerung von Methoden

Nun fügen wir eine Maus hinzu, die man aber kaum hören kann:

{ package Maus; @ISA = qw(Tier); sub ton { "quieck" } sub sprechen { my $klasse = shift; print "Eine Maus macht ", $klasse->ton, "!\n"; print "(Man kann sie aber kaum hören.)\n"; } Maus->sprechen; }

Aber jetzt haben wir wieder teilweise Code von Tier->sprechen dupliziert, und das ist nicht gerade wartungsfreundlich. Können wir das umgehen? Können wir irgendwie sagen, dass die Maus alles macht, was andere Tiere auch machen, und dann den Kommentar hinzufügen? Klar doch!

# Package Tier von weiter oben hier einfügen, dann: { package Maus; @ISA = qw(Tier) sub ton { "quiek" } sub sprechen { my $klasse = shift; Tier::sprechen($klasse); print "[Man kann sie aber kaum hören.]\n"; } }

Beachte, dass wir den Parameter $klasse (Wert: "Maus") als ersten Parameter für Tier::sprechen angeben müssen, weil wir den Pfeil-Operator hier nicht verwenden. Warum nicht? Tja, wenn wir Tier->sprechen aufgerufen hätten, so wäre als erster Parameter nicht "Maus", sondern "Tier" übergeben worden. Und zu dem Zeitpunkt, an dem der Ton abgerufen wird, wäre nicht bekannt, in welche Klasse der Code zur¸ckkehren muss. Die Methode w¸sste nicht, dass sie aus der Klasse Maus aufgerufen worden wäre.

Tier::sprechen direkt aufzurufen ist aber auch nichts tolles. Was ist, wenn Tier::sprechen nicht direkt existiert, sondern von einer Klasse vererbt wurde, die in @Tier::ISA gelistet ist? Aufgrund des nicht verwendeten Methoden-Pfeils haben wir nur eine einzige Chance, die Subroutine zu treffen.

Wir haben hier außerdem den Klassen-Namen "Tier" fest in den Code des Subroutinen-Aufrufs getippt. Schlimm, wenn nun jemand später an dem Code arbeitet und @ISA für die "Maus" ändert, ohne auf das "Tier" in "sprechen" zu achten. Also ist das auch nicht der richtige Weg.

Von woanders aus suchen

Eine bessere Lösung ist, Perl zu sagen, es solle die Suche an einer höheren Stelle in der Vererbungskette beginnen:

# Package Tier von weiter oben hier einfügen, dann: { package Maus; @ISA = qw(Animal); sub ton { "quieck" } sub sprechen { my $classe = shift; $klasse->Tier::sprechen; print "[Man kann sie aber kaum hören.]\n"; } }

Aha! Das funktioniert. Mit dieser Syntax starten wir die Suche nach sprechen bei Tier, und benutzen die ganze Vererbungskette dieser Klasse, wenn die Methode nicht direkt dort zu finden ist. [Der Methoden-Pfeil wird ja benutzt.] Der erste Parameter ist $klasse, sodass die gefundene Methode sprechen "Maus" als ersten Eintrag erhällt und ggf. für weitere Details den Weg zurück zu Maus::ton findet.

Boah - SUPER

Wenn wir in diesem Aufruf statt der Klasse Tier die Klasse SUPER verwenden, erhalten wir automatisch eine Suche durch alle unsere Super-Klassen:

# Package Tier von oben hier einfügen, dann: { package Maus; @ISA = qw(Animal); sub ton { "quieck" } sub sprechen { my $klasse = shift; $klasse->SUPER::sprechen; print "[Man kann sie aber kaum hören.]\n"; } }

Also: SUPER::sprechen heißt: Schau in @ISA von diesem Package nach und rufe die erste Methode sprechen auf, die in den dort gelisteten Packages - unter Einbeziehung von deren Vererbungsketten - findest.

Zusammenfassung

Die Notierung mit dem Methoden-Pfeil haben wir kennengelernt:

Klasse->methode(@argumente);

oder, äquivalent:

$a = "Klasse"; $a->methode(@argumente);

Dieses erzeugt die Argumentenliste
("Klasse", @argumente)
und versucht, folgende Methode aufzurufen:

Klasse::methode("Klasse", @argumente);

Wenn aber Klasse::methode nicht gefunden wird, dann wird @Klasse::ISA rekursiv nach einem Package durchsucht, in dem methode tatsächlich definiert ist. Diese Methode wird dann aufgerufen.

Die Benutzung dieser simplen Syntax erlaubt Klassen-Methoden, (Mehrfach-) Vererbung, Überlagerung und Erweiterung von Methoden. Allein mit dem bisher gezeigten war es uns möglich, allgemeinen Code auszulagern und solchen mit Variationen wiederzuverwenden. Dies ist im Wesentlichen, was Objekte uns bieten können. Aber Objekte bieten auch Instanz-Daten. Dies wurde bisher noch nicht behandelt.

Dafür reicht der Platz jetzt nicht, also seht Euch die Fortsetzung an. Viel Spaß.

© Hermann Faß, 2005