Hallo, ich heiße Matthias Zeis - schön, dass Sie den Weg zu meiner Übersetzung von ZFSTDE gefunden haben!
Falls Sie mehr über Magento 2 oder meine Arbeit als Technical Lead bei LimeSoda erfahren möchten, kommen Sie doch auf meiner Website vorbei.
Inhaltsverzeichnis
In Kapitel 2 haben wir uns mit dem Model-View-Controller-Designpattern (MVC) bekannt gemacht und erforscht, wie es zusammengesetzt ist und warum es für Webanwendungen nützlich ist. Ich habe dort bereits angemerkt, dass das Model dabei das am schwersten zu begreifende Konzept ist.
Es gibt zahlreiche Interpretationen des Models, doch viele Programmierer setzen es mit dem Datenzugriff gleich - eine falsche Auffassung, die von vielen Frameworks noch unabsichtlich gefördert wird, da die Frameworks nicht klar und deutlich zugeben, dass sie kein vollständiges Model anbieten. In unserer von "Buzzwords" überschwemmten Gesellschaft belassen es viele Frameworks in ihrer Dokumentation bei einer unklaren und undurchsichtigen Definition des Models.
In Wirklichkeit repräsentiert das Model einen Teil des Domain-Models. Das Domain-Model ist zusammengesetzt aus vielen miteinander verbundenden Model-Objekten, welche die Daten und das Verhalten des Systems repräsentieren. Es ist vom Controller und von der View vollkommen unabhängig.
Worum es sich bei Domain-Models und Systemen handelt, ist nicht unbedingt gleich greifbar, also stellen Sie sich als Analogie einen Supercomputer vor, der eine Simulation klimatischer Modelle ablaufen lässt. Die Simulation mag auf einer beliebigen Anzahl von Backend-Architekturen laufen, aber was sie wirklich ausmacht, sind die Daten, die den Anfangszustand des Klimamodells herstellen und die logischen Regeln, welche die Gesetze definieren, nach denen sich das Model mit der Zeit entwickelt. Inneralb dieses Gesamtsystems gibt es viele miteinander verbundene und voneinander unabhängige Modelle. Das M in MVC wird aus ähnlichen Gründen Model genannt - es modelliert genauso das Verhalten des Systems, wie es auch seine Daten definiert.
Die Abbildung des Models in einer Datenbank oder jedem anderen Speichermedium, in dem Daten abgelegt werden, reicht von einer einfachen Abbildung, in der ein Model einer einzelnen Datenbanktabelle entspricht, bis zu komplexeren Verhältnissen, in denen ein Model aus vielen untergeordneten Models besteht und mehrere Tabellen involviert sind. Wie auch immer: das Abbilden von Models in einer Datenbank oder einem anderen Speichermedium alleine beschreibt das Model nicht ausreichend. Vielmehr können Sie den Datenzugriff dann noch nachträglich hinzufügen, wenn Sie das Model entwickelt haben, und das ist kein unüblicher Ansatz! Das Model schließt die Geschäftsregeln, das Verhalten und die Beschränkungen der Daten mit ein, die es repräsentiert, und diese können in einer Datenbank gespeichert werden (üblicherweise dann, wenn sie auf quantifizierbare, konfigurierbare Regeln reduziert wurden), nachdem sie entworfen und dokumentiert worden sind.
Der andere diskussionswürdige Aspekt von Models ist, wie sie verwendet werden und wie wir sie gegenüber den anderen MVC-Komponenten gewichten. Die erste Regel ist natürlich, dass Models nicht wissen dürfen, wie sie präsentiert werden. Die Präsentation ist Aufgabe der View und des Controllers. Sie müssen das Model kennen, mit dem sie agieren. Dabei handelt es sich aber um eine Einbahnstraße: Controller und Views dürfen das Model kennen, aber das Model darf nie von ihnen wissen.
Wie in Kapitel 2 erwähnt hat das Model im Model-View-Controller-Schema zwei Hauptaufgaben: erstens den Zustand zwischen Anfragen zu erhalten, indem alle Anwendungsdaten vor Ende der Abfrage gespeichert werden, damit sie in folgenden Anfragen wieder abgerufen werden können, und zweitens die Geschäftsregeln und Bedingungen zu beherbergen, welche auf die Daten zutreffen, die den aktuellen Status bilden. Nun kann jedoch kein Web-Application-Framework voraussagen, welche Geschäftsregeln, Arten von Daten oder Beschränkungen benötigt werden, und somit ist es für Frameworks unmöglich, ein vollständiges Model anzubieten. Stattdessen müssen Entwickler all ihre Models selbst erstellen, da sie für jede Anwendung einzigartig sind. Das heißt nicht, dass Framework-Funktionalitäten in Bezug auf Models nutzlos sind! Frameworks bieten trotzdem außerordentlich nützliche Komponenten an, um auf Daten des Models in Datenbanken, Web-Services und anderen Quellen zuzugreifen.
Diese Erklärung sollte einen essentiellen Punkt betonen, den Sie berücksichtigen müssen: ein Model greift nicht nur auf eine Datenbank zu. Es kapselt alle Daten und jedes Verhalten in eine spezifische Einheit innerhalb der Anwendung, ausgenommen jegliche Logik, die für die Darstellung der Daten zuständig ist.
Das hört sich in der Theorie alles gut an, aber wie wäre es mit einem kurzen Beispiel? Unser Beispiel folgt einem einfachen Model-Design, in dem jedes Model in einer einzelnen Datenbanktabelle abgebildet wird. Zudem wird unser Model durch eine Subklasse von Zend_Db_Table_Abstract repräsentiert. So bildet sich zwischen Model und Datenzugriffsschicht eine is-a-Beziehung im Gegensatz zur alternativen, in komplexeren Domain-Models üblichen has-a-Beziehung, in welcher der Datenzugriff zu Models erst hinzugefügt und nicht notwendigerweise als Grundlage verwendet wird, auf der die Models aufbauen.
<?php
class Order extends Zend_Db_Table_Abstract
{
protected $_name = 'orders';
protected $_limit = 200;
protected $_authorised = false;
public function setLimit($limit)
{
$this->_limit = $limit;
}
public function setAuthorised($auth)
{
$this->_authorised = (bool) $auth;
}
public function insert(array $data)
{
if ($data['amount'] > $this->_limit
&& $this->_authorised === false) {
throw new Exception('Unauthorised transaction of greater than '
. $this->_limit . ' units');
}
return parent::insert($data);
}
}
In der obigen Klasse haben wir ein wirklich einfaches Bestell-Model implementiert, in dem Benutzer ausreichende Befugnisse haben müssen, damit Bestellungen in einem Umfang von mehr als 200 zugelassen werden. Wenn diese Befugnis nicht von einer externen Stelle erteilt wird, wird eine Ausnahme geworfen, die von einem Controller aufgefangen werden kann, um das Problem zu behandeln. Das Limit liegt standardmäßig bei 200, kann aber von außen modifiziert werden (z.B. durch eine Konfigurationsdatei).
Ein anderer guter Weg, sich Gedanken über das Konzept des Models zu machen ist, die Geschäftslogik aus der oben gezeigten Klasse herauszunehmen und darüber nachzudenken, wohin sie platziert werden sollte, wenn nicht ins Model. Das führt uns zu einem anderen klassischen Gedankenkonzept, das wir uns näher ansehen werden.
Jamis Buck (der Autor
von Capistrano, nun angestellt bei 37signals) schrieb
einmal über ein Konzept
namens "Skinny Controller, Fat Model" (unser
Chris Hartjes schrieb
einen Blogeintrag
über dasselbe Thema). Ich mochte schon immer die
Schlichtheit dieses Konzepts, denn es illustriert einen Schlüsselaspekt
von MVC. In diesem Konzept wird es als "best practice" angesehen,
Anwendungslogik (wie unsere Geschäftslogik weiter oben) so weit als
möglich im Model vorzuhalten, nicht in der View- oder
Controllerschicht.
Die View sollte nur damit beschäftigt sein, eine Benutzeroberfläche zu generieren und darzustellen, damit die Benutzer die angeforderte Ausgabe erhalten und Anfragen an die Anwendung stellen können. Controller fungieren als Koordinatoren, die Eingaben aus der Benutzeroberfläche in Aktionen von und mit Models umsetzen und dann dem Client die Ausgabe zurückschicken, wobei die View ausgibt, was die Models an Daten übergeben haben. Controller definieren das Verhalten der Anwendung nur insofern, als sie Eingaben von der Benutzeroberfläche in Aufrufe von Models übersetzen und die Interaktion mit dem Client abwickeln. Darüber hinaus jedoch sollte klar sein, dass die gesamte restliche Geschäftslogik im Model enthalten ist. Controller sind simple Geschöpfe mit minimalem Code, die nur die Grundvoraussetzungen und eine Umgebung schaffen, in der die Anwendung in geordneter Weise ablaufen kann.
Damit ergibt sich eine wichtige Verbindung, denn sowohl der Controller als auch die View sind auf die Darstellung ausgerichtet. Für die View ist das offensichtlich, für den Controller nicht unbedingt. Da er jedoch so damit beschäftigt ist, sich um die HTTP- oder Konsolenmechanismen der Anwendung zu kümmern, ist er fest in die Darstellungsschicht integriert.
Ein großer Teil der PHP-Entwickler versteht diese Konzepte allerdings nicht vollständig. Viele sehen "Model" als ein Modewort für den Datenbankzugriff an, andere setzen es mit verschiedenen Designpattern für den Datenbankzugriff wie "Active Record", "Data Mapper" und "Table Data Gateway" gleich. Wir können aber nicht alles auf PHP-Entwickler schieben - es gibt genauso viele Ruby- und Pythonentwickler, die ähnlich denken. Diese falsche Auffassung wird von Frameworks oft unfreiwillig gefördert, indem nicht vorab ein Model definiert wird. Weil Entwickler nicht vollständig verstehen, was ein Model ist, warum es so eine tolle Sache ist und wie es entwickelt, konzeptioniert und verwendet wird, schießen sie sich unabsichtilich in den Fuß, indem sie andere Alternativen einsetzen.
Dieser alternative Weg besteht darin, dass Entwickler ihr Model auf den Datenzugriff beschränken und die Anwendungslogik in den Controller auslagern. Das ist ein Problem, denn so vermischt sich die Anwendungslogik mit dem Teil von MVC, der für die Präsentation zuständig ist. Ja, noch einmal: Controller sind Teil der Präsentationsschicht. Benutzereingaben in Model-Aktionen übersetzen, festlegen, welche View die richtige ist und diese rendern, die Kommunikation mit dem Client handhaben...sehen Sie, Präsentation!
Am einfachsten lässt sich das Problem verdeutlichen, indem man überlegt, wie sich eine solche Geschäftslogik testen lässt. Controller sind berühmt-berüchtigt dafür, dass sie schwer zu testen sind, da sie Teile der Präsentation darstellen, die - wie die View - für jeden einzelnen Test den kompletten Ablauf der Anwendung benötigen. Und dann können Sie nur die endgültige Ausgabe testen, was heißt, dass Controller-Tests entweder Annahmen über Daten treffen, die an die View übergeben worden sind, oder gar nur über den Inhalt der finalen View selbst. Wenn Sie mit Unit-Testing vertraut sind, wissen Sie, dass dies das grundsätzliche Prinzip bricht, wonach die einzelnen Teile isoliert getestet werden, und so lassen sich auch die Praktiken testgetriebener Entwicklung (Test-Driven Design, TDD) und verhaltensgetriebener Entwicklung (BDD) nur schwer auf Geschäftsregeln anwenden. Die meisten dieser Unit-Tests sind in Wirklichkeit Funktionalitätstests und testen die gesamte Anwendung, nicht nur einen kleinen Teil davon.
Das Problem lässt sich auch in einem anderen Fall demonstrieren. Stellen Sie sich vor, Sie haben gerade ein neues Projekt mit dem Zend Framework fertiggestellt. Der Kunde ist beeindruckt, aber unterbreitet Ihnen die Neuigkeit, dass alle Ihre Anwendungen auf Symfony umgestellt werden müssen. Zugegebenermaßen ist das kein häufig auftretendes Szenario, aber es ist in der Vergangenheit schon mehr als einmal vorgekommen, dass eine Anwendung auf eine andere Plattform migriert werden musste oder in letzter Minute einer Erneuerung unterzogen wurde.
Wenn Sie alle Geschäftsregeln im Domain Model untergebracht haben, dann können Sie ziemlich schnell und einfach migrieren. Models sind im Wesentlichen unabhängig vom Web-Application-Framework, auch wenn sie sich eventuell auf Datenzugriffskomponenten stützen, die nicht so leicht zu migrieren sind (kein Problem für das Zend Framework, da dort alles lose gekoppelt ist) - außer es wurde eine has-a-Beziehung mit der Datenzugriffsschicht etabliert, was ermöglicht, Datenzugriffskomponenten leichter durch Substitute zu ersetzen. Haben Sie Geschäftslogik in Controller ausgelagert, werden Sie Ihren Fehler bemerken - Symfony kann Zend-Framework-Controller nicht ausführen! Nun müssten Sie anfangen, ganze Code-Abschnitte zwischen den Frameworks zu migrieren anstatt der minimalen Koordinationslogik, auf die sich ein Controller beschränken sollte. Bedenken Sie nun noch die Probleme in Hinblick auf Unit-Tests und Sie können sich von Ihren Funktionalitätstests verabschieden, die auf Zend_Test beruhen.
Letzten Endes sind Models eigenständige Klassen, die von der Darstellungsschicht getrennt getestet und leicht auf andere Framework-Systeme migriert werden können. Controller sind das komplette Gegenteil! Sie sind so sehr an die Darstellungsschicht und das Framework selbst gebunden, dass sie nicht direkt portierbar sind.
In der Vergangenheit haben PHP-Entwickler zwei gebräuchliche Designpattern verwendet, um Anwendungen zu erzeugen: den "Page-Controller" und das "Transaction-Script". Trotz ihrer schönen Namen und obwohl sie Designpattern sind, weisen sie nur auf den intuitivsten Ansatz zum Erstellen von Inhaltsseiten mit PHP hin, bei dem jede Seite durch ein PHP-Skript repräsentiert wird, das in prozeduraler Form alle Operationen für diese Seite enthält. Obwohl wir uns von diesen Pattern weg in Richtung MVC bewegt haben, wird man alte Gewohnheiten schlecht los und so hat sich ein Weg gefunden, in dem diese alte Mentalität in Form von Elementen im Model-View-Controller-Paradigma neu aufersteht.
Da der Begriff einprägsam und anschaulich ist und mir vielleicht etwas Anerkennung für meinen Esprit (oder auch für sein Fehlen) einbringt, bezeichne ich diese wieder auferstandenen Elemente als fette, dumme, hässliche Controller (Fat Stupid Ugly Controllers, FSUCs). Zuerst erwog ich "dumme, fette, hässliche Controller" (Stupid Fat Ugly Controllers, SFUC), aber das erschien mir als etwas zu grob, auch wenn sie dadurch nachdrücklicher schlecht gemacht würden.
Der typische FSUC resultiert daraus, dass man beim Adoptieren von MVC nicht den Gedankenschritt weg vom Page-Controller- und Transaction-Script-Modus macht, sondern Models kontinuierlich degradiert, indem man sie auf Datenzugriffsrollen beschränkt. Der Modus führt zu Controllern, die das "Skinny Controller, Fat Model"-Paradigma durchbrechen und es komplett umkehren. Hier wird die Geschäftslogik vollständig in Controller ausgelagert.
Der typische FSUC führt alle Operationen der Geschäftslogik durch und versagt beim Konzept des Domain-Models, da Controller nicht eigenständige, wieder verwendbare Klassen sind. Entwickler nutzen sie genauso selbstverständlich und häufig, wie sie das auch mit Page Controllern getan haben - mit dem gleichen Effekt. Jede Seite der Anwendung ist gebunden an ein furchtbares Knäuel von Spaghetticode, das nur wenige Vorteile von gutem objektorientisierten Design erkennen lässt. Methoden des Controllers blähen sich auf, die Zahl der Hilfsklassen explodiert, wenn zu einem bestimmten Zeitpunkt versucht wird, in Richtung Domain-Model nachzurüsten, und Unit-Testing wird oft aufgegeben oder auf funktionales Testen beschränkt, weil man die komplette MVC-Anwendung initialisieren muss, um auch nur irgendetwas zu testen - was problematisch ist, wenn die View noch nicht existiert, weil man noch keinen Webdesigner angeheuert hat.
FSUCs sind groß, unhandlich, sehen hässlich aus und sind fett. Der passende (quasi-)Programmiererbegriff dafür ist "aufgebläht". Sie übernehmen Rollen, für die sie nie vorgesehen waren. Sie sind das genaue Gegenteil von allem, was man bezüglich der Anwendung der Prinzipien objektorientierter Programmierung beigebracht bekommt. Entwickler scheinen aus mysteriösen Gründen FSUCs den Models vorzuziehen, obwohl diese Controller einfach nur getarnte Page-Controller oder Transaction-Scripts sind.
Erinnern Sie sich an die Probleme, die ich erörtert habe, welche entstehen, wenn Models durch Controller ersetzt werden? Unit-Testing wird schwer durchführbar, die Anwendung von TDD oder BDD ohne ausdauernden, unbeugsamen Willen nahezu unmöglich, und es wird Ihnen nie möglich sein, diese Menge von Code effizient auf ein anderes Framework zu migrieren, ohne die halbe Anwendung neu zu entwickeln.
Diese Art von enger Kupplung und verworrenem Code-Design ist das, was Fans von Kent Back beim Refactoring als "code smell" bezeichnen. Das ist aber nicht das einzig übel riechende Artefakt von FSUCs. FSUCs beinhalten häufig duplizierten Code, lassen refaktorierte, extrahierte Klassen vermissen und es kann ein Albtraum sein, sie zu warten. Kurz gesagt sind sie außer für einfachste Anwendungen einfach nur übel und schlecht.
Sehen Sie sich dieses Beispiel einer Kombination aus einfachem Model und FSUC an:
<?php
class Order extends Zend_Db_Table_Abstract
{
protected $_name = 'orders';
}
class OrderController extends Zend_Controller_Action
{
protected $_limit = 200;
protected $_authorised = false;
public function init()
{
// Setze hier irgendwie auf magische Weise ein
// Autorisierungsflag oder ein Limit zur Begrenzung
}
public function createAction()
{
// Halte es einfach (wenn auch unsicher)
$data = $_POST;
$model = new Order;
if ($data['amount'] > $this->_limit
&& $this->_authorised === false) {
// Bereite die View vor, um den Fehler zu melden
}
$model->insert($data);
}
}
Stellen Sie sich nun vor, Sie wollen ein neues Feature namens "Order Batch" implementieren, mit dem mehrere Bestellungen in einem Schub bearbeitet und abgeschickt werden. Sie stoßen unmittelbar auf ein Problem - die ganze Geschäftslogik für Bestellungen steckt in OrderController::createAction(), das kein wiederverwendbares Objekt ist. Werden Sie die Geschäftsregeln in den neuen BatchController kopieren? Eine Task-Queue erstellen, um mehrere Aufrufe des OrderControllers zu starten? Wahrscheinlich könnten Sie einen neuen Action-Helfer erstellen, der allen Controllern zugänglich ist? Haken Sie alle Optionen ab, bis Sie feststellen, dass eine alleinstehende Klasse (z.B. ein Model) am besten funktioniert. Sie ist die flexibelste Lösung und gehorcht dem Konzept "Keep It Simple Stupid" (KISS).
Wenn Entwickler überzeugt sind, dass Models möglichst klein gehalten werden sollten und Controller das Wichtigste sind, dann fördern sie als Folge dieser nicht zu Ende gedachten Überlegungen das Vertrauen darauf, dass Controller eine neue Rolle als Datenpolizei übernehmen (ein Hauptgrund, warum sie zu FSUCs mutieren).
In dieser Rolle werden alle Datenanfragen durch einen Controller kanalisiert. Das ist aber nicht immer eine geeignete Strategie, vor allem nicht in Szenarien, in denen andere Teile der Anwendungen nur Daten lesen wollen.
Ich habe hier ein Beispiel, auf das ich in den Mailinglisten des Zend Framework gestoßen bin, als ich das Buch geschrieben habe. In einer unbekannten Anwendung wird auf jeder Seite in einer seitlichen Leiste die Anzahl der Benutzer angezeigt. Um diese Daten in das Template zu bekommen, fragen alle Controller-Actions über die gesamte Anwendung hinweg die Daten ab und übergeben sie der aktuellen View. Ein großer Teil des dafür benötigten Codes wird in die Initialisierungs-Methode jedes Controllers kopiert.
Das offensichtliche Problem dabei ist die Duplizierung von Code. Wenn der Code in jede Controller-Klasse kopiert wird, wird er schwer zu warten sein. Doch das Problem enthält eine noch schändlichere Komponente. Der Beschreibung nach ist die View der einzige Teil der Anwendung, der diese Daten verwendet. Der Controller braucht sie nie direkt, er manipuliert sie nie, und er schreibt nie Daten in das Model zurück. Wenn der Controller diese Daten nie verwendet, warum verrichtet er dann all die Kleinarbeit, um sie abzufragen?
Als Lösung habe ich in der Mailingliste vorgeschlagen, den Mittelsmann zu eliminieren. Da der Controller die Daten nie verwendet, gehen wir doch einen Schritt zurück und lassen wir die View selbst die Daten holen, indem wir einen simplen View-Helfer verwenden (eine Klasse, die unendlich wiederverwendbar ist). Diese kurze Gschichte illustriert einen weiterverbreiteten Irrtum - Controller sind nicht die einzige Quelle aller Model-Interaktion. Sie überwachen nicht exklusiv die Daten. Views können sich selbst ohne Erlaubnis des Controllers ein Model holen und so effizienter Daten zur Darstellung abfragen.
Wenn Sie diese Alternative weiter verfolgen, werden Sie bald feststellen, dass Sie den Controllern eine weitere Komplexitätsschicht abnehmen können, wodurch ihre durchschnittliche Größe weiter reduziert wird. Je dünner, desto besser!
Es sollte noch angemerkt werden, dass der Begriff "View-Helfer" mit Java in Form eines Designpatterns in dem Buch "Core J2EE Design Patterns" verbunden ist. In MVC gibt es genug Anzeichen dafür, dass Views über die Models Bescheid wissen sollten, die sie präsentieren und nicht nur über irgendwelche Arrays, von denen wir beschließen, dass wir sie löffelweise über die Controller an die View verfüttern wollen.
Das Lesen dieses Kapitels hat Sie hoffentlich zu einigen neuen Ideen inspiriert. Der eine Gedanke, der sich von Anfang bis Ende durchzieht, ist die Anwendung eleganter objektorientierter Programmierung. Aufgeblähte Controller, die nicht vom darunterliegenden Framework entkoppelt sind, werfen offensichtliche Probleme auf, wohingegen Models, die stark entkoppelt und in einem System unabhängiger Klassen strukturiert sind, weit besser wartbar, testbar und portierbar sind. Durch diese Entkopplung der Models wird es leicht, von überall (inklusive der View) auf sie zuzugreifen.
Dieses Kapitel beschließt unsere Auseinandersetzung mit dem MVC-Pattern, und ich bin mir sicher, dass die für die detailliertere Auseinandersetzung mit dem Model aufgewendete Zeit gut investiert war. Im folgenden Kapitel 4 werden wir einige Zeit damit verbringen, das Zend Framework im Lichte von MVC zu betrachten und einige der Kernkomponenten kennen zu lernen, mit denen Applikationen mit MVC-Architektur implementiert werden können.