Inhaltsverzeichnis
Dieser Vortrag ist praxisorientiert, daher werde ich auf Theorie verzichten, und an dieser Stelle auch keine Diskussion über die Vorteile von objektorientierter Programmierung führen.
An einem kleinen Beispiel zeige ich den Aufbau einer Klasse in Perl. Danach stelle ich die Lösung eines kleinen Problems dar, von der Problemstellung bis zur objektorientierten Lösung. Anhand einer einfachen Aufgabe, die eigenständig gelöst werden soll, besteht die Möglichkeit das Gelernte zu rekapitulieren.
Der Leser sollte mit subs und Modulen vertraut sein. Weiterhin setze ich Erfahrung mit assoziativen Arrays und Referenzen voraus. Im Kapitel Grundlagen gebe ich eine kurze Einführung, weiterhin empfehle ich die folgenden Perl manpages:
zu assoziativen Arrays bzw. Hashes: perldata
zu subs: perlsub
zu Modulen: perlmod
zu Referenzen: perlreftut
An dieser Stelle möchte ich mich kurz bei meiner Frau bedanken, die Verständnis dafür hatte, dass ich die ohnehin knappe Zeit am Wochenende für diesen Vortrag abgeknapst habe. Ebenfalls möchte ich mich bei Cem Sakaryali und Michael Lamertz von Cologne.pm bedanken, die mit ihrem Feedback zur Verständlichkeit dieses Vortrags beigetragen haben. Was an Unverständlichkeit geblieben ist, liegt in meiner Verantwortung.
Assoziative Arrays, auch Hashes genannt, sind ein mächtiger Datentyp in Perl.
Bei herkömmlichen Arrays werden die einzelnen Array-Elemente über Ganzzahlen identifiziert, so liefert
$array[0]
das erste Element des Arrays @array. Die Zahl in den eckigen Klammern wird Index genannt.
Assoziativen Arrays werden nicht mit @ sondern mit % am Anfang deklariert. Bei ihnen können die Indexwerte auch Strings sein. Der Index wird nicht in eckigen, sondern in geschweiften Klammern gestellt. So bezeichnet
$assoc{'beispiel1'}den Wert des assoziativen Arrays %assoc, der unter dem Wert 'beispiel1' gespeichert ist.
Zur Initialisierung von assoziativen Arrays existiert eine Kurzform. Statt
$assoc{'beispiel1'} = 'erster wert';
$assoc{'anderes beispiel'} = 'zweiter wert';kann man auch
%assoc = ('beispiel' => 'erster wert',
'anderes beispiel' => 'zweiter wert');schreiben.
Durch Module lässt sich Programmcode, der logisch zusammengehört, zusammenfassen.
Ein einfaches Modul hat die Form
package MyModule;
my $wert = 0;
sub setzeWert {
my ($inWert) = @_;
$wert = $inWert;
}
1;Das oben genannte Modul MyModule muss in einer Datei mit dem Namen MyModule.pm gespeichert sein.
Die Verwendung des oben dargestellten Moduls würde wie folgt gehen:
use MyModule; MyModule::setzeWert(5); print $MyModule::wert;
Subs und Variablen eines Moduls erhalten beim Aufruf also den Modulnamen, gefolgt von zwei Doppelpunkten, vorangestellt.
Das Modul wird durch das use-Statement eingebunden. Dafür muss das Modul im aktuellen Verzeichnis oder einem Verzeichnis, das in @PATH enthalten ist, stehen.
Referenzen sind ein wichtiges Hilfsmittel, um dynamische Datenstrukturen zu erzeugen. Eine Referenz speichert einen Verweis auf ein Datenobjekt, z.B. eine gewöhnliche Variable oder ein Array. Um eine Referenz auf eine benannte Variable zu erzeugen schreibt man
$referenz = \$variable;
Über die Referenz kann man auf das referenzierte Objekt zugreifen. So ändert der folgende Code den Wert der Variable $a auf 5:
my $a = 4; # Variable anlegen my $refa = \$a; # Referenz auf Variable anlegen $$refa = 5; # Ueber die Referenz die Variable setzen print $a;
Hierbei fällt auf, dass der Zugriff auf die referenzierte Variable mit zwei $-Zeichen eingeleitet wird. Eine Zuweisung erfolgt also an $$refa. Bei Referenzen auf Arrays und Hashes gibt es dafür eine Kurzform, die wir im nächsten Beispiel sehen:
my %hash;
my $hashref = \%hash;
$hashref->{'otto'} = 'karl';
print $hash{'otto'};Hier kann man statt $$hashref{'otto'} auch $hashref->{'otto'} schreiben. Hier ist -> ein Sinnbild für die Tatsache dass $hashref eine Referenz ist. Für ein Array sähe der Code so aus:
my @array; my $arrayref = \@array; $arrayref->[0] = 5; print $array[0];
Bis auf die Verwendung von eckigen Klammern statt geschweiften Klammern ist die Schreibweise also gleich.
Im objektorientierten Perl werden häufig Referenzen auf anonyme Objekte erzeugt. Anonyme Objekte sind Objekte, die sich nicht über einen Namen ansprechen lassen und die so lange existieren, wie eine Referenz auf sie existiert. So erzeugt
$anonHashRef = {};ein anonymes assoziatives Array und speichert eine Referenz darauf in $anonHashRef ab. Und der folgende Code erzeugt ein anonymes Array und speichert eine Referenz darauf in $anonArrayRef ab.
$anonArrayRef = ();
Beim Zugriff auf referenzierte Objekte wird nicht zwischen Referenzen auf benannte und anonyme Objekte unterschieden. So sähe der Code für den Zugriff über die beiden oben eingerichteten Referenzen wie folgt aus:
$anonArrayRef->[0] = 7;
$anonHashRef->{'beispiel'} = 9;Die in Anhang A, Beispiel A.1 dargestellte Klasse TKonto bildet ein einfaches Bankkonto ab. Auf den ersten Blick sieht man beim Code der Klasse keinen Unterschied zu einem normalen Perl-Modul: Es handelt sich um ein Package, das eine Reihe von subs enthält und dann mit 1; und __END__ abgeschlossen wird.
Die im Anhang A, Beispiel A.2 dargestellte Datei TKonto_t.pl verwendet die Klasse TKonto.
Das Einbinden der Klasse geschieht genau wie bei einem Modul durch eine use-Anweisung. In den Zeilen 3 und 4 lege ich zwei Variablen vom Typ TKonto durch Aufruf von new TKonto an. Dabei ist die erste übergebene Zahl der Kontostand und die zweite Zahl das Dispolimit, d.h. der Betrag um den das Konto maximal überzogen werden darf.
Als ersten Schritt richte ich zwei Konten ein:
1 use TKonto; 2 3 my $boss_konto = new TKonto (1000000, 70000); 4 my $mein_konto = new TKonto (200, 500);
Danach führe ich in Zeile 13 eine Überweisung von meinem Boss zu mir durch.
13 $boss_konto->ueberweise ($mein_konto, 1000);
Um zu kontrollieren, dass die Überweisung korrekt durchgeführt wurde und dabei keine wundersame Geldvermehrung oder -vernichtung stattgefunden hat, gebe ich die einzelnen Kontostände, sowie die Gesamtsumme aus.
9 printf "Boss Konto: %9d\n", $boss_konto->getKontostand(); 10 printf "Mein Konto: %9d\n", $mein_konto->getKontostand(); 11 printf "Summe : %9d\n", $boss_konto->getKontostand()+$mein_konto->getKontostand();
An diesem Beispiel sehen wir schon den prinzipiellen Ablauf beim Einsatz einer Klasse: Zuerst wird eine neue Variable erzeugt, dann werden Funktionen in Verbindung mit dieser Variablen aufgerufen:
my $var = new Klasse(); $var->example();
In der Sprache der Objektorientierung: new ist der Konstruktor der Klasse, die Variable speichert eine Instanz der Klasse und die subs werden Methoden genannt.
Nachdem wir gesehen haben, dass die Klasse funktioniert, wollen wir jetzt wissen, wie die Klasse funktioniert.
Betrachten wir zunächst new:
4 # Konstruktor
5 sub new {
6 my ($inPkg, $inKontostand, $inDispo) = @_;
7 my $self = {
8 'kontostand' => $inKontostand,
9 'dispo' => $inDispo
10 };
11 bless $self, $inPkg;
12 return $self;
13 }Die erste Besonderheit fällt uns in Zeile 6 auf: Das Testprogramm ruft new mit zwei Parametern auf, tatsächlich werden aber drei Parameter von der sub übernommen. Der erste Parameter von new wird von Perl selber übergeben, und ist der Name der Klasse.
In Zeile 7 mit $self eine Referenz auf ein assoziatives Array angelegt in dem die Werte für Kontostand und Dispolimit gemerkt werden. Als letztes wird in Zeile 11 bless auf die Referenz aufgerufen und die Referenz zurückgeliefert. Hier kommt dann auch wieder der Name der Klasse in's Spiel, der an bless übergeben wird.
Diese Methoden werden in der Form
$var->sub()aufgerufen und erhalten einen Parameter $inSelf übergeben. Es handelt sich um die Referenz auf das in new angelegte Assoc. Darüber greifen Sie auf die im Assoc gespeicherten Werte zu.
aufgerufen werden. Der Wert von $var wird dann als erster Parameter übergeben und damit $inSelf zugewiesen.
Eine Klasse in Perl besteht aus einem Modul, sowie einer sub new, die ein Assoc zurückliefert, auf das zuvor bless angewendet worden ist.
Eine sub, die auf die im Assoc gespeicherten Werte zugreifen will, wird in der Form $var->example() aufgerufen. Die Referenz auf das Assoc erhält sie dann als ersten Parameter.
Ansonsten kann man subs definieren, die wie in normalen Modulen funktionieren, und die in der Form Modul::example() aufgerufen werden.
Nach den Grundlagen der objektorientierten Programmierung in Perl, möchte ich jetzt eine kleine Anwendung zeigen.
An dieser Stelle möchte ich an einem Praxisbeispiel aufzeigen, wie in der Objektorientierung der Weg von der Idee zum fertigen Programm verläuft.
Fangen wir mit einem einfachen Beispiel an. Beim Programmieren gibt es viel langweilige Tipparbeit, so auch beim Schreiben von Perl-Klassen. Um uns die Arbeit zu erleichten, wollen wir das Skelett einer Klasse erstellen lassen, d.h. der reine Code, ohne Innenleben, also z.B:
package Class;
sub Methodenname {
my ($inParm1, $inParm2) = @_;
}
1;Für unseren Zweck verwenden wir die folgende, vereinfachte Definition einer Klasse:
Eine Datei, deren Namen mit dem Namen der Klasse übereinstimmt. D.h. der Klassenname muss bekannt sein.
Eine Liste von Methoden. Jede Methode hat einen Namen und eine Liste von Parametern. Jeder Parameter hat wiederum einen Namen.
Die folgende Abbildung zeigt eine Zerlegung der Datei in ihre Bestandteile.
Aus dieser Definition ergibt sich die folgende Zerlegung:
Eine Klasse TParam. Die Klasse verfügt über ein Attribut Name.
Eine Klasse TMethod. Die Klasse verfügt über ein Attribut Name, sowie eine Liste von Instanzen der Klasse TParam.
Eine Klasse mit dem Namen TClass. Die Klasse verfügt über ein Attribut Name, sowie eine Liste von Instanzen der Klasse TMethod.
Das folgende Bild gibt einen Überblick über die resultierenden Klassen, und zeigt welche Klasse sich auf welche andere Klasse abstützt.
Diese Zerlegung hat mehrere Vorteile:
Sie unterteilt die Gesamtaufgabe in kleinere und überschaubare Teile, die einzeln entworfen, geschrieben und getestet werden können.
Die Aufteilung enspricht der natürlichen Gestalt des Problems, sie bildet die reellen Teile in Software-Teile ab. Die Unterteilung ist kein magischer Akt, sondern orientiert sich an der Beschreibung des Problems.
Der Header hat die Form
package Class;
Der Footer hat die Form
1;Der Rumpf einer Methode hat die Form
sub Methodenname {
my (Param 1, ...,Param n) = @_;
}Aus dieser Darstellung ergibt sich für die einzelnen Klassen die folgende Aufgabenteilung:
Die Klasse gibt den Header aus. Dann durchläuft sie ihre Methoden und fordert sie auf, sich selber auszugeben. Zum Schluß gibt sie den Footer aus.
Eine Methode gibt sub gefolgt von ihrem eigenen Namen und { aus. Dann gibt sie die Parameterliste aus, wobei sie jeden Parameter auffordert, sich selber auszugeben. Danach gibt sie den Rest der Zeile aus, sowie die schliessende Klammer am Schluss.
Dabei gehen wir von „unten nach oben“ vor.
Die Klasse bekommt den Namen des Parameters im Konstruktur übergeben. Mittels der Methode getParamName() liefert er seinen Namen mit einem vorangestellten $ zurück.
Die Klasse bekommt den Namen der Methode im Konstruktor übergeben. Sie verwaltet eine Liste von TParam-Instanzen. Über addParam() wird die Liste erweitert. Der Aufruf von getMethodBody() liefert den Rumpf der Methode zurück.
Der Name der Klasse wird im Konstruktor übergeben. Sie verwaltet eine Liste von TMethod-Instanzen. Über addMethod() wird die Liste erweitert. Der Aufruf von genClass() erzeugt den oben beschriebenen Rumpf einer Klassendatei.
Die Klassen und die zugehörigen Testskripte sind im Anhang 2 aufgeführt.
Die Klassen leisten relativ wenig, gemessen am Aufwand den wir betrieben haben um sie zu schreiben. Ohne Objektorientierung hätten wir das gleiche Ergebnis auch in weniger als der Hälfte Codezeilen erreichen können. Ganz besonders TParam erscheint ziemlich aufgebläht: Alles was sie tut, ist einem String ein $-Zeichen voranstellen.
Programme haben die Tendenz zu wachsen. Und nachdem man sie Schritt für Schritt erweitert hat, ist ihre Struktur ziemlich unübersichtlich und jede Erweiterung führt mehr oder weniger schweren Fehlern in das Programm ein.
Die von mir durchgeführte Zerlegung des Problems hat den Vorteil, dass bei einer Änderung der Ort an dem geändert werden muss in der Regel eindeutig bestimmt ist. Wenn ich bei den Parametern vermerken möchte, ob es sich um Eingabe- oder Ausgabeparameter handelt, dann bietet sich TParam dafür automatisch an. Gleichzeitig muss ich nicht befürchten, dass sich diese Änderung auf Programmteile auswirkt, bei denen das nicht erwünscht ist.
Perl Klassen sind auch nur Module. Sie haben einen Konstruktor new, der eine mit bless behandelte Referenz auf ein Assoc zurückliefert und eine Reihe von Methoden, die als ersten Parameter diese Referenz übergeben bekommen.
Instanzen werden durch
$instanz = new
Klasse(...); erzeugt, und Methoden werden in der Form
$instanz->methode(...);aufgerufen. Daneben kann eine Klasse auch subs enthalten, die wie in normalen Modulen auch, über
Klasse::example(...)aufgerufen werden.
Schreiben Sie eine Klasse TPoint, die einen Punkt abbilden soll. Die Klasse hat zwei Member xk und yk, die im Konstruktor übergeben werden. Zusätzlich verfügt die Klasse über zwei Methoden getX und getY, die den Wert von xk bzw. yk zurückliefern. Als letztes gibt es eine Methode distance, die den Abstand des Punktes zu einem zweiten Punkt zurückliefert. Der Abstand wird berechnet als: Wurzel ( (x1 + x2) ^2 + (y1 + y2) ^2)
Schreiben Sie eine Klasse TLine, die eine Linie abbilden soll. Die Klasse hat zwei Member p1 und p2, die Instanzen von TPoint sind. Die Koordinaten x1, y1 des ersten Punkts und x2, y2 des zweiten Punkts werden dem Konstruktor in der Reihenfolge x1, y1, x2, y2 übergeben. Schreiben Sie zwei Methoden getP1 und getP2, die jeweils p1 bzw. p2 zurückliefern. Schreiben Sie eine Methode length, die die Länge der Linie zurückliefert. Die Länge ist definiert als der Abstand der beiden Punkte p1 und p2.
Beispiel A.1. Die Klasse TKonto
1 package TKonto;
2 use strict;
3
4 # Konstruktor
5 sub new {
6 my ($inPkg, $inKontostand, $inDispo) = @_;
7 my $self = {
8 'kontostand' => $inKontostand,
9 'dispo' => $inDispo
10 };
11 bless $self, $inPkg;
12 return $self;
13 }
14
15 # Verändernde Methoden
16 sub addiere {
17 my ($inSelf, $inBetrag) = @_;
18 $inSelf->{'kontostand'} += $inBetrag;
19 }
20
21 # Die Überweisung wird nur durchgeführt, wenn wir dadurch unseren
22 # Dispo nicht überziehen.
23 sub ueberweise {
24 my ($inSelf, $inTo, $inBetrag) = @_;
25 if ($inSelf->getKontostand() - $inBetrag >= $inSelf->getDispo()) {
26 $inSelf->addiere (-$inBetrag);
27 $inTo->addiere ($inBetrag);
28 }
29 }
30
31 sub verzinse {
32 my ($inSelf) = @_;
33
34 if ($inSelf->getKontostand() > 0) {
35 $inSelf->addiere ($inSelf->getKontostand() * TKonto::guthabenzinsen());
36 } else {
37 $inSelf->addiere ($inSelf->getKontostand() * TKonto::kreditzinsen());
38 }
39 }
40
41 # Lesende Methoden
42 sub getKontostand {
43 my ($inSelf) = @_;
44 return $inSelf->{'kontostand'};
45 }
46
47 sub getDispo {
48 my ($inSelf) = @_;
49 return $inSelf->{'dispo'};
50 }
51
52 # Statische Methoden
53
54 # 2 Prozent Guthabenzinsen
55 sub guthabenzinsen {
56 return 0.02;
57 }
58
59 # 9 Prozent Kreditzinsen
60 sub kreditzinsen {
61 return 0.09;
62 }
63
64 1;
65
66 __END__
Beispiel A.2. Die Datei TKonto_t.pl
1 use TKonto; 2 3 my $boss_konto = new TKonto (1000000, 70000); 4 my $mein_konto = new TKonto (200, 500); 5 6 # Vor und nach der Überweisung muss die Summe gleich sein. 7 8 print "\nVor der Ueberweisung\n"; 9 printf "Boss Konto: %9d\n", $boss_konto->getKontostand(); 10 printf "Mein Konto: %9d\n", $mein_konto->getKontostand(); 11 printf "Summe : %9d\n", $boss_konto->getKontostand()+$mein_konto->getKontostand(); 12 13 $boss_konto->ueberweise ($mein_konto, 1000); 14 15 print "\nNach der Ueberweisung\n"; 16 printf "Boss Konto: %9d\n",$boss_konto->getKontostand(); 17 printf "Mein Konto: %9d\n",$mein_konto->getKontostand(); 18 printf "Summe : %9d\n",$boss_konto->getKontostand()+$mein_konto->getKontostand(); 19 20 __END__ 21
Beispiel B.1. Die Klasse TParam
1 package TParam;
2 use strict;
3
4
5 sub new {
6 my ($inPkg, $inName) = @_;
7
8 my $self = {
9 'name' => $inName
10 };
11 bless $self, $inPkg;
12 return $self;
13 }
14
15
16 sub getParamName {
17 my ($inSelf) = @_;
18
19 return '$'.$inSelf->{'name'};
20 }
21
22
23 1;
24
25 __END__
Beispiel B.2. Das Skript TParam_t.pl
1 use strict;
2 use TParam;
3
4 my $t = new TParam("Example");
5
6 print $t->getParamName();
Beispiel B.3. Die Klasse TMethod
1 package TMethod;
2 use strict;
3
4
5 sub new {
6 my ($inPkg, $inName) = @_;
7
8 my $self = {
9 'name' => $inName,
10 'params' => []
11 };
12
13 bless $self, $inPkg;
14 return $self;
15 }
16
17
18 sub addParam {
19 my ($inSelf, $inParam) = @_;
20
21 push @{$inSelf->{'param'}}, $inParam;
22 }
23
24
25 sub getMethodBody {
26 my ($inSelf) = @_;
27
28 my $result = "sub ".$inSelf->{'name'}." {\n";
29 $result .= " my (";
30
31 my $delim = '';
32 foreach (@{$inSelf->{'param'}}) {
33 $result .= $delim;
34 $result .= $_->getParamName();
35 $delim = ', ';
36 }
37
38 $result .= ") = \@_;\n\n}\n\n";
39 return $result;
40 }
41
42
43 1;
44
45 __END__
Beispiel B.4. Das Skript TMethod_t.pl
1 use strict;
2 use TParam;
3 use TMethod;
4
5 my $p1 = new TParam("MyParam1");
6 my $p2 = new TParam("MyParam2");
7 my $m = new TMethod("MyMethod");
8 $m->addParam($p1);
9 $m->addParam($p2);
10
11 print $m->getMethodBody();
12
Beispiel B.5. Die Klasse TClass
1 package TClass;
2 use strict;
3
4
5 sub new {
6 my ($inPkg, $inName) = @_;
7
8 my $self = {
9 'name' => $inName,
10 'method' => []
11 };
12
13 bless $self, $inPkg;
14 return $self;
15 }
16
17
18 sub addMethod {
19 my ($inSelf, $inMethod) = @_;
20
21 push @{$inSelf->{'method'}}, $inMethod;
22 }
23
24
25 sub genClass {
26 my ($inSelf) = @_;
27
28 my $filename = $inSelf->{'name'}.".pm";
29
30 my $result = "package ".$inSelf->{'name'}.";\n\n";
31
32 foreach (@{$inSelf->{'method'}}) {
33 $result .= $_->getMethodBody();
34 }
35
36 $result .= "1;\n\n";
37
38 open (OUTFILE, ">$filename");
39 print OUTFILE $result;
40 close (OUTFILE);
41 }
42
43
44 1;
45
46 __END__
Beispiel B.6. Das Skript TClass_t.pl
1 use strict;
2 use TParam;
3 use TMethod;
4 use TClass;
5
6 my $p1 = new TParam("MyParam1");
7 my $p2 = new TParam("MyParam2");
8 my $m = new TMethod("MyMethod");
9 $m->addParam($p1);
10 $m->addParam($p2);
11
12 my $c = new TClass("MyClass");
13 $c->addMethod($m);
14 $c->genClass();
Beispiel C.1. Die Klasse TPoint
1 package TPoint;
2 use strict;
3
4 # Konstruktor
5
6 sub new {
7 my ($inPkg, $inX, $inY) = @_;
8
9 my $self = {
10 'xk' => $inX,
11 'yk' => $inY
12 };
13
14 bless $self, $inPkg;
15 return $self;
16 }
17
18
19 # Zugriffsmethoden
20
21 sub getX {
22 my ($inSelf) = @_;
23
24 return $inSelf->{'xk'};
25 }
26
27 sub getY {
28 my ($inSelf) = @_;
29
30 return $inSelf->{'yk'};
31 }
32
33
34 sub distance {
35 my ($inSelf, $inOther) = @_;
36 return sqrt( ($inSelf->getX() - $inOther->getX())**2 + ($inSelf->getY() - $inOther->getY())**2);
37 }
38
39 1;
40
41 __END__
Beispiel C.2. Das Skript TPoint_t.pl
1 use strict; 2 use TPoint; 3 4 my $p1 = new TPoint (0, 0); 5 my $p2 = new TPoint (3, 4); 6 7 print "Distance : ".$p1->distance($p2); 8 9 __END__
Beispiel C.3. Die Klasse TLine
1 package TLine;
2 use strict;
3
4 use TPoint;
5
6
7 # Konstruktor
8
9 sub new {
10 my ($inPkg, $inX1, $inY1, $inX2, $inY2) = @_;
11
12 my $self = {
13 'p1' => new TPoint ($inX1, $inY1),
14 'p2' => new TPoint ($inX2, $inY2)
15 };
16 bless $self, $inPkg;
17 return $self;
18 }
19
20
21 sub getP1 {
22 my ($inSelf) = @_;
23
24 return $inSelf->{'p1'};
25 }
26
27 sub getP2 {
28 my ($inSelf) = @_;
29
30 return $inSelf->{'p2'};
31 }
32
33 sub length {
34 my ($inSelf) = @_;
35
36 return $inSelf->getP1()->distance($inSelf->getP2());
37 }
38
39 1;
40
41 __END__