Ein eigenes Forum programmieren, Zustandsmodell einer Webanwendung

Das Forum als Webanwendung und eine Betrachtung zur Datenhaltung hierarchischer Strukturen

Ist es kompliziert, ein Forum zu programmieren? Klares nein! Ein Forum ist eine Webanwendung wie jede Andere und bevor es losgeht, schauen wir doch erst einmal, welche Zustände ein Forum annehmen kann.

Als Nächstes betrachten wir die Möglichkeiten, die es zur Datenhaltung gibt und setzen uns mit hierarchischen Strukturen auseinander. Dann überlegen wir uns, wie die einzelnen Zustände auf HTTP-Request-Parameter abgebildet werden können, ja und dann kanns ja losgehen mit der Programmierung.

Zustände eines Forum als Webanwendung

Das Forum im Browser kennt genau zwei Grundzustände:

Browse
Die Übersicht der Nachrichten bzw. Threads wird im Browser dargestellt. Zusätzlich hierzu enthält das Dokument in der Seite ein Formular womit ein neuer Thread eröffnet werden kann.
Eine einzelne Nachricht im Browser zeigen
Es wird eine einzelne Nachricht im Browser ausgegeben, zusätzlich wird der gesamte dazugehörige Thread dazu gezeigt und unter der Nachricht gibt es ein Formular, was das Antworten auf die ausgewählte Nachricht ermöglicht.

Zwischen beiden Zuständen gibt es einen Zustands-Übergang der eine spezielle Beachtung verdient: Das Senden einer Nachricht. Dies ist von jedem der oben gezeigten Zuständen aus möglich, konkret handelt es sich dabei entweder um eine Nachricht für einen neuen Thread oder um eine Nachricht als Antwort auf eine bereits vorhandene Nachricht.

Das Senden einer neuen Nachricht als Zustands-Übergang (State Transit) endet wiederum in einem der oben aufgeführten Zustände, das ist eine Frage der Festlegung/Vereinbarung. Benutzerfreundlich ist es, nach dem Senden einer neuen Nachricht die Nachricht selbst zu zeigen.

HTTP-Request und Parameter

Gewöhnlich werden Zustände einer Webanwendung auf Parameter abgebildet, daneben spielt die Request-Method eine Rolle. Für die beiden oben genannten Zustände empfiehlt sich die Request-Method GET und zur Unterscheidung genügt ein Parameter, d.h.: Mit einem GET-Parameter im Request wird exakt eine Nachricht dargestellt. Ohne Parameter haben wir den Zustand browse, hier wird die Übersicht gezeigt.

Für den Zustands-Übergang hingegen, also das Senden einer Nachricht, empfiehlt sich die Request-Method POST. Hierbei sind mehrere Parameter im Spiel, welche das sind bestimmt unser Datenmodell, das betrifft die Benutzereingaben. Für den Zustand (2) und den Übergang (POST) jedoch, legen wir jeweils nur einen Schlüsselparameter fest, was die spätere Kontrollstruktur vereinfacht.

Datenmodell einer Nachricht in Hinblick auf eine Hierarchie

Viel Geschriebenes gibt es über Nested Sets in Datenbanken, hierbei werden hierarchische Strukturen über die Mengenlehre an eine Tabelle mit einem bestimmten Aufbau gebunden. Das bedeutet, dass die Bindung an eine Datenbank nicht lose ist, für den Forumsbetrieb ist eine Solche gar notwendig.

Betrachten wir einzelne Nachrichten jedoch als Objekte, ergeben sich andere Möglichkeiten der Datenhaltung, die den Einsatz eines relationalen Datenbank-Management-Systems nicht zwingend notwendig machen. Es sei an dieser Stelle darauf hingewiesen, dass der Zugriff auf eine Datei oftmals performanter ist, als die Erstellung einer Datenbankverbindung.

Mesg.Nr. Thread.Nr.
381 381
382 381
383 381

Betrachte nebenstehende Tabelle, die Nachricht mit der Nummer 381 kennzeichnet eine Nachricht, welche einen neuen Thread eröffnet. Diese Thread-Nummer wird an alle weiteren, zum Thread gehörigen Nachrichten durchgereicht, jede Nachricht bekommt die Thread.Nr. als ein zur Nachricht gehöriges Attribut zugewiesen.

Mit diesem Modell ist es einerseits möglich, zu jeder Nachricht den dazugehörigen Thread zu ermitteln und feststellen zu können, mit welcher Nachricht der Thread eröffnet wurde.

Hinzu kommt nun ein weiteres Attribut, was die Hierarchie betrifft: Die Mesg.Nr. des Parent (in der Tabelle nicht dargestellt).

Entsprechend den Erfordernissen erweitern wir die Liste unserer Attribute beispielsweise um subject, name, messagebody und Zeitstempel. Sebstverständlich sind derartige Strukturen erweiterbar und keineswegs als feste Vorgabe zu verstehen.

Jede Nachricht im Forum bekommt eine fortlaufende Nummer, diese werden mit einem Perl-Modul erzeugt, dabei ist dafür gesorgt, dass der Prozess zur Erzeugung einer eindeutigen Nummer atomar ist. Eine Nummer für eine neue Nachricht wird erst dann vergeben, wenn sie fehlerfrei am Server angekommen ist.

Die Reihenfolge der aus dem Pool stammenden fortlaufenden Nummern müssen innerhalb eines Threads nicht lückenlos aufeinanderfolgend sein, denn die Nummer selbst bestimmt einzig und allein das die Nummern vergebende Modul.

Schließlich wäre noch der Speicherort zu klären, dafür nutze ich einen Data Abstraction Layer (DAL) womit die Nachrichten als Datenobjekte persistent gemacht sind. Jedes zu speichernde Objekt hat eine Struktur wie untenstehend gezeigt:

$VAR1 = {
          'parent'     => '',
          'nickname'   => 'Der kleine Nick',
          'subject'    => 'Ein interessanter Beitrag',
          'threadroot' => '527',
          'datetime'   => '02.12.2014 21:15:13',
          'body'       => 'Sehr geehrte Damen und Herren, ...',
          'thread'     => '527',
          'timestamp'  => '1417551313',
          'isroot'     => '1'
        };

Die Daten im Attribut body können selbstverständlich auch mehrzeilig sein. Lassen Sie sich nicht dadurch irritieren, dass die Zeitangaben doppelt sind, das braucht zwar geringfügig mehr Platz im Massenspeicher, beschleunigt jedoch die Ausgabe, denn zum Einen wird nach timestamp sortiert und zum Anderen liegt in datetime die Zeitangabe fix und fertig formatiert bereit. Beim Speichern einer neuen Nachricht jedoch, fällt dieser kleine Mehraufwand nicht weiter ins Gewicht.

Das gezeigte Objekt hat im Attribut parent keinen Eintrag. Dafür ist das Attribut isroot auf den Wert 1 gesetzt, hier liegt also eine Nachricht vor, mit welcher ein neuer Thread eröffnet wurde. Im Massenspeicher hat das Objekt selbst eine Objekt-ID als fortlaufenden numerischen Eintrag.

Baumdarstellung der Threads

Im Ergebnis möchten wir eine mit den HTML-Tags ul/li verschachtelte Listendarstellung. Da jedes Nachrichtenobjekt seinen Parent kennt, können wir dafür eine rekursiv arbeitende Funktion einsetzen, vorher jedoch müssen einmal alle Objekte durchlaufen werden, um die Parent-Relation umzudrehen zu einer Child(s)-Relation, damit kennt jede Nachricht ihre etwaigen Childs. Dieser Vorgang und die sich daran anschließende Rekursion wird komplett im Hauptspeicher abgewickelt.

Parameter Kontrollstruktur, Schlüsselparameter

Entsprechend dem Zustandsmodell und dessen Abbildung auf Parameter im Request ergibt sich folgende Kontrollstruktur:

    # Interface Method control im MVC
    # diese Methode wird aufgerufen, wenn Parameter im Request sind
    sub control{
        my $self = shift;
        if( my $mesg_id = $self->param('show') ){
            # Ausgabe der Nachricht mit der $mesg_id
            # Darstellung Thread als Baum
            # Ausgabe des Antwortformulars
            # Das Antwortformular beeinhaltet die mesg_id vom Parent
        }
        elsif( $self->param('post') ){
            # Erfassung aller zur Nachricht gehörigen Parameter wie
            # subject, nickname, mesg_body, ggf. parent usw.
            # Prüfen der Eingaben
            # Wenn Eingaben ok, Vergabe einer neuen Nummer für die Nachricht
                # Umleitung auf die neue Nachricht
                # Darstellung Thread als Baum
                # Ausgabe eines Antwortformulars
            # Bei fehlerhaften Eingaben oder Eingaben fehlen
                # der Thread und das Formular wird erneut ausgegeben
        }
        else{
            # Fehlerseite: Unbekannter Parameter
        }
    }

Schlüsselparameter sind also lediglich die Parameter show und post. Diese Parameter legt der Entwickler fest, sie können natürlich auch anders lauten. Wichtig ist, dass sie eineindeutig einem der Anwendung entsprechenden Zustand zugeordnet sind.

Bindung der Parameter an das Zustandsmodell

Eine Kontrollstruktur übernimmt die Kontrolle über die Parameter (Schlüssel), die der Entwickler festlegt. Ein Parameter-Kontrollstruktur dient in erster Linie nicht der Kontrolle der Benutzereingaben sondern dazu, die verschiedenen Zustände einer Anwendung auf Parameter abzubilden.

Zum Beispiel kennt ein Forum 2 Zustände:

Es gibt einen Zustand-Übergang: Das Senden einer Nachricht, entweder als Erföffnung für einen neuen Thread oder als Antwort auf eine bereits vorhandende Nachricht. Ein einziger Parameter genügt, diesen Übergang auszulösen, wohlgemerkt: Der Parameter, nicht dessen Wert.

Nach dem Zustandsübergang ergibt sich wieder einer der beiden Zustände (1) oder (2), je nachdem, wie der Programmierer das vorgesehen hat, wird nach dem Senden entweder die Übersicht gezeigt oder der betreffende Thread.

Das ist das Grundgerüst: Die Abbildung der Zustände einer Anwendung über Parameter bzw. deren Namen. Hierzu ist eine Kontrollstruktur das Mittel zum Zweck, ein Switch ist dafür völlig ungeeignet.

Nun kommen die Werte ins Spiel, die müssen natürlich auch kontrolliert werden. Bspw. ob eine eingegebene mesg-ID valide ist (ein Benutzer kann den URL manipulieren), oder ob der Benutzer alle Pflichtfelder ausgefüllt hat. Wir müssen im Fall einer fehlerhaften Eingabe nicht das ganze Zustands-Modell über den Haufen werfen, es genügt die Ausgabe einer Fehlermeldung in einem der beiden Zustände (1) oder (2). Das sind sozusagen die stabilen Zustände: Tritt beim Zustandsübergang (Senden einer Nachricht) ein Fehler auf, fällt die Anwendung zurück in einen der beiden Zustände, d.h., sie ist und bleibt stabil.

Letzeres kannst Du nur mit einer Kontrollstruktur (if, elsif, else) sicherstellen. Natürlich kannst Du einen Switch über die Benutzereingaben legen, das bietet sich an, wenn die Eingaben vorbelegte Werte haben, z.B. Radiobuttons, Selectfelder oder Checkboxen.

Und nochwas: Einem Benutzer darf es nicht möglich sein, das Zustandsmodell zu ändern bzw. einen instabilen Zustand der Anwendung zu erzeugen. Auch dann nicht, wenn er zusätzliche oder unbekannte Parameter ins Spiel bringt. Derartige Manipulationsversuche landen unweigerlich im else-Zweig.

Es ist eine Frage der Bindung. Es ist einfacher, das Zustandsmodell sowie Zustandsübergänge an Parameter zu binden, als an Werte.

Die Abstraktion erfolgt bereits bei der Bindung und die ist idealerweise von Sprachdateien unabhängig. Wenn die Bindung über von Sprache unabhängige Parameter(namen) erfolgt, ist das praktisch der Idealfall.

Finessen

Mit den Einsatz von Ajax zum Einstellen einer neuen Nachricht wird die Benutzerführung etwas freundlicher. Deswegen, weil zum Ausgeben einer etwaigen Fehlermeldung bei fehlerhaften Eingaben keine neue Seite erforderlich ist, sondern allenfalls ein kleines mit JavaScript erzeugtes Popup.

Um das zu implementieren, genügt es, in der Ajax-Response entweder den Text für die Fehlermeldung oder die Nummer für die neu erstellte Nachricht zu senden. Somit muss in der Callback-Funktion lediglich geprüft werden, ob die Response eine ganze Zahl ist. Wenn ja, leitet der Browser selbstständig auf diese Seite um: URL?show=123 als Beispiel, wenn 123 die Nummer für die eben erstellte Nachricht ist. Andernfalls wird die Fehlermeldung ausgegeben:

# Callbackfunktion für den AJAX POST einer Nachricht
    function putmesg_cb(response){
        throbber(false);
        if( response.match(/^\d+$/) ){
            window.location.href = '%url%?show='+response;
        }
        else{
            alert(response);
        }
    }

Ein weiteres Problem ist die Formatierung von Nachrichten, insbesondere dann, wenn CODE originalgetreu wiedergegeben werden soll. Das heißt, dass zum Einen Einrückungen erhalten bleiben und zum anderen vom Benutzer vorgenommene Zeilenumbrüche genauso wiedergegeben werden sollten. Überlange Zeilen jedoch, sollen automatisch umgebrochen werden, so dass der Betrachter nicht nach der Seite scrollen muss. Lösung: Zero Width Space + No Break Space.

Forum testen

Nickname ausdenken, hier einloggen und ab gehts.


Anbieter: nmq​rstx-18­@yahoo.de, die Seite verwendet funktionsbedingt einen Session-Cookie und ist Bestandteil meines nach modernen Aspekten in Perl entwickelten Frameworks.