Objekte in Perl 5 - Teil 2/2
Details zur Quelle und Grundlagen siehe Teil 1.
[Einleitung]
In der letzten Kolumne ging es um Perl-Objekte. Die Syntax mit dem Methoden-Pfeil wurde eingeführt:
Klasse->methode (@argumente);
Oder entsprechend:
$a = "Klasse";
$a->methode(@argumente);
Dabei wird die Argumenten-Liste
("Klasse", @argumente)
erzeugt und der folgende Aufruf versucht:
Klasse::methode("Klasse", @argumente);
Wie auch immer, wenn Klasse::methode nicht gefunden wird, dann wird @Klasse::ISA (rekursiv) durchsucht, um ein Package zu finden, welches die Methode methode enthällt. Diese Subroutine wird dann aufgerufen.
Durch das Bereitstellen dieser simplen Syntax liefert uns Methoden, (Mehrfach-) Vererbung, Überlagerung und Erweiterung. Mit dem bisher vorgestellten können wir gemeinsamen Kode auslagern und mit Variationen wiederverwenden. Dies ist der Kern dessen, was Objekte uns bringen, aber Objekte erlauben auch Instanz-Daten, die bisher noch gar nicht behandelt wurden. Bisher!
Eine Ente ist eine Ente, oder? Beginnen wir mit dem Kode für die Klassen "Tier" und "Ente"aus der letzten Folge:
{
package Tier;
sub sprechen {
my $klasse = shift;
print "Eine $klasse macht ", $klasse->ton, "!\n";
}
}
{
package Ente;
@ISA = qw(Tier);
sub ton { "gack" }
}
Dies erlaubt uns nun, Ente->sprechen aufzurufen, um uns zu Tier::sprechena durchzuschlängeln, von dort zurück nach Ente->ton zu springen und dort den gänse-spezifischen Ton ("gack") zu finden und dann auszugeben:
Eine Ente macht gack!
Aber all unsere Ente-Objekte wären jetzt absolut identisch. Wenn eine Subroutine hinzugefügt wird, so ist sie allen Gänsen gemeinsam. Das ist gut, wenn man lauter gleiche Gänse haben will, aber wie können wir die Unterschiede zwischen den einzelnen Gänsen festhalten? Zum Beispiel stelle man sich vor, ich will meiner ersten Ente einen Namen geben. Es muß einen Weg geben, diesen Namen von den anderen von den anderen Gänsen fernzuhalten. [Nur diese eine Ente soll diesen bestimmten Namen erhalten.]
Wir können dies tun, indem wir eine neue Unterscheidung treffen, die wir "Instanz" nennen. Eine Instanz wird von einer Klasse erzeugt. In Perl kann jede Referenz eine Instanz sein, also beginnen wir mit der einfachsten Referenz, die einen Gänse-Namen beinhalten kann: Eine Skalar-Referenz.
my $name = "Anna";
my $sprechend = \$name;
Jetzt ist $sprechend also eine Referenz auf das, was instanz-spezifische Daten sein aollen, nämlich auf den Namen. Um das nun in eine richtige Instanz zu verwandeln, brauchen wir einen speziellen Operator, genannt bless.
Aufrufen einer Instanz-Methode
Der Methoden-Pfeil kann sowohl auf Instanzen als auch auf Package-Namen (Klassen) angewendet werden. Somit erhalten wir den Ton, den $sprechend von sich gibt, wie folgt:
my $geraeusch = $sprechend->ton;
Um ton aufzurufen, bemerkt Perl zunächst, dass $sprechend eine geblesste Referenz und damit eine Instanz ist.
[Anm.: Auf die Übersetzung von bless wird bewußt verzichtet, da es zur Syntax gehört. Stattdessen sei das Verb "blessen" hiermit erfunden.]
Dann wird die Argumentenliste konstruiert. Die besteht hier nur aus $sprechend. (Später werden wir sehen, dass Argumente ihren Platz nach der Instanz-Variable einnehmen; genau wie bei den Klassen.)
Jetzt wird's lustig: Perl nimmt die Klasse, in die die Instanz geblesst wurde - in diesem Fall Ente - und sucht darin nach der Subroutine für den Methoden-Aufruf. In diesem Fall wird Ente::ton direkt gefunden (ohne in der Erbfolge zu suchen). Es ergibt sich schließlich der Subroutinen-Aufruf:
Ente::ton($sprechend);
Wir erhalten "gack" als Rückgabewert und dies wird letztendlich unsere Variable $geraeusch.
Wäre Ente::ton nicht gefunden worden, so wären wir das Array @Ente::ISA heraufmarschiert, um die Methode in einer der Super-Klassen zu finden; genau wie bei einer Klassen-Methode.
Zugriff auf Instanz-Daten
Da wir die Instanz als ersten Parameter erhalten, können wir jetzt auf die instanzspezifischen Daten zugreifen. In diesem Falle erzeugen wir einen Weg, um an den Namen zu kommen:
{
package Ente;
@ISA = qw(Tier);
sub ton { "gack"; }
sub name {
my $self = shift;
$$self;
}
}
Wir rufen jetzt den Namen auf:
print $sprechend->name, " sagt ", $sprechend->ton, "!\n";
Innerhalb von Ente::name enthällt das Array
@_ nur $sprechend, welches mittels
shift in $self gespeichert wird.
(Es ist eine Tradition, den ersten Parameter mittels
shift abzuschneiden und in einer Variable namens
$self zu speichern. [Daher wurde self
hier auch nicht übersetzt, es soll aber soviel heißen wie:
"Ich selbst", weil es der Name der aufrufenden Instanz
höchstselbst ist.] Also lassen wir es bei dem
my $self = shift
, wenn es keine sehr guten Gründe
dageben gibt.)
Dann wird $self als Skalar-Referenz de-referenziert,
was hier "Anna" ergibt. Das Ergebnis ist:
Anna macht gack!
Wie man eine Ente baut
Klar, hätten wir all unsere Enten von Hand gebaut, hätten wir sicher ab und an mal einen Fehler gemacht. Außerdem verletzen wir eine Eigenschaft der objektorientierten Programmierung: Die "Innereien" der Ente sind sichtbar. Das ist gut, wenn man Tierarzt oder Metzger ist, aber nicht, wenn man einfach nur Enten liebhat. So, nun wollen wir es der Entenklasse überlassen, eine neue Ente zu bauen:
{
package Ente;
@ISA = qw(Tier);
sub ton { "gack"; }
sub name {
my $self = shift;
$$self;
}
sub genannt {
my $klasse = shift;
my $name = shift;
bless \$name, $klasse;
}
}
Mit der neuen Methode können wir nun eine Ente bauen:
my $sprechend = Ente->genannt("Lisa");
Vererben des Konstruktors
Gab es in dieser Methode jetzt irgendetwas entenspezifisches? Nein! Mit der gleichen Methode könnte man alles bauen, was von Tieren abstammt (erbt), also packen wir es in die Klasse Tier:
{
package Tier;
sub sprechen {
my $klasse = shift;
print "Eine $klasse macht ", $klasse->ton, "!\n";
}
sub name {
my $ich = shift;
$$ich;
}
sub genannt {
my $klasse = shift;
my $name = shift;
bless \$name, $klasse;
}
}
{
package Ente;
@ISA = qw(Tier);
sub ton { "gack" }
}
Aha. Aber was passiert wenn wir sprechen aus einer Instanz aufrufen.
my $sprechend = Ente->genannt("Luzzi");
$sprechend->sprechen;
Wir erhalten eine Debugging-Meldung, z.B.
Eine Ente=SCALAR(0xaca42ac) macht gack!
Warum? Weil die Routine Tier::sprechen einen Klassennamen als
ersten Parameter erwartet, nicht eine Instanz (die da Luzzi heißt).
Methoden, die mit Klassen oder Instanzen funktionieren
Wir müssen hierzu nur feststellen, ob eine Methode jetzt als
Klassen-Methode oder als Instanz-Methode aufgerufen wird. Am
einfachsten geht dies mit dem Operator ref
. Der gibt,
wenn er auf eine gebless
te Referenz angewendet wird, einen String
zurück: Den Klassennamen. Wird er auf einen String (wie in dem
Fall den Klassennamen) angewendet, gibt er undef
zurück.
Modifizieren wir jetzt erst mal die Methode so, dass der Unterschied
sichtbar wird:
sub name {
my $was_jetzt = shift;
ref $was_jetzt ? $$was_jetzt : "namenlose $was_jetzt";
}
Die Konstruktion .. ? .. : ..
kommt
hier ganz gelegen, um entweder die De-Referenzierung
durchzuführen, oder aber einen String zurückzugeben[, der
hier unter Einbeziehung des Klassennamens generiert wird].
[Wenn es sich bei $was_jetzt (dem übergebenen
Parameter) um eine gebless
te Referenz
handelt, gibt ref
den Klassennamen
zurück, in den die Referenz gebless
t
wurde. Zur Erinnerung: Die Syntax hierfür im Konstruktor ist
bless ($referenz, $klasse)
, und genau den
zweiten Parameter gibt ref
zurück.
my $sprechend = Ente->genannt("Luzzi");
print Ente->name, "\n"; # Ausgabe: namenlose Ente
print $sprechend->name, "\n"; # Ausgabe: Luzzy
Und nun kriegen wir sprechen auch dazu, dies zu benutzen:
sub sprechen {
my $was_jetzt = shift;
print $was_jetzt->name, " macht ", $was_jetzt->ton, "!\n";
}
Da ton schon seit vorhin sowohl mit einer Klasse als auch mit einer Instanz arbeiten kann, haben wir es nun!
Einer Methode Parameter hinzufügen
[Anm. d. Übers.: In diesem Abschnitt wurden Ergänzungen / Wiederholungen sowie Perl-Kommentare (Code) eingefügt.]
Bringen wir den Tieren nun das Fressen bei:
{
package Tier;
sub genannt {
my $klasse = shift;
my $name = shift;
# Hier wird die Instanz (Ref. auf $name) der Klasse zugeordnet:
bless \$name, $class;
}
sub name {
my $irgendwas = shift;
# $irgendwas kann jetzt eine Klasse oder ein Instanzobjekt sein.
ref $irgendwas
? $$irgendwas
: " eine namenlose $irgendwas";
# Voriges Konstrukt wertet aus:
# Wenn $irgendwas eine Referenz ist, wird sie de-referenziert
# sodass diese Routine die Instanz zurückgibt.
# Ist es keine Referenz, erzeugen wir eine Bezeichnung
# und geben diese zurück.
}
sub frisst {
my $irgendwas = shift;
my $futter = shift;
print $irgendwas->name, " frisst $futter.\n";
}
}
{
package Kuh;
@ISA = qw(Tier);
sub ton { "muh" }
}
{
package Ente;
@ISA = qw(Tier);
sub ton { "gack" }
}
Und nun probiere wir das aus:
my $sprechend = Kuh->genannt("Adelheid");
$sprechend->frisst("Heu");
Ente->frisst("Gras");
Das sollte zur Ausgabe führen:
Adelheid frisst Heu. Eine namenlose Ente frisst Gras.
Eine Instanz-Methode mit Parametern wird wie folgt aufgerufen:
Als erster Parameter wird die Instanz selber übergeben; dann
folgt die Parameterliste des Methodenaufrufs. Damit wird der erste
Aufruf im obigen Beispiel, nämlich:
$sprechend->frisst("Heu")
in folgenden Aufruf umgesetzt:
Tier::frisst($sprechend, "Heu");
Interessantere Instanzen
Was, wenn eine Instanz mehr Daten braucht? Die meisten interessanten Instanzen sind aus vielen Einzelheiten zusammengesetzt, von denen jede wiederum eine Referenz oder sogar ein anderes Objekt sein kann. Der einfachste Weg, solche Daten zu speichern, ist oft ein assoziatives Feld [Hash]. Die Schlüssel [keys] dienen als Bezeichner für die Bestandteile des Objektes und werden oft auch "Instanz-Variablen" [...] genannt. Die entsprechenden Werte [values] sind - wie könnte es anders sein - die Werte dieser Bestandteile.
Aber wie macht man aus einer Kuh ein Hash? Erinnern wir uns daran, dass
ein Objekt immer irgendeine Referenz ist [Referenz auf was auch immer],
die mit bless
einer Klasse zugeordnet wurde.
Genauso einfach wie wir oben mit Skalar-Referenzen arbeiten, können
wir eine Hash-Referenz benutzen bzw. bless
en.
Natürlich muß alles, was auf
die Referenz zugreift, entsprechend angepaßt werden.
Wir machen jetzt eine Kuh, die einen Namen und eine Farbe hat:
my $freund = bless { Name => 'Peter', Farbe => "rotbraun" }, Kuh;
Demnach wird dann $freund->{Name}
den Wert "Peter"
enthalten, und entsprechend ist $freund->{Farbe}
"rotbraun". Aber wir wollen ja lieber, dass man mit
$freund->name
auf den Namen zugreift. Das geht aber nicht
mehr, weil diese Funktion ja eine Skalar-Referenz erwartet. Immer locker
bleiben; das ist relativ einfach anzupassen:
# Im Package Tier:
sub name {
my $was = shift;
ref $was ? $was->{Name} : "eine namenlose $was";
}
Natürlich baut unsere Methode genannt immer noch skalare Kühe, daher passen wir dies auch noch an:
## Im Package Tier:
sub genannt {
my $klasse = shift;
my $name = shift;
my $self = { Name => $name, Farbe => $klasse->standard_farbe };
bless $self, $klasse;
}
Was hat es jetzt mit dieser standard_farbe auf sich? Nun, wenn genannt nur den Namen kriegt, müssen wir trotzdem eine Farbe festlegen. Also legen wir eine klassenspezifische Anfangsfarbe fest. Für eine Ente könnten wir sinnvollerweise weiß festlegen:
# Im Package Ente:
sub standard_farbe { "weiss" }
# Im Package Ziege:
sub standard_farbe { "grau"; }
Die Methoden genannt und name sind die einzigen Methoden, die auf die Struktur des Objektes zugreifen. Die anderen Methoden können bleiben, wie sie sind. Das Sprechen funktioniert also auch noch mit Farbe.
Eine nicht-rotbraune Kuh
Lauter rotbraune Kühe werden auf die Dauer langweilig. Also schreiben wir doch eine Methode, mit der man die Farbe nicht nur auslesen, sondern auch setzen kann.
# Im Package Tier:
sub farbe {
$_[0]->{Farbe};
}
sub farbe_setzen {
$_[0]->{Farbe} = $_[1];
}
Hier wurde im Interesse der Geschwindigkeit direkt auf die Elemente des
Arrays @_ zugegriffen, anstatt die Parameter
in lokale Variablen einzulesen (shift
).
Jetzt kann die Farbe von Kühen verändert werden:
my $sprechend = Kuh->genannt("Lisa");
$sprechend->farbe_setzen("schwarzbunt");
print $sprechend->name, " hat die Farbe ", $sprechend->farbe, ".\n";
Die Ausgabe: Lisa hat die Farbe schwarzbunt.
Buntere Konstruktoren
[Dieser Abschnitt wurde vom Übersetzer hinzugefügt.]
Natürlich sind Konstruktoren, denen mehr als nur der Name
übergeben wird, möglich. Zum Beispiel Name, Farbe und
Nutzen. In solchen Fällen kann der Name des Konstruktors zwar
immer noch genannt sein (insbesondere weil dies der
erste Parameter ist und andere z.B. nur optional verlangt werden
können), aber vielleicht gefällt dem ein oder anderen
die Bezeichnung new
besser.
Zusammenfassung
So! Jetzt haben wir Klassen-Methoden, Kontruktoren, Instanz-Methoden, Instanz-Daten, und Zugreifs-Methoden. Dies ist aber nur der Anfang dessen, was Perl [in puncto OOP] bieten kann. Noch nicht gesprochen haben wir über Zugriffs-Methoden, die Werte sowohl auslesen als auch setzen. Ebenfalls unberücksichtigt blieben Destruktoren, Subklassen, Klassen-Daten, Überlagerung, "isa"- und "can"-Tests, UNIVERSAL-Klasen und so weiter.
Weitere Informationen kann man der Perl-Dokumentation (perlobj, perltoot, perlbot) sowie Büchern wie z.B. Damian Conways exzellentem "Objekt Oriented Perl" entnehmen.