Benutzer-Werkzeuge

Webseiten-Werkzeuge


start:software:moderne_webauftritte

Moderne Webauftritte

Herausforderung für Entwickler

Die Entwicklung von zeitgemäßen Webseiten wird zunehmend aufwändiger. Im Zeitalter von Web 2.0 erwarten Besucher von einer Webseite einen Grad von Interaktivität, den es früher nur mit Client-Anwendungen gab, immer neue Hacker- und Spammer-Angriffe erfordern immer aufwändigere Gegenmaßnahmen und viele moderne Webanwendungen ermöglichen eine Personalisierung durch den Nutzer, um komplexe Anwendungsfälle wie eine Beteiligung bei der Auswahl, Bewertung oder gar Erstellung von Inhalten zu ermöglichen. Moderne Webanwendungen haben häufig integrierte Diskussionsforen, Wikis oder Blogs, RSS-Feeds und besitzen Schnittstellen zu Webservices wie Twitter, Facebook, Google-Maps, erledigen Geolocation oder verifizieren zumindest eingegebene Addressen oder Bankleitzahlen über einen Webservice. Kommerzielle Seiten brauchen häufig noch Schnittstellen zu Bezahldiensten wie z.B. PayPal oder Shops wie Amazon. Zunehmend ist auch eine Vielzahl von Endgeräten von Smartphone bis Hybrid-TV neben einer Vielzahl von Browsern für die gängigsten PC- Betriebssysteme zu unterstützen.

Dies alles von Grund auf selbst - mit welcher Programmierumgebung auch immer - umsetzen zu wollen, ist für kein Projekt ökonomisch vertretbar und es gibt auch kein Web-Framework, das allein all diese Anforderungen abdecken könnte. Das liegt auch an der Heterogenität bei der Umsetzung: Interaktive Anwendungen laufen nicht nur auf einem Server, sondern erfordern auch Client-Programmierung und Kommunikationsprotokolle zwischen Client und Server. Auf der Server-Seite hat man es auch stets mit mehreren Servern zu tun: Mindestens einem Datenbank-Server, einem Server für die Auslieferung statischer Webseiten, mindestens einem Anwendungsserver, meist Schnittstellen zu verschiedenen Webservices und vielleicht noch Loadbalancer.

Alle diese Server müssen mit den unterschiedlichsten Protokollen kommunizieren und Entwickler müssen sich mit all ihren Schnittstellen beschäftigen.Die oben aufgeführten Komponenten und Dienste sind mit ganz unterschiedlichen Programmierschnittstellen und Datenformaten anzusprechen, für viele gibt es bereits fertige Frameworks oder Plugins, aber eine Webanwendung muß letztlich all diese unterschiedlichen Technologien, Frameworks und Kommunikationsbeziehungen zu einem stabil und performant alle Anwendungsfälle durchführendem Ganzen zusammenbringen (orchestrieren). Die Herausforderung bleibt mindestens, für den gesamten Workflow zueinander kompatible Komponenten zu finden, also zueinander passende Versionen von geeigneten Frameworks mit einer Menge „Glue Code“ zu vereinigen und mit Sicherheitsupates aktuell zu halten. In Abhängigkeit geraten …

Die Kombination unterschiedlicher Frameworks wäre unkritisch, wenn diese nicht voneinander oder gemeinsam genutzten Drittkomponenten - wie etwa Laufzeitbibliotheken oder wiederum anderen Frameworks - abhängen würden. Gerade bei statisch typisierten, objektorientierten Programmiersprachen wie Java oder C# und nicht optimal modellierten Klassenhierarchien hängen schnell einzelne Klassen eines Frameworks oder einer Anwendung von einer bestimmten Version eines anderen Frameworks ab. Wenn nun verschiedene Anwendungsteile unterschiedliche Versionen solcher Basisklassen oder Schnittstellen brauchen, baut man entweder aufwändige Adapter oder findet schlimmstenfalls gar keine Lösung. Unter Umständen sind dann selbst Sicherheitsupdates von Basiskomponenten wegen solcher Probleme nicht mehr möglich- die Anwendung wird unwartbar.

Bei der ersten Generation von Web-Frameworks führten riesige Klassenhierarchien mit langen Vererbungspfaden zu eher schwerfälligen, großen, wenig performanten oder unflexiblen Anwendungen bzw. Anwendungsservern wie etwa einigen zur Java Enterprise Edition kompatiblen Java-EE Servern oder Zope für Python. Die mit solchen Frameworks verbundenen Aufwände sind für moderne Webauftritte und die immer kürzer werdenden Projektlaufzeiten meist nicht mehr tragbar.

Modernes Softwareengineering versucht wegen solcher Probleme und flexibler Konfigurierbarkeit ganz allgemein, Abhängigkeiten zwischen Komponenten auf das absolut notwendige zu beschränken. Ein noch zu kleiner Schritt dorthin ist die Verwendung schlanker Interfaces oder Mixins statt Ableitung von konkreten bzw. abstrakten Klassen. Weiter geht schon eine Dependency- Injection (DI) oder Inversion of Control (IoC) genannte Technik, die auf aspektorientierter Programmierung aufbaut: Beziehungen zwischen Komponenten/Frameworks werden per Konfigurationsdatei explizit erst zur Laufzeit mit der Erstellung von Objektinstanzen hergestellt bzw. in die Komponenten „injected“.

In der Java-Welt - aber auch für Python und Microsoft's .NET-Framework - ist das Framework Spring wohl die bekannteste Umsetzung dieses Konzepts. Spring erleichtert die Kombination und Konfiguration von auf verschiedene Anwendungsschichten wie Persistenz oder Präsentation spezialisierten Frameworks wie Hibernate (Persistenz), Struts oder Tapestry (Präsentation von Webseiten) in einer Anwendung.

Schichtenmodell: Model - View - Controller

Ein unkontrollierbarer „Wildwuchs“ von Kommunikationsbeziehungen und damit einher gehenden Komponenten-Abhängigkeiten läßt sich durch klare Trennung von Aufgaben erreichen. So habe ich oben bereits eine Persistenzschicht und eine Präsentationsschicht erwähnt. Was fehlt, ist noch eine Steuerungsschicht, welche den Ablauf der Anwendungsfälle steuert und dabei Daten zwischen der Präsentationsschicht und der Persistenzschicht transportiert. Diese drei Schichten finden sich wohl in jedem Webauftritt, insbesondere, sobald nicht nur unveränderliche Inhalte angezeigt werden oder wenn der Besucher auch Daten an den Server übermitteln kann.

Für jede dieser Schichten existieren spezielle Frameworks oder zumindest eine Trennung von Framework-Komponenten bei Frameworks, die alle drei Schichten abdecken. Zwar sind häufig Präsentations- und Steuerungsschicht recht eng miteinander verbunden, die Persistenzschicht sollte jedoch immer klar von den anderen Schichten getrennt sein. Wenn man gute Interaktionsmöglichkeiten möchte, werden nicht nur einfache Webseiten mit ein paar Links und Eingabefeldern ausgegeben, sondern der Benutzer kann z.B. die Auswahl der angezeigten Inhalte auch ändern, ohne dass die Seite komplett neu geladen werden müsste (Ajax- Technologie). Gute Interaktionsmöglichkeiten machen die Grenze zwischen Steuerungs- und Präsentationsschicht unscharf, weshalb die Architektur der Frameworks hier unterscheidet.

Die Verbindung zwischen diesen Schichten sollte jedenfalls recht lose sein - im Idealfall sollte man sogar das für eine Schicht verantwortliche Framework gegen ein anderes austauschen können, ohne am Code der eigentlichen Webanwendung allzu viel ändern zu müssen. Das Verbinden dieser Schichten ist deshalb ein typischer Anwendungsfall für Dependency Injection und Frameworks wie Spring. Doch zuerst möchte ich die Aufgaben der einzelnen Schichten verdeutlichen.

Persistenzschicht (Model): Frameworks für Business-Objekte

Jeder nicht triviale Webauftritt soll Besuchern Daten präsentieren oder auch Daten vom Besucher erfassen und dauerhaft und sicher abspeichern. Für das Abspeichern werden heute meist relationale Datenbanken wie MySQL, Postgres, MS SQL-Server oder Oracle verwendet, aber auch Objekt-Datenbanken, die sich heute oft NoSQL- Datenbanken nennen, gewinnen besonders mit Cloud-Diensten wie Google's AppEngine an Bedeutung.

Die Abbildung von Objektmodellen und Business-Objekten wie Nutzer- oder Produkt- Daten oder einfach nur Textbeiträgen wie in diesem Blog ist besonders bei einer relationalen Datenbank durch einen Paradigmenbruch mühsam, fehleranfällig und wartungsfeindlich. Häufig nutzen ja mehrere Anwendungen (evtl. auch bereits bestehende Client-Anwendungen) die gleiche Datenbank und Änderungen am Datenmodell schlagen dann auf alle Anwendungen durch. Feld für Feld muß zwischen Datenbank und Objekt-Attributen transportiert und wegen unterschiedlicher Typisierung oder Zeichensatz meist auch noch konvertiert werden. Datenabfragen liefern meist nicht nur ein Objekt, sondern ganze Kollektionen zurück, wenn in der Webanwendung gepufferte Datensätze aber verändert zurück geschrieben oder gar gelöscht werden sollen, muß man Synchronisations- oder Locking-Mechanismen umsetzen. Gerade bei Webanwendungen verschärft sich das Problem, weil sich ein Anwender mit mehreren Browserfenstern in unterschiedlichen Zuständen eines Anwendungsfalls oder anderen Teilen der Anwendung befinden kann und schon allein Synchronisationsprobleme verursachen kann.

Die Abfragesprache SQL ist zwar standardisiert, im Detail aber doch bei jeder Datenbank ein bißchen anders. Das gilt gerade auch für Wertebereiche und Darstellung von Datentypen oder administrative Aufgaben wie dem Anlegen oder Ändern von Objekten der Datenbank (Tabellen, Felder, Stored Procedures etc.). Ohne eine klar vom Rest der Anwendung isolierte Behandlung aller Datenbank-Aufgaben in einer Persistenzschicht wird eine Webanwendung nicht nur unwartbar, sondern wird durch nebenläufige Zugriffspfade und unnötig viele Verbindungen zum Datenbankserver fehlerhaft und langsam.

Zur Lösung dieser Probleme tragen Object Rational Mapper (ORM) benannte Frameworks bei. Sie bilden Business-Objekte (Kunden, Artikel, Textbeiträge, …), die als Klasse der jeweiligen Programmierumgebung definiert werden, in Datenbanktabellen ab und stellen einfache Methoden bereit, um Objekte als Datensätze zu erzeugen, zu lesen, zu aktualisieren oder zu löschen (create, read, update, delete → CRUD). Darüber hinaus erleichtern sie auch Abfragen durch Methoden, die eher der Syntax der jeweiligen Programmierumgebung entsprechen als die deklarative Datenbank-Abfragesprache SQL.

Über die Möglichkeiten der Datenbank hinausgehend können für Business-Objekte auch Contstraints (Feldbedingungen) definiert werden, um eine Validitätsprüfung beim Speichern durchzuführen. Constraints können im einfachsten Fall Aufzählungen, zulässige Wertebreiche oder reguläre Ausdrücke sein, Entwickler können aber auch beliebig komplexe Validierungs-Methoden selbst schreiben. Ziel muß jedenfalls sein, daß die Persistenzschicht nur korrekte Datensätze speichern kann und auch Integritätsprobleme verhindert, die etwa beim Löschen von Datensätzen passieren würden, die Datensätze einer anderen Tabelle referenziert. Auch für Synchronisations- Mechanismen und sichere Transaktionen ist das Persistenz-Framework zuständig- so muß etwa ein Rollback den alten Zustand der Datenbank herstellen können, wenn ein komplexer Anwendungsfall wie das Erfassen einer Bestellung mit mehreren Positionen abgebrochen wird oder der Benutzer den Vorgang aus welchen Gründen auch immer nicht in einem begrenzten Zeitraum abschließt.

Das wohl verbreiteste ORM-Framework für Java ist Hibernate, daß z.B. als NHibernate auch für Microsoft's .NET- Framework existiert. Microsoft selbst bietet mit Entity Framework 4 als Nachfolger von ADO.NET auch selbst ein ORM an, das sich vor allem durch die Abfragesprache LINQ hervortut. Das Framework Ruby on Rails hat sicher nicht zuletzt durch seine Persistenz-Komponente ActiveRecord so an Bedeutung gewonnen, das ActiveRecord für Persistenzframeworks anderer Entwicklungsumgebungen ebenso wie NHibernate nachprogrammiert wurde. Beim stark an Ruby on Rails nachempfundenen Framework Grails, welches auf der Ruby- ähnlichen, aber vollständig Java-kompatiblen Sprache Groovy aufsetzt, wurde gar ein ActiveRecord- ähnlicher Überbau auf das Original-Hibernate von Java draufgesetzt.

Auch für Python existieren einige ORMs. Das für dieses Blog verwendete Python Framework Django besitzt eine ActiveRecord nicht unähnliches ORM-Komponente, etwas mächtiger und flexibler ist aber das ORM-Framework SqlAlchemy, das etwa auch in aktuellen Versionen der Webframeworks TurboGears zum Einsatz kommt.

Präsentationsschicht (View): Webseiten-Frameworks

Die ältesten Web-Frameworks kümmerten sich naturgemäß primär um die Ausgabe von Webseiten. In Anlehnung an PHP bestand der erste Ansatz darin, in HTML-Code spezielle Tags einzubetten, in denen recht beliebig Quellcode der jeweiligen Entwicklungs-Plattform geschrieben wurde. Ein Anwendungsserver - bei Java Servlet- Container genannt - kompilierte bzw. führte diesen Code bei Aufruf der Seite aus und die Ausgabe des ausgeführten Codes wurde als zusätzlicher HTML-Code anstelle solcher Tags in die Seite eingefügt, wenn die Seite ausgeliefert wurde.

Bei Microsoft hieß diese Konstruktion dann Active Server Pages (ASP), bei Java nannte man es entsprechend Java Server Pages (JSP), für alle Skriptsprachen gibt es vergleichbare Konstruktionen. Der wohl immer noch populärste Java-Servlet-Container ist wohl immer noch Apache Tomcat.

Das Vermischen von Anwendungscode mit Präsentationscode wie HTML im gleichen Quelltext ist sehr aufwändig, wartungsfeindlich und fehleranfällig, insbesondere weil Webdesigner und Web-Anwendungsentwickler heute normalerweise unterschiedliche Berufe mit ganz unterschiedlicher Ausbildung sind und schlecht zusammen am gleichen Quelltext arbeiten können, ohne diesen zu beschädigen.

Deswegen gehen moderne Web-Frameworks andere Wege, denen aber die vollständige Trennung von Anwendungs- und HTML-Code gemeinsam ist. Spätestens wenn eine Seite auf verschiedenen Endgeräten dargestellt werden soll oder Ajax zum Einsatz kommt, erzeugt die Präsentationsschicht vielleicht gar keinen HTML-Code, sondern XML, JSON oder YAML. Das Endgerät greift möglicherweise auch gar nicht per Browser zu, sondern mit einer Silverlight-, Adobe Air- oder Adobe Flash- Client-Anwendung. Bei Hybrid-TV Geräte und Settop-Boxen von Samsung werden Benutzerschnittstellen komplett von einem Javascript- Framework aufgebaut, die Beschränkungen von HTML4 bzw. XHTML sind für moderne Bedienoberflächen heute häufig nicht mehr akzeptabel, ob HTML5 mit CSS3 hier die Rettung sein kann, muß sich erst noch zeigen.

Wie auch immer: ein modernes Präsentations-Framework sollte flexibel genug sein, beliebigen Text als Templates zu nutzen, in welchem Platzhalter bzw. Tags auf Code verweisen, den man mit der Programmiersprache seiner Wahl schreiben kann. Man sollte solche Custom-Tags auch in eigenen Taglibs einigermaßen komfortabel selbst programmieren können, Grundfunktionen für das Einfügen von Attributen oder auch ganzen Objekten sollten vorhanden sein, Kontrollstrukturen zur Fallunterscheidung (if/else/endif) und Iteration über Objektkollektionen (foreach) sollten aber zur Grundausstattung gehören. Wichtig sind auch Mechanismen zur Verschachtelung solcher Templates. Webseiten setzen sich ja meist aus getrennten Bereichen (z.B. Kopf, Navigationsleisten/Reiter, Seiteninhalt und Seitenfuß) zusammen, deren Code unabhängig voneinander ist. Bei Java gibt es für solche Seiten-Layout- Aufgaben das Framework SiteMesh, das auch für diese Seite eingesetzte Django-Framework besitzt eine sehr einfache, aber dennoch flexible und mächtige Präsentations-Komponente, die das nebenbei erledigt.

Besondere Anforderungen an die Präsentationsschicht stellen noch Eingabefelder bzw. HTML-Formulare. Möglichkeiten und Angebot von HTML zur Darstellung von Steuerelementen (Eingabefelder, Listen, Baumstrukturen, Reiter, Buttons, …) sind ja extrem bescheiden - zumindest verglichen mit GUI- Frameworks für Clients wie etwa Microsofts WPF bzw. Window.Forms oder Mozilla XUL. So müssen beispielsweise Eingabefelder ja Daten einer ganzen Reihe von Datentypen annehmen, validieren und anzeigen können und Listen kann man nicht immer mit allen in Frage kommenden Daten füllen, damit die ausgelieferte Seite nicht zu groß wird. Letzteres ist dann schon ein typischer Fall für Ajax- das häppchenweise Befüllen einer Liste ohne eine komplett neue Webseite anzufordern. Auch die z.B. vom Google- Suchfeld bekannte, mit jedem Tastendruck aktualisierte Vorschlagsliste ist Ajax- Komfort.

Die Validierung von Eingaben kann man natürlich auch der Persistenzschicht überlassen, diese kann die eingegebenen Daten aber erst prüfen, nachdem das komplette Formular an den Server geschickt wurde und für eine Rückmeldung muß die ganze Seite erneut an den Browser übertragen werden. Besser ist also, Fehleingaben gar nicht erst zuzulassen oder beim Verlassen des Felds dessen Inhalt mit Javascript-Code zu prüfen. Das ist viel Arbeit, die ein Präsentations-Framework dem Entwickler auch möglichst erleichtern sollte. Deshalb liefern komfortable Präsentationsframeworks mit der Webseite auch gleich Javascript Frameworks wie jQuery, Prototype oder Scriptacolous nebst etlichen Plugins für diese aus. Dazu gehören eventuell auch Wysiwyg- Editoren wie TinyMCE oder Wymeditor, wenn bei Blogs wie diesem, Foren oder Wikis formatierter Text eingegeben werden kann.

Eine wichtigere Anforderung als Bedienkomfort ist aber die Sicherheit. Sobald ein Benutzer Daten an eine Webanwendung schicken kann, muß man mit Hacker-Angriffen rechnen. Das Thema ist aufgrund der Vielzahl der Angriffsmethoden zu umfangreich, um es hier darzustellen. Es soll nur soviel gesagt werden, dass Hacker in Eingabefeldern gerne Javascript-, HTML- oder SQL- Codesequenzen eintragen. Wenn diese Daten ungeprüft durch die Schichten eines Webframeworks transportiert werden, besteht an verschiedenen Stellen die Möglichkeit, dass dieser Code z.B. beim Zusammenbau einer SQL-Abfrage an die Datenbank oder einer Navigation zu einer anderen Seite auch ausgeführt wird. Beim Empfangen von Formulardaten ist es auch wichtig zu prüfen, ob sie tatsächlich von dem Besucher stammen, dem dieses Formular auch angezeigt wurde. Derartige Sicherheitsmechanismen sollte ein Präsentationsframework bereits enthalten, denn eine eigene, stabile und wirklich sichere Umsetzung ist schwierig.

Steuerungsschicht (Controller): Seitennavigation

Für eine Web-Anwendung besteht die Steuerungsschicht vor allem in der Abbildung von Web-Adressen (Uniform Resource Locator) zu Controller-Implementierungen der Anwendung. Einem Besucher einer Web-Anwendung wird in einem Anwendungsfall eine Folge von Webseiten bzw. Formularen präsentiert. Das für die Kommunikation mit dem Webserver vorgesehene Protokoll http ist zustandslos. Bei einer Anfrage für eine Webseite (Request) werden zwar neben der Adresse eine Reihe anderer Daten wie beispielsweise Cookies an den Webserver geschickt, wenn der Server dann aber mit einer Seite geantwortet hat (Respond), ist seine Aufgabe erledigt.

Um einen Anwender also in einem Anwendungsfall, einem Flow von Webseiten zu halten und ihn dabei identifizieren zu können, muß er mit jedem weiterem Request eine Sitzungskennung (session id) übertragen, damit die Webanwendung ihn wieder erkennt und ihm Zugang zu bereits eingegebenen oder mit der Persistenzschicht ihm bereits zugeordneten Daten ermöglicht. Eine einfache, aber nicht sichere Möglichkeit ist, bei der nächsten Abfrage benötigte Daten und die Sitzungskennung hinter einem Fragezeichen an die Web-Adresse anzuhängen. Unsicher ist das deshalb, weil ein Angreifer dann leicht mal beliebige Sitzungskennungen und Abfragedaten durchprobieren kann und sich so unberechtigten Zugang zu Daten verschafft. Dies ist eines der ältesten Angriffs-Szenarien, das selbst bei Webauftritten bekannter Firmen und Organisationen und sogar Banken immer wieder vorkommt.

Leider kommen Cookies für Sitzungskennungen auch nicht immer in Frage, weil der Benutzer sie in allen Browsern abschalten kann. Ein beliebter Trick sind deshalb Formulare mit unsichtbaren Feldern, die mit der Webseite übertragen werden und Dinge wie die Sitzungskennung enthalten können. Die Versteckerei von Sitzungskennungen in Cookies oder unsichtbaren Eingabefeldern ist natürlich auch nicht wirklich sicher. Deshalb sollten Sitzungskennungen nur eine kurze Lebensdauer bzw. Sitzungen ein nicht zu langes Timeout haben. Als Benutzer sollte man sich trotzdem von Seiten abmelden, wenn eine entsprechende Funktion dafür vorgesehen ist, denn das Abmelden bewirkt eine Zerstörung der Sitzung und gibt dabei auch den auf dem Server für die Sitzung genutzten Speicher frei.

Ein Framework für die Steuerungsschicht nimmt jedenfalls Requests entgegen und ordnet dem Request - je nachdem ob eine zum Request passende Sitzungskennung existiert - bereits bestehende oder neu angelegte Sitzungsdaten zu. Außerdem werden alle anderen als URL- Parameter oder dem Request z.B. als Formularfelder angehängten Daten ausgewertet und in einer Kollektion verpackt, die dann von einem für den Request neu erzeugtem Controller-Objekt verarbeitet werden (Bei Django sind Requests allerdings nur Methoden zugeordnet).

Für einen Programmierer beginnt hier nun die Arbeit: Er muß die per Request übergebenen Daten prüfen, weiterverarbeiten und gegebenenfalls an die Persistenzschicht übergeben. Anschließend muß er bestimmen, wie der Flow fortgesetzt wird. Das kann entweder die Weiterleitung zu einer anderen Seite oder die Auslieferung einer weiteren Webseite mittels der Präsentationschicht bedeuten. Die neue Webseite wird er mit von der Persistenzschicht gelieferten Daten befüllen und im Fehlerfall muß er der Präsentationsschicht noch Fehlerdaten übergeben, welche diese dann z.B. zum Markieren der falsch eingegebenen Felder und zur Anzeige von Fehlermeldungen nutzt; dafür brauchbare Fehlermeldungen liefert unter Umständen schon die Persistenzschicht.

Abgesehen vom Gestalten der Seiten mittels der Präsentationsschicht und all ihren Templates mit ihren (X)HTML, CSS und Javascript Texten hat der Webanwendungs-Entwickler bei allen Frameworks am meisten Arbeit mit der Erstellung der Controller. Sicherlich ist hier - von der Datenmodellierung mal abgesehen - auch die meiste Planung erforderlich, denn hier wird ja der eigentliche Ablauf der Anwendungsfälle festgelegt. Das ist gerade bei Webanwendungen - wie bereits kurz angesprochen - wegen der Zustandslosigkeit und damit unvorhersehbarem Eintreffen von Requests schwierig. Wenn ein Benutzer z.B. mit den Zurück- und Vor- Buttons des Browsers herumspielt bekommt der Server davon nichts mit und bereits zuvor durchgeführte Requests werden mit evtl. veränderten Daten wiederholt, ohne dass der Benutzer das eigentlich beabsichtigt. So sollte die Anwendung nach einer erfolgreichen Formular-Verarbeitung unbedingt einen Redirekt durchführen, damit ein ungeduldiger Benutzer nicht mit ein paar Klicks das gleiche Formular mehrfach abschickt. Man muß auch berücksichtigen dass in den seltensten Fällen ein Besucher die Sitzung wirklich abschließt. Wenn der Entwickler oder der Anwendungsserver das nicht berücksichtigt, läuft garantiert irgendwann der Speicher über oder die Verbindungen zum Datenbankserver oder einem Webservice sind ausgeschöpft.

Echte Objektorientierung vermeidet Abhängigkeiten

Ein verblüffender Effekt war der Siegeszug von Skriptsprachen wie PHP, Ruby und Python gegenüber den Java-Kolossen der Web-Frameworks. Selbst die Verschlankung durch Dependency Injection und POJOs (Plain old Java Objects) ließ die Web-Anwendungsentwicklung in Java immer noch (auch wegen jetzt aufwändig zu erstellenden XML-Konfigurationsdateien) sehr mühsam erscheinen, während Skriptsprachen- Programmierer immer komplexere interaktive Web 2.0 Anwendungen in immer kürzerer Zeit erstellten.

Einen weiteren Durchbruch der Skriptsprachen schaffte schließlich Ruby on Rails, was zuerst zu einer Hype der bislang eher unbekannten Sprache Ruby und dann zu sehr ähnlichen Frameworks auch für andere Programmiersprachen - auch im Java-Umfeld - führte. So entstand neben einer echten Ruby-Implementierung für die Java-Plattform mit JRuby auch die zu Java-Code vollkompatible Sprache Groovy mit dem Framework Grails, dessen Ausstattung sehr stark an Rails 2 angelehnt ist, aber auf bereits etablierten Java-Frameworks wie den bereits erwähnten Hibernate, Spring und SiteMesh aufbaut. Auch für die noch junge objektfunktionale Sprache Scala existiert bereits ein Rails-ähnliches Framework namens Lift, obwohl Scala als statisch typisierte Sprache nicht zu den „Skriptsprachen“ gerechnet werden kann, denn dynamische Typisierung ist wohl ein Hauptmerkmal von Skriptsprachen. Für die Sprache Python entstand das hinsichtlich Ausstattung und Architektur zu Rails ähnliche Web-Framework Django, das aber unabhängig und parallel zu Rails entstand.

Doch nun zurück zu den Abhängigkeitsproblemen bei statisch typisierten, objektorientierten Programmiersprachen. Wie bereits angesprochen, ist vor allem Vererbung eine sehr starke Bindung bzw. Abhängigkeit. Im Sinne purer Objektorientierung ist aber eine Beziehung wie Klassen-Vererbung zur Kommunikation von Objekten gar nicht notwendig - hier wurde beim Entwurf von C++ Objektorientierung falsch verstanden und leider auch bei Java und C# (aber nicht bei Objective-C) falsch übernommen.

Das liegt daran, das für C/C++/Java Programmierer eine Methode nichts anderes als eine in einer Klasse gekapselte Funktion ist. Das hat aber nichts mit Objektorientierung zu tun. Bei puren objektorientierten Sprachen wie Smalltalk, Simula und deren Abkömmlingen wie Objektive-C, Ruby und Python kann ein Objekt einem beliebigen anderen Objekt eine Message senden. Für den Empfänger ist dabei nur wichtig, ob seine Klasse oder eine Oberklasse Methoden für die Signatur dieser Message bereitstellt. Andernfalls wird eine Standardmessage zur Behandlung unbekannter Message- Signaturen aufgerufen.

Ein weiteres Unverständnis der C/C++/Java- Sprachdesigner hat dazu geführt, dass nicht alle Sprachelemente selbst Objekte sind. Das wäre insbesondere bei Klassen, aber auch Methoden immens wichtig. Wieso darf sich eine Sprache wie Java oder C# eigentlich objektorientiert nennen, wenn noch nicht einmal Klassen und Methoden First-Class-Objekte sind, die zur Laufzeit erzeugt und erweitert und als Funktionsparameter übergeben und als Ergebnis zurück gegeben werden können?

Eben genau das erlauben Skriptsprachen wie Python und Ruby und selbst Javascript, das als prototypisch-objektorientierte Sprache gar keine Klassen kennt, ist somit viel eher objektorientiert als Java oder gar C#. Das mag sich nun alles sehr theoretisch anhören, aber genau solche Mechanismen sind der Schlüssel für Frameworks wie Rails, Grails oder Django. Beispiele hierfür finden sich leicht bei den Persistenzschicht- Komponenten dieser Frameworks. Für Ruby on Rails kann bereits nach folgender Klassendefinition mit einer Vielzahl von Methoden auf eine Datenbank-Tabelle Firma zugegriffen werden:

class Firma < ActiveRecord::Base
end
...
firma= Firma.find_by_name("IBM")
firma.name= "Microsoft"
firma.save()

Dafür kommt jede Datenbank in Frage, für die ein Adapter für die Anwendug konfiguriert wurde. Mit dem gleichen Quelltext kann auf Tabellen in Sqlite3, MySQL, Postgres und andere Datenbanken zugegriffen werden. Die Klassendefinition von Firma führt zum Auslesen der Tabellenstruktur und Erzeugung von Attributen und Zugriffsmethoden für alle Felder der Tabelle und auch für die komplette CRUD- Funktionalität (erzeugen, lesen, aktualisieren und löschen von Datensätzen) stehen Methoden wie save, find, update und delete bereit. Methoden wie find_by_name werden erst bei Aufruf (bzw. Empfang der Message bei objektorientierter Terminologie) generiert. Eine Oberklasse wie ActiveRecord::Base braucht nur die Methode für unbekannte Message-Signaturen method_missing zu implementieren und aus der Message-Signatur und der ihr bekannten Tabellenstruktur feststellen, welche SQL-Abfrage für diese Message aufzubauen ist.

Wären Methoden und Klassen nicht selbst zur Laufzeit erzeugbare Objekte, wären so komfortable ORMs wie ActiveRecord (Rails), GORM (Grails) oder Djangos ORM gar nicht erst möglich. Der Aufrufer einer Objektinstanz wie firma im Beispiel oben braucht übrigens die ActiveRecord- bzw. Modell-Klassen gar nicht zu kennen. Er verwendet einfach eine Signatur wie find_by_name und das Zielobjekt weiß schon, was damit zu tun ist. Seit Ruby und Python wird diese auf Message-Mapping beruhende Aufrufmethode übrigens auch Duck-Typing genannt, wirklich objektorientierte Sprachen wie Smalltalk funktionierten aber auch vorher schon so. Der Begriff Duck-Typing geht auf eine Zeile von einem Gedicht von James W. Riley zurück:

„When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.“

Bezogen auf Objektorientierung hebt Duck-Typing also die Abhängigkeit von konkreten Klassen oder Interfaces auf, was im Umfeld von Python und Ruby häufig schon wie in Smalltalk genutzt wird. Natürlich entfällt dabei für einen Compiler die Möglichkeit, die Typisierung zu prüfen. Da in der heutigen Zeit Compiler-Tests aber ohnehin nicht ausreichen und Unit-Tests gefordert sind, braucht man sich die Abhängigkeits-Probleme, die durch statische Typisierung entstehen, auch gar nicht erst anzutun. Bei ausreichender Unit-Test-Abdeckung fallen Typisierungs-Probleme auch ohne Compiler-Unterstützung auf. Ein Argument für statische Typisierung ist natürlich die Performance. Duck-Typing ist durch Suche in Message-Maps natürlich langsamer als schon beim Übersetzen bestimmbare Funktions-Einsprungpunkte. Auch erleichtert statische Typisierung die Realisierung von Auto-Vervollständigungs-Funktionen in modernen Entwicklungsumgebungen wie VisualStudio 2010 und Jetbrains's IntelliJ IDEA. Aber gerade Jetbrains zeigt mit RubyMine und PyCharm, dass man auch für dynamische Sprachen wie Ruby und Python sehr komfortable IDEs bauen kann. Gerüstbau

Bei Frameworks der „Rails-Klasse“ wie Django oder Grails hört die „Magie“ mit der automatischen Erzeugung von Klassen für Business-Objekte noch lange nicht auf, mit Scaffolding wird bei Rails oder Grails die Generierung fertiger Administrations-Benutzerschnittstellen für alle als Klasse definierten Tabellen der Datenbank bezeichnet. Bei Django wird dieser Begriff vermieden, weil man meint, nicht nur ein Baugerüst (Scaffold), sondern sogar ein richtiges Haus zu liefern. Tatsächlich sind die Administations- Benutzerschnittstellen von Django wirklich so brauchbar, dass adminstrative Benutzer sie sogar in fertigen Anwendungen noch weiter verwenden können. Ich schreibe beispielsweise diesen Text hier mit einem Editor in einem Django-Admin-Interface und sehe keinen Grund dafür, hier jemals etwas Besseres programmieren zu müssen.

Das gesamte Content-Management dieses Blogs, das ich bisher nur ansatzweise nutze, geschieht mit durch Zinnia und Wymeditor ein bißchen aufgebohrte Django-Admin-Interfaces und das Ganze geht in vielerlei Hinsicht bequemer und einfacher als etwa mit Microsoft Word.

Nun ist Django speziell für Content-Management-Anwendungen wie auch Wikis, Blogs und Webforen natürlich im Vorteil, weil dieses Framework ursprünglich aus einem Content-Management-System für eine Zeitung hervor ging. Ich habe auch schon genug mit Rails und Grails gearbeitet, um zu wissen, das ich hier mit Django und Mezzanine am schnellsten zum Ziel komme.

Testdriven Development und Coverage

Ich habe oben bereits angedeutet, daß gerade wegen der Komplexität von Webanwendungen die Syntax- und Typ-Prüfung durch den Compiler einer statisch typisierten Programmiersprache keinesfalls vom Schreiben von Testcode entbindet. Das gilt umso mehr für agile Methoden des Software- Engineerings und erst recht für dynamisch typisierte Sprachen. Beim Testdriven Development realisiert man nach Definition der Schnittstelle für eine Komponente oder auch nur eine Funktion zuerst Tests, welche die korrekte Funktion dieser Komponente oder Funktion verifizieren. Erst dann beginnt man mit der Entwicklung der Komponente selbst und führt bei Fertigstellung oder jeder weiteren Änderung des Komponenten- Quelltexts diese Unit-Tests durch. Solange man mit der Entwicklung der Komponente noch nicht fertig ist, werden Tests fehlschlagen und man hat jederzeit ein Feedback, wie weit man mit der Implementierung bereits gekommen ist (Test Coverage, Testabdeckung).

Weil Tests so wichtig sind, sind in aktuellen Web-Frameworks wie Rails, Grails und Lift bereits Test-Frameworks wie RSpec oder JUnit integriert. Auch wird für jede Modell- oder Controller-Klasse, die man erzeugt, automatisch eine zugehörige Testklasse für die Unit-Tests generiert. Das Schreiben der Tests kann dem Entwickler natürlich kein Framework abnehmen, aber die Administrations-Skripte, die auch Kommandos vom Erzeugen solcher Klassen bis hin zur Ausbreitung der Webanwendung auf einen Server enthalten, nutzen auch die Test-Frameworks zur Durchführung automatisierter Tests. Moderne IDEs - wie die bereits erwähnten von Jetbrains - führen alle Tests auf einen Klick durch und zeigen auch gleich die Testergebnisse an. Build-Tools wie das in der Java-Welt und auch im Scala+Lift Umfeld unverzichtbare Maven berücksichtigen auch die Durchführung kompletter Testläufe. Noch weiter gehen von Teams genutzte Continuous Integration Server wie Hudson. Diese führen bei jedem Einchecken neuen Quellcodes in das Quellcode-Repository (Versionsverwaltung) oder periodisch den gesamten Prozess von Auschecken, Übersetzen, Unit-Test, Integrationstest, Testabdeckung bestimmen bis hin zur Ausbreitung des Produkts auf einen Testserver automatisch durch.

Ausbreitung von Quellcode und Webanwendungen

Besonders im OpenSource-Umfeld werden Frameworks, Komponenten und fertige Anwendungen ja auch per Quellcode ausgebreitet. Betriebssysteme wie Linux und BSD besitzen bereits Paketverwaltungssysteme (BSD ports, Debian deb, Red Hat/Fedora rpm), die sowohl bereits übersetzte Programme und Bibliotheken als auch deren Quellcode ausbreiten können. Diese Pakete sind an eine bestimmte Release des jeweiligen Betriebssystems gebunden, um zu garantieren, daß die ausgebreiteten Versionen aller Pakete zusammen funktionieren. Von regelmäßigen Sicherheitsupdates abgesehen sind diese Versionen aber nicht die aktuellen.

Tagesaktueller Quellcode kann jederzeit über Repository-Server wie Subversion, CVS oder git herunter geladen werden, ist aber nicht mehr unbedingt kompatibel zu allen Paketen der installierten Betriebssystem- Release. Wie zu Beginn angesprochen besteht eine komplette Webanwendung aus einem Sammelsurium von Frameworks und Bibliotheken, von denen die meisten von anderen abhängen. Da kann man sich leicht vorstellen, daß die Ausbreitung einer Webanwendung auf einen Produktions-Server zu einem Alptraum wird.

Nach einiger (schlechter) Erfahrung mit Paketverwaltungssystemen begann man deshalb mit der Entwicklung intelligenterer Paketverwaltungsprogramme und verteilten Versionsverwaltungssystemen wie git, welche Abhängigkeiten erkennen und zusammen funktionierende Pakete zu einem Distributionspaket zusammensetzen können. Diese zuerst bei der Weiterentwicklung von Betriebssystemen wie Linux eingesetzten Werkzeuge werden zunehmend auch im Zusammenhang mit Package Indexern eingesetzt, die mit zur Popularität der Skriptsprachen beigetragen haben.

Für alle relevanten Skriptsprachen existieren eigene Quellcode- Repositories mit OpenSource-Paketen:

Quellcode Bibliotheken für Skripte

Perl CPAN http://www.cpan.org PHP PEAR http://pear.php.net Python PyPI http://pypi.python.org Ruby Gem http://rubygems.org

Zu all diesen Sprachen gibt es mittlerweile auch Tools, welche Framework-Pakete nicht nur herunter laden und in die lokale Entwicklungsumgebung installieren, sondern auch alle anderen Bibliotheken und Frameworks installieren, von denen jenes Framework abhängt. Das geht - leider noch nicht perfek t- sogar für nativen Code, also in C/C++ erstellte Bibliotheken, die eine Skript-Komponente nutzt. Meist ist man deshalb besser beraten, wenn man den C/C++ Code, den ein Skripting-Framework braucht, mit der Paketverwaltung des Betriebssystems (also z.B. aptitude bei Debian/Ubuntu oder yum bei Red Hat/Fedora) installiert, bevor man die Paketverwaltung der jeweiligen Skriptsprache benutzt.

Nicht nur wenn man auf einem Server nun auch noch unterschiedliche Versionen der Skriptsprache und ihrer Frameworks braucht, sollte man lokale Installationen in einem Benutzerverzeichnis und sogenannte virtual environments einrichten. By Python geht das beispielsweise mit der Befehlszeile virtualenv. Ein damit hergestelltes virtual environment ist nur noch von einer der auf dem Zielsystem installierten Python-Versionen abhängig und enthält praktischerweise auch gleich das Paketverwaltungsprogramm easy_install, mit dem man Pakete direkt aus dem PyPI -Repository holen kann. Somit erhält man in einem lokalen Verzeichnis ein vollständiges Set aller für eine Webanwendung zusammen passenden Pakete (python eggs). Wenn man das wiederum als python egg zusammenpackt, wird eine Ausbreitung der Anwendung auf einen anderen (Produktions-)Server recht einfach- nur der Inhalt der Datenbank bzw. deren Migration macht unter Umständen noch einige Arbeit.

Bei Ruby ist die Ausbreitung leider meist nicht ganz so problemlos, bei Grails profitiert man davon, dass das Grails-Administrationstool bereits die gesamte Anwendung in ein WAR-Paket packen kann, welches Tomcat als Anwendungsserver direkt verwenden kann. Zur Ausbreitung einer Grails Anwendung muß man sich also auf dem Produktionsserver neben der Datenbank also nur um eine funktionierende Installation von Java und Tomcat kümmern, den Rest erledigt dann Tomcat.

start/software/moderne_webauftritte.txt · Zuletzt geändert: 2018/09/21 16:45 (Externe Bearbeitung)