MVC-Architektur in Tntnet (Variante I.)

Standard

MVC und Tntnet

Tntnet macht dem Entwickler keine Vorschrift, wie er sein Projekt zu organisieren hat oder welche Paradigmen zur Anwendung kommen sollen. Tntnet konzentriert sich auf die Basisfunktionalität. Wenn eine höhere Abstraktion gewünscht ist, muss der Entwickler diese selber implementieren. C++ gibt es lange bevor es das heute sehr popoläre model view controller(MVC) pattern gab.  C++ wird es höchstwahrscheinlich noch geben, wenn das MVC-Pattern durch andere Konzepte abgelöst oder weiterentwickelt wurde. Das ist der Grund, warum das MVC auch nicht fester Bestandteil von Tntnet ist. Das heißt aber nicht, dass MVC nicht mit Tntnet gehen würde. Hier zeige ich eine denkbare Implementierung von MVC in Tntent.

Der Controller

Die Controller-Klasse wird von tnt::Component abgeleitet, und muss eine Funktion „operator()“ implementieren:

class MyCopmonentController : public tnt::Component
{
    public:
        unsigned operator() (
            tnt::HttpRequest& request,
            tnt::HttpReply& reply,
            tnt::QueryParams& qparam
        );
};

Da die Klasse kein Interface hat über die sie angesprochen wird, entfällt die Header-Datei. Es ist nur eine *.cpp erforderlich. Mit „qparam.arg<TYPE>(KEYWORD)“ kann ein Argument ausgelesen, das dem HTML-Requeset mitgegeben wurde. Als Beispiel:

// URL arguments
std::string arg_login_name =
    qparam.arg<std::string>("arg_login_name");

„TYPE“ ist der variable Type, den man zurück bekommen möchte. „KEYWORD“ ist der Bezeichner, mit dem der Wert übergeben wird. Möchte man eine Liste von Werten zurückbekommen, (z.B. aus Listen in HTML-Formularen, in denen eine Mehrfachauswahl erlaubt ist), muss eine andere Funktion genutzt werden. Diese heißt „args“ statt „arg“ und gibt einen Vector von Typen zurück, den man angibt:

std::vector<std::string>  args_userroles =
    qparam.args<std::string>("args_userroles");

Um nicht mit Argumenten und shared Variablen durcheinander zu kommen, kann es hilfreich sein, sich auf die Konvention zu einigen, dass Argumente mit den Präfix „arg_“ beginnen. Die Namen in den HTML-Formularen (bzw. der View in den *.ecpp-Datein) müssen natürlich der gleichen Konvention folgen.

<p>
 <label for="login_name">Login*:</label>
 <br>
 <input 
    class="full-size" 
    name="arg_login_name" 
    type="text" 
    value="<$ accountData.getLogin_name() $>" 
    maxlength="80"> 
</p>

Empfohlene Argumenten Typen

Hier eine tabellarische Übersicht, welcher C++-Cast für welchen HTML-Typ sinnvoll ist.

HTML-Type C++-Type
button bool
input/text string
input/password string
input/number int, long, short…
input/checkbox bool
select/multiple vector

Gemeinsam genutzte Variablen

Um Werte an den View zu übergeben, nutzt man shared-Opjekte und -Variablen. Diese müssen mit einem Macro registriert und initialisiert werden:

 
// shared variables
TNT_REQUEST_SHARED_VAR( UserSession, s_userSession, ());

Der erste Parameter ist der Typ; der zweite Name und der Dritte ist der aufzurufende Constructor. Wenn dieser einen Parameter braucht, kann dieser hier angegeben werden. Es empfehlt sich für  die Übersicht die Namenskonvention zu verwenden, die shared Variablen ein „s_“ als Präfix voranstellen.

Es gibt für die shared Opjekte verschiedene Gültigkeitsbereiche bzw. Lebensdauer. So werden über „TNT_SESSION_GLOBAL_VAR“ die Objekte die gesamte Session überdauern. Es gibt noch „TNT_REQUEST_SHARED_VAR“. Hier haben die Objekte nur eine Lebensdauer für ein Request. Es ist ratsam mit „TNT_SESSION_GLOBAL_VAR“ sehr sparsam umzugehen und wenn möglich, nur mit „TNT_REQUEST_SHARED_VAR zu“ arbeiten. Andernfalls kann es zu ungewollten Effekten kommen, wenn Objekte von einer vorigen Request-Prozedur noch mit einen altem Wert belegt sind.

Damit der Controller tatsächlich beim Routing berücksichtigt wird, muss die Klasse der Component-Factory bekannt gemacht werden:

static tnt::ComponentFactoryImpl<MyCompController>
     factory("MyCompController");

Model

Im Controller (und später im View) wird eine Klasse UserSession als shared variables verwendet. Es ist im Prinzip möglich jeden Typ, also auch primitive Typen wie int zu benutzen. Davon ist jedoch abzuraten, weil es keine Namensräume gibt, die die einzelnen Komponenten trennen können, wenn sie von der Tntnet-Component-Factory verwaltet werden. Die Wahrscheinlichkeit bei sehr großen Projekten aus Versehen einen Variablennamen zu verwenden, der schon an anderer Stelle verwendet wird, ist sehr groß.

Kapselt aber jede Komponente (logische Einheit einer Website) ihre Session-Daten in eine eigene Klasse (bzw. Model) – wie hier – können verschiedene Komponenten, die selben Variablennamen haben, verwenden. Durch die Verwendung unterschiedlicher Typen (Klassen) werden sie von Tntnet als unterschiedliche Variablen verwendet. Zur Veranschaulichung:

Ist in dem Controller der Komponente A folgender Code zu finden

TNT_SESSION_SHARED_VAR(CompA::SessionShareds, sessionInfo, ());

und in dem Controller der Komponente B folgender

TNT_SESSION_SHARED_VAR(CompB::SessionShareds, sessionInfo, ());

so benutzen beide Komponenten zwar den gleichen Instanznamen aber unterschiedliche Klassen-Typen, so dass sie sich nicht gegenseitig ihre Werte überschreiben.

View

Der View wird in Tntnet mit einer erweiterten HTML-Auszeichnungssprache namens epcc erstellt und vom Precompiler ecppc in C++-Code umgewandelt. Ich werde bestimmt noch in einem späteren Artikel detaillierter auf diese Aufzeichnungssprache eingehen. Bis dahin sei vorerst noch auf die offizielle eng. Doku verwiesen: http://www.tntnet.org/man7/ecpp.7.html An diese Stelle gehe ich erst mal nur soweit darauf, ein wie es das MVC-Thema berührt.

Damit die shared Variablen des Controllers auch dem View zur Verfügung stehen, müssen diese der View-Umgebung bekannt gemacht werden. Das geschieht auf die folgende Weise:

<%session
    scope="shared"
    include="models/UserSession.h">
        UserSession s_userSession;
        std::vector<std::string> s_allRolls;
</%session>

<%request
    scope="shared">
            std::vector<std::string> sh_allRolls;
</%request>

Mit dem scope-Wert „shared“ wird angezeigt, dass es sich um shared Variablen handelt. Mit „include“ können benötigte Header-Dateien eingebunden werden. In diesem Fall includieren wir die Klasse „UserSession“. Diese wird gebraucht, damit der Type „UserSession“ dem Compiler bekannt ist. Zwischen den Tags werden die eigentlichen Variablen aufgelistet bzw. bekannt gemacht. In dem Beispiel sieht man auch, dass hier bei der Lebensdauer der shared Variablen differenziert wird. Im Tag „session“ kommen alle Werte, die zuvor mit dem Makro „TNT_SESSION_GLOBAL_VAR“ deklariert wurden. In „request“ kommen alle Variablen, die in dem Controller „TNT_REQUEST_SHARED_VAR“ initialisiert wurden.

Routing

Damit der View und der Controller tatsächlich gemeinsam eine Anfrage bearbeiten, müssen sie noch mit einer gemeinsamen Route verknüpft werden.

    app.mapUrl( "^/(.*)$", "$1Controller" );
    app.mapUrl( "^/(.*)$", "$1View" );

Diese Regel sagt aus, dass jede URL einmal um „Controller“ und einmal um „View“ ergänzt wird, und damit zuerst der Conntroller und dann der View aufgerufen wird. Lautet nun unser Controller z.B. „MyCompController“ und der Viel „MyCompView“, so wird die neue Kompnent über die URL „MyComp“ aufgerufen.

Sicherheitsaspekt

Der View sollte keinerlei Logik enthalten. Sollte einmal wegen eines Fehlers oder einer Fehlkonfiguration der Controller nicht aufgerufen werden, liefert der View trotzdem seinen Inhalt ab. Wenn die Zugriffsrechte im Controller realisiert sind, greifen diese nicht mehr und der View könnte Daten preisgeben, die nicht für den Besucher gedacht sind. Deshalb sollte IMMER der Controller für die Befüllung mit Inhalt und Daten zuständig sein.

Troubleshooting

Der Controller beim MVC wird (scheinbar) nicht aufgerufen

Problem: Man hat eine Komponente mit dem MVC-Konzept erstellt, aber der Controller scheint nicht aufgerufen zu werden.

  • Überprüfen, ob das wirklich zutrifft, indem z.B. eine Testausgabe mit std::cout auf die Standartausgabe ausgegeben wird
  • Kontrollieren, ob die Route stimmt, die über app.mapUrl() oder über das Konfigurations-File von Tntnet gesetzt wurde
  • Prüfen, ob der Name des Controller korrekt in der Component-Factory angegeben wurde: static tnt::ComponentFactoryImpl factory(„MyController“);
  • Kontrollieren, ob die Komponente compiliert und gelinkt wurde, indem das Makefile überprüft wird
  • Prüfen, ob ggf. form-Tag im View  die richtige Componente aufruft

Nachwort

Es werden sich schon einige Leser Gedanken gemacht haben, warum im Titel „(Variante I.)“ steht. Tntnet gibt den Entwickler die Freiheit, die Dinge so zu tun, wie man sie für richtig hält. Was „richtig“ ist, da gehen die Meinungen meist auseinander. Deshalb gibt es auch noch andere Lösungen. Ich werde noch in ein weiteres MVC-Pattern für Tntnet vorstellen. Auch das Thema „Verzeichnisstruktur in Tntnet-Projekten“ werde ich noch ein mal separat behandeln.


Creative Commons Lizenzvertrag