Oop In Perl
 
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern

Objektorientierung mit Perl    

Inhaltsverzeichnis dieser Seite
Objektorientierung mit Perl   
Vorweg   
Grundlage: Perl 5 Objekte   
Vererbung?   
Eine Anmerkung zu @ISA   
Attribute von Objekten / Objektdaten   
Eine Klasse   
Datenkapselung   
Eine Tochterklasse   
Der Konzeptfehler   
Interne Methoden   
Also keine "internen Methoden"   
Interne Member-Variablen   
Die Lösung   
Das Kleingedruckte   

Vorweg    

Perl 5 bietet die Möglichkeit, ObjektOrientierteProgrammierung zu betreiben. Da dieses Konzept speziell für die SoftwareEntwicklung interessant ist, weil es das Zerlegen eines Problems in überschaubare Teile sozusagen implementiert, hat sich auch Perl davor nicht verschlossen. Jedoch sind Objekte in Perl5 bei Weitem nicht so grundlegend in die Sprache eingeflossen wie in der SpracheJava oder SprachePython (...).

Es gibt also durchaus auch Probleme, die beleuchtet werden müssen, damit OopInPerl auch "richtig" funktioniert.

Alles folgende wird nur für Perl 5 gelten, Perl 6 ist nämlich generell objektorientiert.

Grundlage: Perl 5 Objekte    

Ein Objekt ist kein grundlegender Datentyp in Perl, Objekte sind das Produkt eines Tricks, der auf Referenzen angewandt wird. Deswegen ist für das Verständnis von Objekten das Verständnis von Referenzen Voraussetzung. Wer dort also noch nicht wirklich den Durchblick hat, wird hiermit nicht so viel anfangen können.

Ein Perl5Objekt? entsteht, indem man einfach einer Referenz sagt, dass es zu einem Package gehört. Dies passiert durch das dafür geschaffene bless().

bless {anonyme=>'Hash-Referenz'} , 'Package'

Das erste Argument für bless() ist die zu "segnende" Referenz, das zweite das Package, das dadurch zur Klasse erhoben wird. Dabei wird die Referenz selbst manipuliert und ist gleichzeitig Rückgabewert von bless().

Eine derartig "gesegnete" Referenz ist an sich noch nichts besonderes: Eine Referenz wie jede andere, fast! Denn nun gibt es die Möglichkeit Methoden aufzurufen:

my $obj = []; #anonyme arrayreferenz

bless $obj,'Some::Package';

$obj->methode();

Diese methode(), und das ist bereits der ganze Zauber, wird nun im Namensraum 'Some::Package' gesucht und an diese Funktion wird die Referenz $obj als erstes Argument übergeben. Das war's.

sub Some::Package::methode {
    my $obj = shift; #erstes Argument entnehmen, vgl. PerlIdioms

    print "$obj\n";
}

Vererbung?    

Ja. Dazu dient das Array @ISA (Im Sinne von "is a"). Dieses Array enthält die Superklassen einer Klasse, dh. @Some::Package::ISA enthält die Superklassen der Klasse Some::Package.

package Eine::Klasse;
our @ISA = qw/Dessen::Superklasse/;

#...

Was passiert damit? Nun, die Magie der OopInPerl besteht darin, dass Methoden, die über eine "gesegnete" Referenz aufgerufen werden, in dessen Klassen-Package gesucht werden. Wird aber eine Methode nicht gefunden, gibt es nicht sofort einen Fehler, sondern es werden alle Superklassen nach dieser Methode durchsucht. Welche das sind, steht in @ISA der jeweiligen Klasse.

Da @ISA ein Array ist, lässt sich folgern, dass Perl Mehrfachvererbung unterstützt. Richtig gefolgert, das tut Perl. Eine Klasse erbt alle Methoden, die in seinen @ISA-Klassen stehen.

Eine Anmerkung: Nun könnte es ja theoretisch passieren, dass ein Package durch @ISA ein anderes zur Superklasse erklärt und selbst von jenem als Superklasse deklariert wird. Dieser Fehler wird von Perl entdeckt und entsprechend wird eine endlose Suche verhindert.

Eine Anmerkung zu @ISA    

Da Klassen nichts anderes als Packages sind, werden sie auch als Module gespeichert. Module müssen bekanntermaßen geladen werden, via use bzw. require. Die bloße Manipulation von @ISA führt das nicht durch. Um aber nicht jedesmal folgendes tun zu müssen:

package Klasse;
use Superklasse;
our @ISA = qw/Superklasse/;

gibt es das 'base' Pragma, das diese zwei Schritte vereinigt:

package Klasse;
use base qw/Superklasse/;

Attribute von Objekten / Objektdaten    

Nun, ein Objekt besteht nicht nur aus Methoden, sondern es enthält immer einen Satz an Daten, die über die Methoden genutzt/manipuliert werden. In "richtigen" OO-Sprachen sind diese Member-Variablen Teil des Sprachkonzeptes und keine Frage. Perl gehört nicht zu diesen und lässt es offen, wie man die Daten des Objektes speichert.

Intuitiv sagt man sich, dass diese Daten doch in der Referenz gespeichert werden können, die zum Objekt erhoben wird:

bless [ 'Die' , 'Daten' , 'im' , 'Objekt' ] , $class
# oder abar als hash, was die populärste Methode darstellt
bless {  foo => 'xyz',
         bar => '...',
} , $class

Dieser Ansatz ist so einleuchtend, dass er zum generellen OO-Ansatz geworden ist und i.d.R. nicht hinterfragt wird. Zu Unrecht, wie sich herausstellt.

Eine Klasse    

OOP-Einführungen haben immer eine Beispiel-Klassenhierarchie, daran lässt sich viel Erklären. OOP schwach zu kennen, setze ich zwar voraus, aber trotzdem verwende ich diese Einführungstechnik, um die Perl-spezifischen besonderheiten zu erklären.

package Fahrzeug; #Klasse aller Fahrzeuge mit Rädern
use Data::Dumper 'Dumper';

# Der Konstruktor, dessen Name keineswegs festgelegt ist
sub erschaffe {
    my $class = shift;
    
    bless {
        'räder'  => shift || 4,
        'farbe'  => shift || '-',
        'plätze' => shift || 0,
    } , $class
}

sub dump {
    my $self = shift; #my $this = shift; as you like it

    print "$self\n",Dumper($self)
}

Diese Klasse lässt sich prompt verwenden:

use Fahrzeug;
use Data::Dumper 'Dumper';

my $instanz = Fahrzeug->erschaffe(4,'rot',5); #Schreibweise "erschaffe Fahrzeug (...)" ist möglich

$instanz->dump;

Dieses kleine Beispiel sollte man sich ansehen (und es einmal ausführen), um zu sehen, wie die Objektdaten gespeichert werden.

Datenkapselung    

OOP ist so toll, weil sie uns die Datenkapselung so einleuchtend abnimmt. In Perl ist das mit der Kapselung "so eine Sache", wenn man die Objektdaten wie gezeigt speichert.

$instanz->{farbe} = 'blau'

$instanz->dump;

Dieses Vorgehen widerspricht natürlich dem Konzept, also müssen Methoden her, die den Zugriff auf die Daten kapseln.

sub Fahrzeug::get_farbe {
    my $self = shift;
    $self->{farbe}
}
sub Fahrzeug::set_farbe {
    my $self = shift;
    $self->{farbe} = shift;
}
#...

Und aus dem obigen Code wird:

$instanz->set_farbe( 'blau' );

$instanz->dump;

So weit, so gut. Allerdings kann diese Zugriffsmethode (auf eigenes Risiko) umgangen werden.

Eine Tochterklasse    

package Auto;
use base 'Fahrzeug';

sub get_ps { #PS-Zahl, weil motorisiert
    my $self = shift;
    $self->{ps}
}
sub set_ps {
    my $self = shift;
    $self->{ps} = shift;
}

Nun, das Auto erbt alle Methoden der Superklasse, (auch den Konstruktor) und fügt diese zwei hinzu.

So weit ist die Vererbungsgeschichte ja ganz toll, aber es gibt einen Haken.

Der Konzeptfehler    

Die oben beschriebene Technik, die Daten "im Objekt" zu speichern leuchtet ein. Die Informationen stecken _in_ der Instanz. Leider hat dieses Vorgehen einen großen Nachteil. Während in anderen Sprachen, wo Objekte nicht wie in Perl zu 95% selbstgestrickt sind (OopInPerl ist SyntacticSugar?), die Daten von Objekten und Klassen vom Interpreter/Compiler?/RE verwaltet werden, habe ich bei Perl selbst im Griff, wo die Daten sind und deswegen kann ich sie in Perl auch zerfetzen.

Interne Methoden    

Per Konvention ist in Perl festgelegt, dass Funktionen und Variablen mit "_" als erstem Zeichen Interna sind, die die Außenwelt nicht interessieren sollen. Hilfsfunktionen in Modulen und debugging-flags etc. werden so benannt. Wie sieht es nun in einer Klasse aus? Gibt es "Hilfsmethoden"? Prinzipiell schon:

sub _private_methode {
    my ($self,$foo) = @_;

    #mach was ...
}

sub oeffentlich_superklasse {
    my ($self,$arg1,$arg2) = @_;

    # ...
    $self->_private_methode( $arg2 );

    #...
}

Gut, da niemand die Konvention bricht, ist diese Methode privat, es kennt sie also prinzipiell niemand. Aber was ist denn nun in der Tochterklasse? Was passiert hier:

package Tochteklasse;

sub _private_methode {
    my ($self,%gaga) = @_;

    # mach was ...
}

sub oeffentlich_subklasse {
    my ($self,$bar,$buz) = @_;

    $self->_private_methode( a => $bar , b => $buz);
    #...
}

Nun, die Tochterklasse läuft prima, oeffentlich_subklasse() verwendet _private_methode(), toll. Aber was ist wenn eine Instant der Tochterklasse doe Methode oeffentlich_superklasse() aufruft, was ja völlig i.O. ist? Nun, dann wird dort die _private_methode() aufgerufen, die aus der Tochterklasse stammt und diese tut etwas völlig ungewolltes.

Also keine "internen Methoden"    

Exakt, für "interna" verwendet man also nicht die OOP-Syntax, sondern einfache Funktionen. Dann wird zur Compilierzeit festgestellt, welche Funktion verwendet wird und die Vererbungen und Überschreibungen kommen uns nicht mehr in die Quere.

Interne Member-Variablen    

Noch viel garstiger zeigt sich dieses Problem bei Member-Variablen, die ja bis jetzt in der Hashreferenz gespeichert wurden, die die Objektinstanz darstellt. Die Superklasse speichert das Ergebnis einer aufwendigen Aktion in $self->{_geheim}, die Subklasse speichert etwas völlig anderes in $self->{_geheim} und die Superklasse bekommt irgendwelchen "Müll" an diese Stelle geliefert.

Auch korrekte Kapselung der Variablen durch Accessor- und Mutatormethoden löst das Problem nicht.

package Superklasse;
sub :_intern { #wird nur als Funktion aufgerufen
    my ($self,$wert) = @_;
    $self->{_intern} = $wert if $wert;
    $self->{_intern};
}
sub berechne_dings { #Methode
    my ($self) = @_;

    _intern($self) or  # Interna werden als Funktionen aufgerufen
       _intern($self, _berechne_komplexen_dings( $self ) );
}
    
package Subklasse;

sub _intern { #wird nur als Funktion aufgerufen - keine Kollision der Funktion selbst!
    my ($self,$wert1,$wert2) = @_;
    $self->{_intern} = [$wert1,$wert2] if $wert1 and $wert2;
    @{ $self->{_intern} || [] }
}

sub berechne_werte {
    my ($self) = @_;

    my ($x,$y) = _intern($self);
    ($x,$y) = _intern( $self , _berechne_komplexe_werte($self) )
        unless $x and $y;

    ($x,$y)
}

Je nachdem, welche Klasse nun zuletzt das _intern Feld manipuliert hat, beim nächsten Aufruf durch die Andere sind Daten korrumpiert. Ich rufe berechne_dings() auf und danach berechne_werte und paff: can't use string as array reference ... Obwohl nur die richtigen Methoden und Funktionen zum Zugriff auf diese Daten verwendet wurden.

Und das nur, weil die beiden Klassen die Konvention berücksichtigen und die _felder nicht beachten und deswegen versehentlich kaputt machen. Es ist unmöglich und kontraproduktiv, zu verlangen, dass man alle internen flags eine Superklasse kennt, um eine kleine Tochterklasse zu schreiben. Es muss also eine andere Lösung her.

Die Lösung    

InsideOutObjects?. Entgegen dem intuitiven Gedanken, alle Daten im Objekt zu sichern, speichert diese Methoden alle Daten in lexikalischen Hashes der Klasse und verwendet die Speicheradresse der Instanz als Zeiger auf diese Daten.

Kann man nicht einfach per Konvention private Member mit _Klassenname qualifizieren? Z. B.
$this->{_klasse}{private}
?

Yep, das ist ein guter Ansatz, hat aber den Nachteil, dass die Superklasse das auch schon getan haben muss, was aber bei CPAN-Module kaum vorauszusetzen ist. Ausserdem schützt es nicht vor Typos, was die InsideOutObjects? tun, aber das erkläre ich, wenn ich mal keine Geschichte-Hausaufgaben habe :) Siehe auch die PerlMonks? Diskussion [lesenswert!]

Hat man die Möglichkeit, eine Klassenhierarchie komplett kontrolliert aufzubauen (also ohne Altlasten), kann der Ansatz HoH? durchaus sinnvoll sein:

$object = {
   Superklasse => { foo => 1 },
   Subklasse   => { foo => 3 },
}

Dadurch hat jede Klasse ihren eigenen Speicherraum und kann den Zugriff darauf publik via Methoden erlauben.

Was ist nun aber, wenn die Superklasse das alte Schema verwendet? In dem Fall kann ich das Objekt selbst (die gesegnete Variable) nicht für meine Daten verwenden, das dieses von der Superklasse zumindest theoretisch beliebig verändert werden kann.

Auch die Frage nach Typos spielt hier hinein:

sub foo { #accesssor/mutator methode für Eigenschaft foo()
    my ($self, $new_value) = @_;

    if( defined $new_value ){
        $self->{fooo} = $new_value #auf der "o"-Taste hängengeblieben, passiert
    }
    
    return $self->{foo}
}

Hmmm. Hier wird nichts funktionieren, aber perl hat keine Chance, einen solchen Fehler zu bemerken, denn es ist ja kein "Fehler".

Also wird aus zwei Gründen das Objekt umgekrempelt, d.h. die Daten werden nach aussen gelegt. Dabei macht man sich die Eigenschaft von Referenzen zu nutze, dass sie im String-Kontext eine eindeutige Speicheradresse zurückgeben. Dieser String ist unwidersprüchlich und kann als Schlüssel in einem Hash verwendet werden.

Damit wir die Daten privat halten und perl die Chance geben wollen, uns einen Typo anzuzeigen, legen wir also in der neuen Klasse für jede Member-Variable des Objektes ein lexikalisches Hash an:

package Subklasse;
use base 'Superklasse';

my @members = \(  #  \( %a , @b , $c ) entspricht ( \%a , \@b , \$c )

   my %foo,
   my %bar,

);

# @members ist also ( \%foo , \%bar )


# Konstruktor wird von Superklasse geerbt.

sub foo { #accessor/mutator
    my ($self , $new_value ) = @_;

    if( defined $new_value ){
        $foo{ $self } = $new_value
    }

    return $foo{ $self }
}

sub bar {
    my ($self , $new_value ) = @_;

    if( defined $new_value ){
       $barr{ $self } = $new_value # "barr", die "r"-taste klemmt manchmal, passiert
    }

    $bar{ $self }
}

Nun, dieser Code wird zunächst zur Compilezeit bereits einen Fehler hervorrufen, denn "%barr" existiert nicht, der Typo wird also sofort entdeckt, stundenlange Fehlersuche wird vermieden. Und das Objekt der Superklasse? Nun, es wird nicht angerührt.

Das Kleingedruckte    

Nun, das ist alles gut und schön, jedoch ist die Klasse in dieser Form noch nicht i.O., denn auch gelöschte Objekte belegen hier Speicher. Wir müssen explizit die Einträge löschen:

sub DESTROY { #destruktor-Methode wird aufgerufen, wenn ein Objekt verschwindet
    delete $_->{ $_[0] } for @members
}

Da wir referenzen auf unsere Member-Hashes %foo und %bar in @members gespeichert haben, geht das hier ganz einfach, darf aber nicht vergessen werden!

Weitere Probleme ist z.B. das Dumping von Daten, denn die Objektdaten werden von Modulen wie Data::Dumper nicht erfasst, ebenso auch nicht von Storable, weswegen diese Objekte ohne weiteres nicht serialisierbar sind, d.h. sie werden immer unvollständig erfasst.

Nun, für beide Zwecke kann man Methoden anbieten, die zusätzliche Arbeit wird sich aber nicht ersparen lassen.

( ToDo )


KategoriePerl KategorieOop
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Text dieser Seite ändern (zuletzt geändert am 14. Januar 2004 21:08 (diff))
Suchbegriff: gesucht wird im Titel im Text