Wegbeschreibung eines Perl-Entwicklers
Die Entwicklung meines Web-Application-Framework ist noch nicht abgeschlossen. Diese Entwicklung wird auch nie ein finales Stadium erreichen, denn gerade im Laufe der Entwicklung ergeben sich neue Ideen deren Verwirklichung jedoch nicht unbedingt neue Features in der Programmiersprache erfordern sondern oftmals auf das zurückgreifen was verfügbar ist. In diesem Artikel zeige ich den Weg meiner Entwicklung anhand eines konkreten Beispiels. Es ist schon so, daß ich mich oft gefragt habe warum ich nicht gleich auf das Alles gekommen bin. Aber so manche Idee wächst eben erst mit den Anforderungen und ebenso eine dazugehörige Lösung. Andererseits ist dieser Artikel auch eine Laudatio auf Perl, denn alles was hier beschrieben ist war schon vor 20 Jahren machbar ab Major-Version 5 und von daher habe ich das ganze Framework von Anfang an objektorientiert entwickelt.
Aus allgemein verständlichen Gründen wird man ein komplexes Framwework nicht in einer einzigen Scriptdatei verwirklichen. Das ist weder möglich noch zweckmäßig, vielmehr wird man von Beginn an einen modularen Aufbau anstreben. In Perl beginnt die erste Zeile eines Moduls mit:
package XR;
Dabei steht der Name XR
für Extended Replace
, kurzum, so heißt meine eigens entwickelte Template-Engine. Mit dem Schlüsslwort package
wird zum Einen der Name der Klasse bekanntgegeben und zum Anderen ein eigener Namensraum (Namespace) definiert. Neben zwei privaten Methoden, einer Public-Methode und einer Helper-Class ist diese package jedoch nicht als eine instanzierbare Klasse entwickelt. Von daher ist die Public-Methode eine statische Methode, also eine Klassen-Methode die von außerhalb mit dem Namen der package aufgerufen wird:
require XR;
XR::xr(TEMPLATE, STASH);
womit auf den Export dieser Funktion in den eigenen Namespace verzichtet werden kann. Nun ist es aber auch so, daß diese Funktion XR::xr()
praktisch in jeder Webanwendung gebraucht wird weil sogut wie alle Webanwendungen über ein Template in den Browser gerendert werden. Von daher ist es naheliegend, diese Methode einer fremden Klasse zu einer Methode der eigenen Klasse zu machen und genau das nennt man Delegation:
sub xr{
my $self = shift;
return XR::xr(@_);
}
Das heißt, daß diese Delegation einfach nur ein Wrapper für die externe Methode ist; in der eigenen Klassenhierarchie jedoch eine Methode die von jeder abgleiteten Klasse geerbt wird und somit nur einmal definiert werden muß. Im Laufe der Zeit stiegen jedoch die Anforderungen an meine Template-Engine, insbesondere wurde es immer dringlicher, HTML-Entities zu kodieren. Die Frage die sich daraus ergab war, wie eine dazu erforderliche Klasse eingebunden werden kann ohne daß der eigene Code unübersichtlich wird. Die Lösung führte zunächst wieder auf eine eigene Methode:
sub encode_entities{
my $self = shift;
my $val = shift;
return HTML::Entities::encode_entities( $val, q(<>#'""'&) );
};
die für diesen Zweck jedoch bisher immer extra für jeden Platzhalter einzeln aufgerufen werden musste. Woraus sich die nächste Frage ergab wie man das vereinfachen kann. Im ersten Schritt hatte ich hierzu eine Klassenvariable in der package XR
festgelegt:
# XR::ENCODE_ENTITIES
# Hash mit den Namen der Platzhalter die encoded werden sollen
Damit ließ es sich prima arbeiten, in der eigenen Subklasse war dafür nur dieser Hash zu notieren und danach konnte jede Abfrage aus MySQL direkt der Template-Engine übergeben werden:
%XR::ENCODE_ENTITIES = (
title => 1,
descr => 1,
name => 1,
mesg => 1
);
sub browse{
my $self = shift;
# Daten zum Rendern in das Template
$self->{STASH}{messages} =
$self->selectall_arrayref(q(
select
title, descr, name, mesg
from messages
), {Slice=>{}});
}
Aber schön ist das auch nicht mit einem Hash im globalen Namespace. Der letzte Schrei war wie immer die Delegation:
sub browse{
my $self = shift;
# xr_encode_entities delegiert die Namen der Platzhalter
# an die Klassenvariable der XR-Klasse %XR::ENCODE_ENTITIES
$self->xr_encode_entities(qw(
title descr name mesg
));
# Daten zum Rendern in das Template
$self->{STASH}{messages} =
$self->selectall_arrayref(q(
select
title, descr, name, mesg
from messages
), {Slice=>{}});
}
Erst damit wurde es möglich, die Namen verschiedener Platzhalter in mehreren Methoden eigenständig und unabhängig voneinander festzulegen und der Code bleibt übersichtlich. Die Klassenvariable der XR-Klasse ist nicht weg, sie wird lediglich im Namespace der eigenen Klasse genutzt und bestimmt ebenda das Verhalten der ebenfalls statischen Methode XR::xr()
. Eine vollständige Umstellung im Sinne OOP würde die XR-Klasse instanzierbar machen, somit könnte man die Liste der Platzhalter dem Konstruktor übergeben und bräuchte dann auch keine Klassenvariable.
Der ganze Inhalt für diese Seite mit dem URL /mod2class.html
wird über eine Instanz einer Klasse ausgeliefert die von der main
-Class erbt. Für das Content-Management könnte man diese Inhalte (title, descr, body) an genau diesen URL senden. Das würde aber bedeuten daß die Instanz der an den URL gebundenen Klasse eine Methode haben muß womit diese Inhalte permanent gemacht werden können. Das würde aber auch bedeuten, daß jeder URL dieser Webseite dazu befähigt werden muß als Webservice zu dienen welcher Veränderungen an den Inhalten vornimmt und dafür einer Zugangskontrolle bedürfte was wiederum zur Folge hätte daß die Inhalte nicht mehr öffentlich abrufbar sind.
Aus diesem Grund habe ich für das Content-Management einen festen, nicht öffentlichen URL mit Zugangskontrolle konfiguriert. Somit werden alle lokal erstellten Inhalte per Knopfdruck aus dem Editor heraus an diesen einen URL gesendet. Wie üblich ist auch dieser Manager-URL an eine Klasse gebunden die über die entsprechenden Methoden verfügt zum Ändern von Inhalten und deren Persistierung. Doch wie kommen nun die an den Manager-URL gesendeteten Daten dahin wo sie hingehören? Die Lösung ist ganz einfach: Die Inhalte sämtlicher Seiten dieser Domäne liegen zum Zeitpunkt der Auslieferung im Hauptspeicher als eine Datenstruktur in der main
-Instanz selbst. Das heißt daß jede Instanz die Inhalte ausliefert auch auf die Inhalte der anderen Seiten zugreifen kann was z.B. für den Aufbau des Menü und einer konsistenten Navigation über die gesamte Website unabkömmlich ist.
Kurzum: Die Inhalte aller Seiten liegen fein strukturiert in einem Attribut einer jeden Instanz die eine Seite ausliefert, so auch in der Instanz die für den Webservice und das Management-Backend zuständig ist. Der Controller dieser Instanz nimmt also den POST entgegen, baut die neuen oder geänderten Inhalte als Attribute ein und ruft danach den Serializer auf, eine Methode welche die Daten zurück auf die Festplatte schreibt. Untenstehend der dazugehörige Code:
sub control{
my $self = shift;
if( $self->param('pub') ){
# REST Schnittstelle
# rufe Methode thaw
my $hunt = $self->thaw(*STDIN);
my $url = $hunt->{url};
$self->{BIN}{$url} = $hunt;
# BIN zurückschreiben
# rufe Methode freeze
$self->freeze();
$self->content("Written: $url");
}
Nehmen wir eine Webpräsenz deren Umfang stetig wächst. Jede Seite benötigt immer wiederkehrende Angaben wie z.B. einen Titel und eine Beschreibung, Angaben zum Autor und natürlich den sichtbaren Inhalt selbst. Egal ob es sich um eine aktive Seite handelt auf der Benutzer in Formularen bestimmte Dinge eingeben können oder Seiten die einfach nur zum Lesen und Angucken sind, o.g. Eigenschaften sind immer dieselben. Infolgedessen können wir eine Einzelseite oder auch eine ganze Anwendung als ein Objekt betrachten mit den Eigenschaften Titel, Beschreibung und Seiteninhalt (body). Und unabhängig davon woher diese Angaben kommen (Datei, MySQL...) braucht man eine Instanz welche für die Auslieferung dieser Seiten zuständig ist. Auch der Ablauf ist immer wieder derselbe, Benutzer fordern einen bestimmten URL im Browser an und die dazugehörige Seite wird, soweit vorhanden ausgeliefert. Somit ist die Instanz zuständig dafür den Request entgegenzunehmen, den zum angeforderten URL zugehörigen Inhalt rauszusuchen und schließlich diesen auszuliefern. Mit anderen Worten heißt daß, daß die Instanz nicht nur Daten transportiert sondern auch über Methoden verfügen muß um seiner Zweckbestimmung gerecht zu werden.
Natürlich muß nicht jede Instanz welche eine Seite ausliefert auch in der Lage sein Benutzereingaben zu verarbeiten. Das bedeutet, daß es je nach Anwendung bzw. Seiteninhalt unter Umständen Aufgaben gibt die erheblich voneinander abweichen. Und natürlich braucht man keine Datenbankverbindung wenn es nur darum geht den Text einer Datei zum Browser zu schicken oder eine Grafik. Diese Aufgaben, sprich Methoden zu organisieren, genau das ist die Zweckbestimmung von Klassen. Und hier kommt die Vererbung ins Spiel, derart daß Klassen die sehr unterschiedliche Methoden definieren eben nicht auch all diejenigen Methoden definieren müssen die für alle Instanzen dieselben sind. Letztere werden in der Basisklasse definiert, also in derjenigen Klasse von der alle abgeleiteten Klasse bzw. deren Instanzen erben.
Objekte sind abstrakte Datentypen, transportieren und bewegen Daten. Dafür brauchen Objekte Methoden und diese Methoden werden in Klassen organisiert. Von daher sind Objekte auch Instanzen dieser Klassen was sie dazu befähigt diese Methoden auszuführen.
Kritiker der mit Perl seit Major-Release 5 implementierten Objektorientierung haben den Entwicklern stets vorgeworfen, daß die OO-Implementierung mangelhaft sei. Perl würden wichtige Schlüsselworte fehlen die es in anderen Sprachen längst gibt und Perl-Objekte seien nur Referenzen auf Datenstrukturen die mit dem Namen einer Klasse gesegnet sind. Des Weiteren sei es ein großer Mangel daß man in Perl-Objekten keine versteckten bzw. schreibgeschütze Eigenschaften festlegen kann und alle Eigenschaften eines Perl-Objekts von außen zugänglich seien, ebenso wie sämtliche in einer Klasse definierten Methoden allesamt nur public sind und von daher nicht privat sein können. Und darüber hinaus natürlich immer wieder die Leier von der fehlenden Typisierung.
Auf diese z.T. fragwürdigen Argumente eingehend, sei zunächst einmal gesagt, daß man gerade in OOP kein Schlüsselwort static
braucht und package
sehr wohl ein Schlüsselwort ist. Daß mit man mit package
auch den Namen einer Klasse definiert und und nicht mit class
ist in Perl eben so und nicht etwa ein Mangel. Genauso wie eine Methode eben mit dem Schlüsselwort sub
deklariert wird. Und ganz sicher muß ein Objekt mit dem Namen einer Klasse gesegnet sein (blessed
) denn nur so ist dieses Objekt eine Instanz dieser Klasse und dazu berechtigt Methoden auszuführen die in ebendieser Klasse definiert sind. Ob eine allen Klassen übergeordnete Methode new
diese Segnung vornimmt oder der Programmierer selbst Hand anlegt mit bless
tut doch der OOP gar keinen Abbruch, geschweige denn ist wirklich ein Mangel.
Und überhaupt kann Perl sehr wohl mit Datentypen umgehen sogar plattformübergreifend. So kann man mit Perl beispielsweise einen Floating-Point-Value unzerknittert wiederherstellen aus einer Bytesequenz die mit c, Java, PHP, JavaScript oder einer beliebig anderen Programmiersprache erzeugt wurde. Und selbstverständlich kann man mit Perl auch derartige Bytesequenzen erzeugen, siehe pack()
-Funktion. Und wer seine Methoden verstecken will, bitteschön: Mit Perl XS kann man seinen ganzen Source-Code verstecken. Welcher Anwender hätte da ein Interesse daran herauszufinden ob es noch weitere Methoden gibt die nach außen hin nicht dokumentiert sind außer wenn es darum geht, den Code weiterzuentwickeln? Also mal ehrlich, das ist doch kein Mangel. Sämtliche Kritiken dieser Art sind einfach nur lächerlich.
Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. sos@rolfrost.de.