PHP: Verkettete Methoden / Fluent Interface
Häufig möchte man auf einem Objekt mehrere Methoden aufrufen. Wenn man dann immer den Objektnamen vor den Methodennamen schreiben muss und jeder Aufruf somit eine neue Codezeile bedeutet, kann das schnell unübersichtlich und nervig werden. In diesem Artikel bringe ich euch die sogenannten Fluent Interfaces näher, die z.B. im Zend Framework sehr häufig benutzt werden. Andere Namen dafür sind z.B. “Verkettete Methoden” oder auf engl. “Method Chaining“.
Für diejenigen, die sich nicht vorstellen können, was genau ich meine, kommt hier ein kleines Beispiel. Es geht um eine fiktive Datenbankklasse, die Queries zusammenbauen kann, ohne dem User das Verwenden von SQL-Code aufzuzwingen. Sowas wird übrigens auch ORM genannt.
Wir gehen davon aus, dass die Klassen des ORM bereits fertig sind und wir sie nur anwenden möchten. Die Aufgabe lautet nun z.B. “Wähle alle Attribute von 5 Personen aus, die männlich sind. Sortiere dabei nach dem Alter (Absteigend)”. Der Code dazu:
$query = new DbQuery($db_connection); //$db_connection beinhaltet z.B. die Verbindung zur Datenbank. Hier unwichtig.
$query->select("*"); //Alle Attribute
$query->from("persons");
$query->where("gender", DbQuery::Cond_Equals, "male");
$query->order("age", DbQuery::Order_DESC);
$query->limit(5);
$result = $query->execute();
Wir haben hierdurch zwar keinerlei SQL-Code verwendet, aber der Code ist unnötig kompliziert. Wie man sieht, wird $query in fast jeder Zeile verwendet. Wäre so etwas nicht viel schöner?
$query = new DbQuery($db_connection); //$db_connection beinhaltet z.B. die Verbindung zur Datenbank. Hier unwichtig.
$result = $query->select("*")->from("persons")->where("gender", DbQuery::Cond_Equals, "male")->order("age", DbQuery::Order_DESC)->limit(5)->execute();
Der Code macht exakt das selbe und benötigt nur 2 statt 7 Zeilen. Außerdem lässt sich die 2. Zeile fast wie ein Satz lesen, was ein großer Vorteil von Fluent Interfaces ist. Der Satz lautet “Wähle alles von Personen aus, wo das Geschlecht männlich ist, ordne nach dem Alter, aber limitiere auf 5 Datensätze”.
Doch wie setzt man so etwas um? Das ist gar nicht so schwer. Man muss einfach in jeder Methode (hier: select(), from(), where(), order(), limit()) wieder das DbQuery-Objekt zurückgeben. Wie wir wissen, kann man das aktuelle Objekt in jeder Klasse mit $this referenzieren. Also geben wir einfach $this zurück!
class DbQuery
{
/*
Methoden, Attribute, ...
*/
public function select($what)
{
//...
return $this;
}
public function from($where)
{
//...
return $this;
}
public function where($attribute, $condition, $value)
{
//...
return $this;
}
public function order($attribute, $order)
{
//...
return $this;
}
public function limit($amount)
{
//...
return $this;
}
public function execute()
{
$result = .... (Object von DbResult wird erzeugt)
return $result;
}
}
Wie unschwer zu erkennen ist, geben alle Methoden $this zurück, mit der Ausnahme, dass execute() das nicht tut. Bei genauerem Betrachten fällt auch auf, dass alle Methoden Setter sind und execute() ein Getter ist. Daraus lässt sich folgern, dass alle Setter $this statt nichts (wie es der Normalfall wäre) zurückgeben. Das trifft auf die meisten Anwendungsfälle zu, denke ich.
Kann ich nicht alles in einer Zeile unterbringen? Auch das geht! Wir benötigen nur eine Factory, die das DbQuery-Objekt erzeugt. Diese könnte z.B. so aussehen:
class DbQueryFactory
{
private $connection;
public function __construct(DbConnection $connection)
{
$this->connection = $connection;
}
public function getQuery()
{
return new DbQuery($this->connection);
}
public static function getQueryStatic(DbConnection $connection)
{
$factory = new self($connection);
return $factory->getQuery();
}
}
Diese Factory bietet jetzt 2 Möglichkeiten, an ein DbQuery-Objekt zu kommen. Einmal statisch und einmal objektgebunden.
Statisch
$result = DbQueryFactory::getQueryStatic($connection)->select("*")->from("persons")->where("gender", DbQuery::Cond_Equals, "male")->order("age", DbQuery::Order_DESC)->limit(5)->execute();
Sehr schön! Alles in nur einer Zeile! Und wir sprechen das Query-Objekt niemals irgendwo direkt an. Wenn das mal nicht abstrakt ist
Objektgebunden
Wollen wir mit einer Factory mehrere Queries erzeugen, dann ist diese Möglichkeit evtl. besser. Aber das ist Geschmackssache!
$factory = new DbQueryFactory($connection);
$result1 = $factory->getQuery()->select("*")->from("persons")->where("gender", DbQuery::Cond_Equals, "male")->order("age", DbQuery::Order_ASC)->limit(5)->execute();
$result2 = $factory->getQuery()->select("name")->from("persons")->where("gender", DbQuery::Cond_Equals, "female")->order("age", DbQuery::Order_DESC)->limit(5)->execute();
$result3 = $factory->getQuery()->select("*")->from("persons")->where("gender", DbQuery::Cond_Equals_Not, "male")->order("age", DbQuery::Order_DESC)->limit(10)->execute();
Für die eigentliche Query wird auch hier nur eine einzige Zeile benötigt. Man könnte aber auch hier jeweils die statische Methode nehmen.
Weitere Möglichkeiten
Es gibt auch noch mehr Dingen, die man mit Fluent Interfaces so anstellen kann.
Hat man beispielsweise eine Klasse, deren Methoden Objekte aufnehmen kann, dann bietet sich dieses Vorgehen besonders gut an:
class Container
{
private $items = array();
public function addItem(Item $item)
{
$this->items[] = $item;
}
}
class Item
{
//Attribute...
public function setName($name)
{ ... }
public function setId($id)
{ ... }
public function setValue($value)
{ ... }
}
Nehmen wir an, dass wir 3 Items in den Container aufnehmen wollen. Die Items sollen alle einen Namen, eine ID und einen Wert haben. Ohne ein return $item in Container::addItem(Item $item) und return $this in den Setter-Methoden in Item müssten wir folgendermaßen vorgehen:
$container = new Container();
$item1 = new Item();
$item1->setName("name");
$item1->setId(1);
$item1->setValue("wert");
$container->addItem($item1);
$item2 = new Item();
$item2 ->setName("name2");
$item2 ->setId(2);
$item2 ->setValue("wert2");
$container->addItem($item2 );
$item3 = new Item();
$item3 ->setName("name3");
$item3 ->setId(3);
$item3 ->setValue("wert3");
$container->addItem($item3 );
Viel Code für wenig, oder? Also machen wir es doch einfach so!
class Container
{
private $items = array();
public function addItem(Item $item)
{
$this->items[] = $item;
return $item;
}
}
class Item
{
//Attribute...
public function setName($name)
{
//...
return $this;
}
public function setId($id)
{
//...
return $this;
}
public function setValue($value)
{
//...
return $this;
}
}
Alle Methoden geben jetzt die richtigen Objekte ($this bzw. $item) zurück. Den obigen Code zur Einlagerung von 3 Items in den Container lösen wir jetzt so:
$container = new Container();
$container->addItem(new Item())->setName("name")->setID(1)->setValue("wert");
$container->addItem(new Item())->setName("name2")->setID(2)->setValue("wert2");
$container->addItem(new Item())->setName("name3")->setID(3)->setValue("wert3");
Aus 22 Zeilen wurden 4 Zeilen. Und der Code ist wesentlich lesbarer! Ihr seht, die Fluent Interfaces sind sehr nützlich.
Fazit
Fluent Interfaces oder auch verkettete Methoden bringen keine Vorteile in der Programmfunktionsweise. Sie machen Programme aber wesentlich lesbarer und erleichtern die Anwendung der OOP! Die oben geschriebenen Abfragen lassen sich wie englische Sätze lesen. So verstehen sogar nicht-Programmierer diese Zeilen! In einer typschwachen wie PHP ist dies z.b. auch wesentlich einfacher möglich als in C++.
Ich möchte jedenfalls nicht mehr ohne die kleinen Helferchen arbeiten und bin dankbar, dass PHP5 uns dieses wunderbare Geschenk macht
- Objektorientierte ID-Verwaltung in PHP
- Speicherung von Daten (in einer Model-Klasse)
- Mein eigenes MVC-Framework: Das Model
- Mein eigenes MVC-Framework: Die Session-Klasse
Dieser Artikel wurde von Simon verfasst.
Gelesen: 516x heute: 3x
Dieser Artikel wurde am Freitag, Februar 5th, 2010 um 17:00 in den Kategorien Wissenswertes geschrieben. Du kannst die Kommentare über den Feed (RSS 2.0) beobachten. Du kannst eine Antwort hinterlassen, oder einen Trackback von deiner Seite setzen.



Wieder mal ein sehr guter Beitrag zur Horizonterweiterung! Danke!
Dankeschön!