Unit of Work Implementation
Vor einiger Zeit gab es im Zend Framework Forum eine Diskussion über die sinnvolle Implementation des Unit of Work-Patterns. Da ich diesen Pattern bereits aus dem Buch Patterns of Enterprise Application Architecture von Martin Fowler
kenne, habe ich mich natürlich rege an dieser Diskussion beteiligt. Zu
dem Zeitpunkt hatte ich den Pattern allerdings noch nie implementiert,
was es schwierig machte, meine Erfahrungen zu beschreiben.
Glücklicherweise wurde ich dann beauftragt, eine Seite zu
programmieren, auf der man einen sog. Flughafentransfer buchen kann. Da
die Applikation bereits existierte und diese Bestellung zu dem
Zeitpunkt komplett ohne Datenbank ablief, kam mir die Idee, dieses
Prinzip nicht zu verwerfen. Warum denn auch? Solange es funktioniert,
kann ich mich nicht beschweren. Allerdings reichte mir die Eleganz des
Ansatzes nicht, da der Code (leider) nicht objektorientiert
programmiert war. Also begann ich mir eine Idee zu ersinnen, mit der es
mir möglich sein sollte, die Buchung objektorientiert zu realisieren.
Schnell fiel mir da der Unit of Work-Pattern
ein. Nun will ich endlich die Erfahrungen "nachreichen", die ich mit
der Implementation gemacht habe und vor allem auch, wie ich vorgegangen
bin.
Zuerst war die Frage, wo ich die gespeicherten Daten der Buchung
zwischenspeichere. Es gab da einige Möglichkeiten: Von benutzerseitiger
Speicherung (POST, GET, COOKIE) über serverbasierte Speicherung
(SESSION, Dateien) bis hin zur Datenbank. Letztendlich habe ich mich
für die Session entschieden, da dies die einfachste Variante war. Da
ich das Projekt auf Basis von MVCLite
entwickelt habe, war eine weitere Frage, wo ich diese Unit of
Work-Implementation speichere. Optionen gab es viele, aber die beste
Option war in meinen Augen ein eigenes Modell, das dann kurzerhand
"BookingModel" genannt wurde. Diese Klasse sollte nun die gesamten
Daten speichern und mit geeigneten Methoden auch manipulieren und
zurückgeben. Implementiert wurde das ganze Modell wie ein beliebiges
anderes auch. Es existieren lediglich zwei signifikante Unterschiede:
Erstens ist die Datenquelle eine andere und zweitens ist es notwendig,
die Daten später persistent zu speichern.
Schnell begann ich die Implementation. Das Modell sollte ja seine Daten
in der Session speichern. Aber wieso nur die Daten speichern, wenn man
gleich das ganze Objekt speichern kann? Dies setzt natürlich voraus,
dass man bedächtig programmiert. Denn man sollte wenig mit
verschachtelten Arrays oder Objektstrukturen arbeiten. Vor allem ist es
wichtig, so gut wie möglich auf Referenzen zu verzichten, da diese
nicht durch die Serialisierung berücksichtigt werden. Das war aber in
meinem Fall kein großes Problem.
Mein erster Ansatz war folgender:
class BookingModel
{
public function __construct ()
{
MVCLite_Request_Global_Session::getInstance()->booking = $this;
}
}
Wenn ich nun ein Objekt instanziere, speichert es sich sofort selbst
in der Session. Da ich nur ein Objekt der Art habe, wären keine
Diskrepanzen zwischen erwartetem und realem Objekt enstanden.
Es hätte auch wunderbar funktioniert, leider habe ich vergessen, dass
ich nicht immer auf die gleiche Art und Weise an das Objekt komme. Ich
hätte es gerne so gehabt, dass ich, egal, wie ich an das Objekt komme,
nicht mehrere Instanz der Klasse habe. Wie realisiert man so was?
Natürlich über den Singleton-Pattern.
Es ist also nur notwendig, die Klasse um eine Methode getInstance zu erweitern, die mir das BookingModel zurückgibt. Klingt einfach und ist es auch. Ein Vorschlag könnte so aussehen:
class BookingModel
{
private static $_instance;
private function __construct ()
{
MVCLite_Request_Global_Session::getInstance()->booking = $this;
}
public static function getInstance ()
{
if(self::$_instance == null)
{
$session = MVCLite_Request_Global_Session::getInstance();
self::$_instance = (isset($session->booking) ?
$session->booking : new self());
}
return self::$_instance;
}
}
Hmm... Sieht passabel aus und könnte man so benutzen. Aber als Perfektionist bin ich immer auf der Suche nach besonders effektiven Lösungen. Also habe ich den Code noch ein wenig umgeformt.
class BookingModel
{
public static function getInstance ()
{
$session = MVCLite_Request_Global_Session::getInstance();
if($session->booking == null)
{
$session->booking = new self();
}
return $session->booking;
}
}
Das ist jetzt deutlich weniger Code. Perfekt, das passt. Diese
Lösung müsste nun effizient genug sein, um die Bestellung zu
realisieren. Nachdem die Basis geschaffen war, musste man das Model nur
wie gewohnt implementieren. Letztendlich fehlte nur noch eine Methode,
mit der man die Buchung abschließen kann. Dies ist allerdings nicht
wesentlich. Man muss nur die Daten leeren (oder das Modell löschen).
Kurzum: Der Unit of Work Pattern eignet sich hervorragend für
mehrseitige Bestellformulare. Aber auch für viele weitere Einsatzfälle
ist dieser Pattern geeignet. Grundsätzlich lohnt es sich jedoch nicht,
diesen Pattern zu implementieren, wenn man eine gesamte Transaktion auf
einer Seite abarbeiten kann. Mein Beispiel war recht trivial, aber es
hat die grundsätzliche Funktion dieses Patterns beleuchtet. Gehen wir
nun weiter bei Bestellungen mit Datenbanken als Backend, gestaltet sich
der gesamte Prozess komplizierter. Als Beispiel soll folgende Situation
gewählt werden: Ein Benutzer möchte Artikel aus einem Webshop
bestellen. Erstmal will er die Artikel, die er kaufen möchte, in seinen
Warenkorb legen. Diesen Warenkorb kann man sehr gut als Unit of Work
implementieren. Grundsätzlich könnte es funktionieren, wie bei unserem
vorangegangenen Beispiel. Der Käufer braucht ca. 30 Minuten bis er
seinen Einkauf erledigt hat. Nun möchte er die Artikel bestellen.
Klingt auch noch trivial. Was passiert aber, wenn während dieser 30
Minuten so viele Artikel gekauft wurden, sodass der Käufer einen
bestellt, aber keinen mehr bekäme? Bei dieser Frage kann man
zwiegespalten sein und das hängt von den Vorgaben ab, da der Umgang auf
technischer Seite keinen signifikanten Unterschied ergibt. Z.B. könnte
man die Bestellung als Vorbestellung annehmen, die dann verschickt
wird,wenn wieder Artikel vorhanden sind.Oder man weist den Benutzer
darauf hin, dass es diesen Artikel nicht mehr gibt und er ihn nicht
mehr bestellen kann. Der wesentliche Punkt ist: Beim Fertigstellen der
Bestellung muss es eine Routine geben, die überprüft, ob die gesamten
Daten überhaupt noch konsistent sind. Denn andernfalls kommt es leicht
zu Fehlern. Diesen Ansatz nennt man Optimistischen Lock. Wie auch im realen Leben, so gibt auch in der Software Pessimisten, also auch den Pessimistischen Lock.
Das Ziel ist wieder, die Konsistenz zu wahren. Erreicht wird dies,
indem man einen Artikel, der bereits in einem Warenkorb ist, blockiert,
sodass es praktisch gar nicht möglich ist, dass weniger als 0 Artikel
zur Verfügung stehen. Klingt auch plausibel und wird sicherlich auch
gut funktionieren. Aber nicht jeder, der einen Artikel im Warenkorb
ablegt, kauft ihn auch. So kann es vorkommen, dass ein Artikel nicht
mehr gekauft werden kann, obwohl noch real – damit meine ich Artikel,
die nicht gekauft werden – x > 0 Artikel zur Verfügung stehen. Die
wahrscheinlichste Lösung ist das Unlocken dieser Artikel, wenn die
Session zu alt ist.
Wie man gut sehen kann, gibt es etliche Möglichkeiten, eine Unit of
Work zu implementieren und man kann nur das Grundlegendste erklären, da
dieser Pattern sehr viele Facetten aufweist. Viele habe ich noch nicht
mal erwähnt, wie z.B. die Identity Map. Ich hoffe, die Funktionsweise
des Patterns ist klar. Wie man seine Unit of Work umsetzt hängt
hauptsächlich von den Anforderungen ab. Wie in meinem anfänglichen
Beispiel könnte man eine Unit of Work ohne Lock implementieren. Bei
einem Webshop ist der Optimistische Lock wahrscheinlich die bessere
Wahl, während bei kritischeren System sicherlich der Pessimistische
Lock eingesetzt wird.
Es bleibt, wie so häufig, mein Resümee: Nutzt das, was am passendsten ist!