Vor einigen Wochen las ich auf der Homepage von Matthew Weier O'Phinney einen interessanten Artikel über das
.
ist ein konzeptueller Ansatz, um die Verbindung zwischen den Systemen zu beschreiben. Einfach ausgedrückt: Wie greife ich auf meine Daten zu. Matthew's Beispiel war eine User-Verwaltung. Er schrieb dazu mehrere Klassen: Ein Gateway, welches die Benutzerverwaltung übernimmt, eine Users-Klasse, die den Zugriff auf eine Menge von Benutzern ermöglicht und eine User-Klasse, die den einzelnen User repräsentiert. Der Vorteil ist, dass man im Hintergrund den Quelltext austauschen kann, ohne das man Änderungen im Hauptprogramm vornehmen muss. Also z.B. Umstellen der User-Anfragen von Datenbank auf Webservice.
Ich habe etwas mit seiner Variante herumgespielt und kam für mich zu dem Schluss: Nicht praktikabel. Die bereits oben genannten Klassen werden z.B. bereits von Zend_Db_Table bereitgestellt, oder anders ausgedrückt: Man braucht das Rad nicht mehrmals erfinden. Warum soll ich nun nochmal ein Gateway schreiben, das die Arbeit an Zend_Db_Table weiterreicht, wenn es Zend_Db_Table schon macht? Also schrieb ich einfach Interfaces, gegen die ich nun implementieren kann. Damit stelle ich den einheitlichen Zugriff auf meine Klassen sicher.
Genug der Theorie, auf in die Praxis. Als Programmierstandart nutze ich übrigens den des ZendFrameworks. Mein Beispiel ist die Benutzerverwaltung zwischen Serendipity und meinem CMS, sowie angedeutet gegen eine XML-Datei und einem Webservice. Serendipity und das CMS haben zwar im Hintergrund eine Datenbank laufen, aber die Tabellen sind unterschiedlich aufgebaut. Da Programmierer faul sind und sich einheitlicher Quelltext besser warten lässt, sollte man für beide Systeme einheitliche Schnittstellen definieren. Fangen wir mal beim einfachen Nutzer an.
Ein Nutzer hat eine E-Mail-Adresse, einen Namen, einen Benutzernamen, ein Passwort. Er kann sich registriert haben, er kann von einem Administrator aktiviert oder deaktiviert worden sein. Ein Nutzer kann sich am System anmelden. Natürlich müssen die Daten des Benutzers auch in irgendeiner Art gespeichert werden. Dies alles in ein Interface gegossen sieht nun so aus:
interface Users_Row_Interface extends Zend_Auth_Adapter_Interface, Zend_Acl_Role_Interface
{
public function getUserName();
public function setUserName($username);
public function getFirstName();
public function setFirstName($firstname);
public function getLastName();
public function setLastName($lastname);
public function isActivated();
public function setActivated($status);
public function getActivatedDateTime();
public function isBanned();
public function setBanned($status);
public function getBannedDateTime();
public function getEmailAddress();
public function setEmailAddress($email);
public function getPassword();
public function setPassword($password);
public function save();
}
Mein Interface stellt Prototypen zum setzen und abfragen von Benutzernamen, Vor- und Zunamen, der E-Mail-Addresse und des Passwortes bereit. Mit der Methode save() stelle ich sicher, dass es immer eine Möglihckeit gibt, die Daten zu speichern. Sei es in eine Datenbank, in den RAM, eine XML-Datei, per Webservice auf einem anderen Server oder was weiß ich was man noch so machen kann. Wie man außerdem sieht, leite ich das Interface auch von Zend_Auth_Adapter_Interface und Zend_Acl_Role_Interface ab. Damit stelle ich sicher, das auch Methoden zur Authentifizierung und der Zugriffskontrolle zur Verfügung stehen. Aber dazu später mehr.
Wie wenden wir nun das Interface an? Als erstes ein Beispiel für mein selbst geschriebenes CMS:
class cmsUsers_Row extends Zend_Db_Table_Row_Abstract implements Users_Row_Interface
{
}
Und hier ein Beispiel für Serendipity:
class s9yUsers_Row extends Zend_Db_Table_Row_Abstract implements Users_Row_Interface
{
}
Wie man erkennen kann, arbeite ich bei meinen CMS- und Serendipity-Benutzern mit einer Datenbank. Als Datenbank-Layer nutze ich Zend_Db_Table. Oder wollen wir noch etwas verrückter werden? Vielleicht haben wir ja Benutzer in einer XML-Datei stehen?
class xmlUsers_Row extends DOMElement implements Users_Row_Interface
{
}
Zum Schluss noch ein Beispiel für einen Zugriff über einen Webservice:
class webserviceUsers_Row implements Users_Row_Interface
{
}
Was haben alle vier Beispiele gemeinsam? Sie greifen auf unterschiedliche Resourcen zurück, haben aber alle das Interface Users_Row_Interface implementiert. Es ist also für die weitere Verarbeitung egal, ob der Benutzer aus der Serendipity-Datenbank, meinem CMS, einer XML-Datei oder per Webservice in mein System eingebunden wird. Der Zugriff auf die User-Klasse ist immer der Selbe.
Gehen wir nun eine Schicht weiter höher. Wir brauchen eine Möglichkeit, einheitlich auf eine Menge von Benutzern zuzugreifen. Auch dafür gibt es wieder ein Interface. Und was muss dieses beinhalten? Erstmal müssen wir über die User iterieren. Dafür gibt es bereits ein Interface in PHP, namens Iterate. Auch sollten wir eine Methode haben, die uns die Anzahl der User in diesem Rowset zurückgibt. Dafür implementieren wir das Interface Countable. Das sieht dann so aus:
interface Users_Rowset_Interface extends Iterator, Countable
{
}
Man beachte, das man Interfaces in PHP von mehreren Interfaces ableiten kann, aber Klassen nicht. Um ein Interface innerhalb eines Interfaces abzuleiten nutzen wir das Schlüsselwort extends. Und es gibt noch eine Besonderheit: PHP meckert nicht herum, wenn eine Klasse mehrmals von einem Interface abgeleitet wird. Dies machen wir uns bei Zend_Db_Table_Rowset zu nutzen. Dieses Rowset wird bereits vom Interface Iterate und Countable abgeleitet. Warum ich aber nochmal ein eigenes Interface dafür schreibe, werden wir gleich sehen.
Beispiel für meine CMS-User:
class cmsUsers_Rowset extends Zend_Db_Table_Rowset_Abstract implements Users_Rowset_Interface
{
}
Beispiel für die Serendipity-User:
class s9yUsers_Rowset extends Zend_Db_Table_Rowset_Abstract implements Users_Rowset_Interface
{
}
Und hier der Grund, warum ich Iterate und Countable nochmal in ein eigenes Interface gepackt habe:
class xmlUsers_Rowset extends DOMDocument implements Users_Rowset_Interface
{
}
Das DOMDocument ist nicht von beiden Interfaces abgeleitet. Es wird nun aber vom Programmierer verlangt, das er die Methoden implementiert. Und zum Schluss noch unser Webservice-Beispiel:
class webserviceUsers_Rowset implements Users_Rowset_Interface
{
}
Nun haben wir die Grundgerüste für jeden Benutzer und für den Zugriff auf die Benutzer. Zum Schluss brauchen wir noch das Gateway, also eine Klasse mit Methoden, die uns nach bestimmten Kriterien Benutzer zurück gibt. Z.B. fragen wir gezielt einen Benutzer nach Benutzer-ID oder Benutzer-Namen ab. Oder wir wollen alle Benutzer haben, die deaktiviert sind. Um auch hier wieder einen einheitlichen Zugriff zu erhalten, schreiben wir wieder ein Interface.
interface Users_Interface
{
public function setAcl(Zend_Acl $acl);
public function getBannedUsers(array $options = array());
public function getActivatedUsers(array $options = array());
public function createUser(array $data);
public function fetchUser($id);
public function fetchAll(array $options = array());
public function retrive($username);
}
Und nun noch das Beispiel, wie dieses Interface implementiert wird:
class cmsUsers extends Zend_Db_Table_Abstract implements Users_Interface
{
}
Das ganze für Serendipity-Users:
class s9yUsers extends Zend_Db_Table_Abstract implements Users_Interface
{
}
Für unsere Benutzer, die wir in der XML-Datei stehen haben:
class xmlUsers implements Users_Interface
{
}
Und für die Benutzer, die wir über einen Webservice abfragen:
class webserviceUsers implements Users_Interface
{
}
Wie man sieht, kann man mit Hilfe eines Interfaces die Schnittstellen vereinheitlichen. Dem Programmierer kann es egal sein, aus welcher Quelle seine Benutzer kommen. Hauptsache er weiß wie man auf diese zugreift.
Im nächsten Teil erkläre ich, wie man die Methoden der einzelnen Benutzer implementiert. Wir werden Kompromisse eingehen was die Serendipity-Benutzer angeht und einen bösen Fallstrick in Zend_Db_Table kennen lernen, der mir eine halbe Stunde suchen beschert hat.