Unit of Work Implementation

Friday, 30 November 2007, 17:13 von Blackflash

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!

Kommentare


Kommentiere!

Your Name:


Your Email:


Your URL:


Spam Prevention:
Enter the text above into the box below.
If you are unable to read it, refresh the page.


Your Comment: