Was nicht passt, wird passend gemacht - Teil 1

Model View Controller in Webanwendungen
Kommentare

Model View Controller ist eines der bekanntesten Entwurfsmuster. Allerdings ist es mit über dreißig Jahren deutlich älter als das World Wide Web. Wen wundert es da, wenn der Einsatz von MVC im Web das eine oder andere Fragezeichen aufwirft? Wir sehen einmal ganz genau hin.

In der letzten Ausgabe haben wir damit begonnen, einmal „hinter die Kulissen“ von MVC zu schauen. Der zentrale Nutzen von MVC ist „Separate Presentation“, also die Abtrennung der Präsentation von den anderen Belangen wie Logik oder gar Datenhaltung. Die Grundidee dabei ist es, Daten in einem so genannten Model zentral zu repräsentieren, damit verschiedene Ansichten (Views) auf diese Daten synchronisiert werden können. Ein klassisches Beispiel dafür sind Multiple Document-Interface-Applikationen. Sie ersparen es dem Benutzer, wiederholt zwischen zwei Fenstern hin und her zu blättern, denn er kann mehrere Fenster gleichzeitig öffnen. Damit das sinnvoll funktioniert, müssen die unterschiedlichen Views (GUI-Elemente) „automatisch“ jeweils dann aktualisiert werden, wenn sich das Model geändert hat. Hierbei kommt eine Subject-Observer-Beziehung zum Einsatz, die wir in der letzten Ausgabe bereits ausführlich diskutiert haben.

In dieser Ausgabe wollen wir uns mit MVC im Web beschäftigen. Bevor wir uns näher ansehen, wie das überhaupt funktionieren kann, sehen wir uns noch eine „echte“ MVC-Implementierung in PHP an. Wir stellen uns vor, dass unser Model einen Zähler repräsentiert und es zwei Buttons – also Views – gibt, die den Zähler inkrementieren. Jede View zeigt zudem den aktuellen Zählerstand an. Klickt man also auf einen der beiden Buttons, muss der Zähler im Model inkrementiert werden und sich daraufhin beide Views mit dem Model synchroniseren. Als gute Entwickler definieren wir im ersten Schritt die notwendigen Schnittstellen (Listing 1). In Listing 2 implementieren wir zuerst das Model.

interface Model
{
  public function attach(View $view);
}

interface View
{
  public function attach(Controller $controller);
  public function update(Model $model);
}

interface Controller
{
  public function update(View $view);
}
class MvcModel implements Model
{
  private $views = array();
  private $counter = 0;

  public function setCounter($counter)
  {
    $this->counter = $counter;
    $this->notify();
  }

  public function getCounter()
  {
    return $this->counter;
  }

  public function attach(View $view)
  {
    $this->views[] = $view;
  }

  private function notify()
  {
    foreach ($this->views as $view) {
      $view->update($this);
    }
  }
}

Das zentrale Feature des Models ist, dass es Daten hält, in unserem Fall den Zähler. Es gibt einen Getter für den Zähler und einen Setter, der zugleich die private Methode notify() aufruft, um allen angehängten Views Bescheid zu sagen, dass sich das Model geändert hat – beziehungsweise haben könnte.

Um die Views als Beobachter anzuhängen, braucht das Model natürlich eine Methode attach(). Die private Methode notify() schließlich sorgt dafür, dass bei einer Zustandsänderung des Models alle angehängten Views benachrichtigt werden. Dabei wird mit $this jeweils eine Referenz auf das Model an die Views übergeben, damit diese den Zustand des Models abrufen können. Nun kommt die View an die Reihe (Listing 3).

class MvcView implements View
{
private $controller;
  private $counter;

  public function attach(Controller $controller)
  {
    $this->controller = $controller;
  }

  public function update(Model $model)
  {
    $this->counter = $model->getCounter();
  }

  public function getCounter()
  {
    return $this->counter;
  }

  public function render()
  {
    return 'Counter: ' . $this->counter;
  }

  public function notify()
  {
    $this->controller->update($this);
  }
}

Die View bietet mit der Methode attach() zunächst die Möglichkeit, einen Controller anzuhängen. Theoretisch könnten das auch mehrere Controller sein, wenn auf verschiedene Events in der View unterschiedlich reagiert werden oder diese Interaktion Auswirkungen auf mehrere Models haben muss.

Neben der obligatorischen update()-Methode, um der View zu sagen, dass sie sich aktualisierte Daten vom Model holen soll, gibt es noch die Methode getCounter(), über die sich der Controller die Daten von der View holen kann. Um die View darzustellen, brauchen wir natürlich eine render()-Methode, die in unserem Fall einfach nur einen String zurückgibt.

Die Methode notify() ist in der View öffentlich, um den Controller direkt bei einer Zustandsänderung beziehungsweise einem Event wie „Button geklickt“ zu benachrichtigen. Nun fehlt noch der Controller (Listing 4).

class MvcController implements Controller
{
  private $model;

  public function __construct(Model $model)
  {
    $this->model = $model;
  }

  public function update(View $view)
  {
    $this->model->setCounter($view->getCounter() + 1);
  }
}

Der Controller ist fest mit einem Model verbunden, daher muss dieses als Konstruktorparameter übergeben werden. Sobald der Controller von der View benachrichtigt wird, dass es Arbeit gibt, holt er sich den Zustand (also den Zählerstand) von der View, indem er die Methode getCounter() aufruft. Diesen Zählerstand inkrementiert er und setzt den neuen Zählerstand im Model. Man könnte den Code noch etwas schöner machen, indem man im Model eine Methode increment() implementiert, die der Controller aufruft. Dann allerdings müsste der Controller unterscheiden, welches Event die View gemeldet hat: ein Mouse-over-Event beispielsweise soll ja nicht ein Inkrementieren des Zählers auslösen. In der aktuellen Implementierung ist es egal, wegen welchem Event die View den Controller benachrichtigt, er holt sich immer den aktuellen Zählerstand, und wenn dieser unverändert ist, dann machen wir uns zwar unnötig Arbeit, aber das Ergebnis ist in jedem Fall richtig. Diese überflüssige Arbeit könnte man verhindern, in dem das Model nur bei einer „echten“ Zustandsänderung die notify()-Methode aufruft:

public function setCounter($counter)
{
  $previous = $this->counter;
  $this->counter = $counter;
  if ($previous != $counter) {
    $this->notify();
  }
}

Nun wollen wir mal sehen, ob unser MVC auch richtig funktioniert. Im nachfolgenden Steuerprogramm werden zunächst die Objekte erzeugt und miteinander verknüpft. Dann initialisieren wir das Model mit dem Zählerstand 1 und stellen die beiden Views dar, um uns davon zu überzeugen, dass sie den Zählerstand auch wirklich korrekt anzeigen. Danach simulieren wir durch den Aufruf der Methode notify() einen Klick auf den Button, der durch die erste View repräsentiert wird, und stellen erneut beide Views dar, um zu sehen, ob alles korrekt funktioniert hat (Listing 5).

$controller1 = new MvcController($model);
$view1 = new MvcView;

$view2 = new MvcView;
$controller2 = new MvcController($model);

$model->attach($view1);
$model->attach($view2);

$view1->attach($controller1);
$view2->attach($controller2);

$model->setCounter(1);

var_dump($view1->render());
var_dump($view2->render());

$view1->notify();

var_dump($view1->render());
var_dump($view2->render());

Das Programm erzeugt folgende Ausgabe – und wir sind glücklich:

string(10) "Counter: 1"
string(10) "Counter: 1"
string(10) "Counter: 2"
string(10) "Counter: 2"

Leider währt das Glück nur kurz, denn wir müssen uns nun der Realität stellen, dass die vorliegende Software nichts, aber auch rein gar nichts mit dem zu tun hat, was uns in Form von Webframeworks als MVC verkauft wird. Was genau ändert sich also, wenn man MVC im Webkontext einsetzen will – und weshalb?

MVC entstand ursprünglich Ende der siebziger Jahre des letzten Jahrtausends, als es noch kein World Wide Web gab. Es kam zum Einsatz in Desktopanwendungen, die im Gegensatz zu Webanwendungen nicht Requestbasiert arbeiten. Webanwendungen müssen für jede Anfrage erneut ihren Objektgraphen aufbauen, was durchaus ein Vorteil sein kann, wenn man beispielsweise Experimente oder A/B-Tests durchführt und der Objektgraph daher für jeden Request ein wenig anders aussieht.

MVC im World Wide Web

Im Kontext von MVC stehen wir allerdings plötzlich vor einem Problem: Beim klassischen MVC-Entwurfsmuster sind alle Objekte schon vorab fest miteinander verdrahtet worden. Dabei beobachten die Controller die Views und reagieren bei einer Änderung des Zustands (um auf diesem Weg Benutzereingaben zu verarbeiten) und die Views beobachten das Model, um sich bei einer Veränderung gegebenenfalls zu aktualisieren. In einer PHP-Webanwendung erhalten wir einen HTTP-Request, den wir verarbeiten sollen. Dabei stehen wir sozusagen erst einmal vor dem Nichts und müssen uns überlegen, welche Objekte wir erst einmal erzeugen müssen, um die Anfrage überhaupt verarbeiten zu können.

Aber treten wir sogar noch einen weiteren Schritt zurück: Eine Webanwendung ist eine Client-Server-Anwendung mit einem so genannten Thin-Client, der im Prinzip nur Darstellungsaufgaben übernimmt, beispielsweise indem er eine klassische HTML-Seite im Browser darstellt. Aus der Sicht von PHP ist diese Seite lediglich ein String gewesen, der zum Client gesendet wurde.

Je mehr JavaScript in diesem Szenario zum Einsatz kommt, desto „dicker“ wird der Client und wir bewegen uns konzeptuell zunehmend in Richtung einer verteilten Anwendung, was ganz neue Probleme aufwirft. Aber beschränken wir uns für unsere Betrachtungen einmal auf das grundlegende Protokoll des World Wide Web, nämlich HTTP. Ein Client schickt eine HTTP-Anfrage an den Server, der diese mit einer HTTP-Response beantwortet. Sowohl der HTTP-Request wie auch die HTTP-Response, die normalerweise aus Headern und einem Body, der eigentlichen HTMLSeite besteht, sind dabei lediglich Strings. Wenn man also so will, dann macht eine Webanwendung den ganzen lieben langen Tag nichts anderes als Textverarbeitung, weil nämlich ein Eingabe-String in einen Ausgabe-String „umgewandelt“ wird, oder anders gesagt, auf eine Frage, die in Form eines HTTP-Requests gestellt wird, eine sinnvolle Antwort in Form einer HTML-Seite gegeben wird.

Gehen wir vor diesem Hintergrund geistig noch einmal durch, was bei MVC passiert: Wir sehen uns gerade eine View in Form einer im Browser dargestellten HTML-Seite an. Nun klicken wir „irgendwo“. Die einzig mögliche Form der Kommunikation zwischen Client und Server ist nun, dass der Client einen HTTP-Request an den Server schickt. Dabei wird die Seite neu geladen (wie gesagt, wir betrachten gerade nur Seiten ohne JavaScript und AJAX). Wir haben nun das Problem, dass die GUI-Events, für die im klassischen Verständnis die Controller gewissermaßen die „Gerätetreiber“ sind, erst einmal gar nicht im Server ankommen.

Auch wenn wir gar nicht so genau wissen, wie es technisch funktionieren soll, stellen wir uns einfach vor, unser Client schickt einen HTTP-Request, der in irgendeiner geeigneten Form eine Information über das Event enthält, auf das der Controller reagieren soll. Allerdings: welcher Controller? Heutzutage arbeitet man normalerweise alle Anfragen an einen Server durch ein zentrales Bootstrap-Skript ab, das typischerweise index.php heißt. Nun haben wir noch keinen Objektgraphen und müssen erst einmal herausfinden, welcher Controller überhaupt angesprochen wurde. Diesen Vorgang nennt man Routing. Hierbei wird ein HTTP-Request auf einen Controller abgebildet, oder, allgemeiner gesprochen, auf den Aufruf einer Methode in einem vermutlich neu zu instanziierenden Objekt. Die Applikation wird dabei meist durch einen so genannten Front Controller repräsentiert, der erst das Routing durchführt und dann den Controller aufruft.

Hier geht’s zu Teil 2

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -