Objektorientierung mit Perl |
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().
|
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:
|
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.
|
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.
|
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:
|
gibt es das 'base' Pragma, das diese zwei Schritte vereinigt:
|
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:
|
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.
|
Diese Klasse lässt sich prompt verwenden:
|
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.
|
Dieses Vorgehen widerspricht natürlich dem Konzept, also müssen Methoden her, die den Zugriff auf die Daten kapseln.
|
Und aus dem obigen Code wird:
|
So weit, so gut. Allerdings kann diese Zugriffsmethode (auf eigenes Risiko) umgangen werden.
Eine Tochterklasse |
|
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:
|
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:
|
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.
|
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.
|
|
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:
|
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:
|
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:
|
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 )