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 🙂

1 Star2 Stars3 Stars4 Stars5 Stars (Wurde noch nicht bewertet)
Loading...


11 Kommentare zu “PHP: Verkettete Methoden / Fluent Interface”

  1. Wieder mal ein sehr guter Beitrag zur Horizonterweiterung! Danke!

  2. Dankeschön!

  3. Mal ein kleiner Einwurf von mir:
    Welchen Editor benutzt du für php und hat dieser auch Codevervollständigung in oop? Da suche ich nämlich noch einen geeigneten für 🙂
    .-= maTTes´s last blog ..Ein toller Besuch im Europa-Park =-.

  4. Ist das ein WordPressplugin was das Syntaxhighlightling übernimmt? Wäre super wenn du verraten könntest welches. Kann sowas dringend gebrauchen.

  5. Jop, ist ein WP-Plugin. Syntax Highlighter Evolved nennt sich der.

    Habe nach monatelanger Suche endlich ein funktionierendes Plugin gefunden (nachdem ich den Schrott namens WP-Syntax nicht mehr ertragen habe)

    Hier gibts mehr Infos zu dem Thema: http://www.net-developers.de/blog/2009/12/30/syntaxhighlighter-fur-wordpress-gefunden/

    MfG
    Simon

  6. @maTTes: Ich verwende eigentlich immer PSPad ohne Codevervollständigung. Momentan teste ich aber Komodo, ist ganz nett. Aber irgendwie komm ich mit PSPad besser zurecht

  7. Hallo,

    find das Tut sehr schön, aber gäbe es nicht ein Problem wenn man immer $this zurück gibt?

    Beispiel:

    MyDBObject = new MyDBObject();
    MyDBObject->Select(„*“)->Select(„*“)->Select(„*“)…
    oder hab ich da was verpasst? Kann man die Methode nich nochmal aufrufen bei Fluent Interfaces? Gibts da schon einen Mechanismus?

    Ich habe es selbst noch nicht umgesetzt, deswegen meine Frage. Hoffentlich kann sie mir jmd beantworten.

    Vielen Dank im Voraus
    MfG Chris

  8. Hi,

    warum sollte das ein Problem darstellen?
    Das funktioniert ausgezeichnet 🙂

    Methoden kann man so oft aufrufen, wie man will.

    MfG
    Simon
    .-= Simon´s last blog ..ICQ 72 – Das Multitalent =-.

  9. aber warum sollte man das ja eben wollen? also hier mags vlt noch gehen aber nehmen wir mal so ein date beispiel, wie sie es oft machen
    MyObject->Year(„2001“)->Year(„1975“)….das macht ja dann keinen Sinn z.B. LinQ in C# wenn ich dort eine SELECT oder danach eine WHERE aufrufe kann ich auch nicht nocheinmal SELECT oder WHERE dahinter verketten.

    MfG Chris

  10. Ok ich nehm alles zurück und behaupte das gegenteil 😀 in C# isses auch so da kann man Select().WHERE().Select.blabla machen 🙂 da ergibt das ganze wiegesagt einen Sinn.
    Nur was macht man wirklich bei diesem Datebeispiel? Year(„2010“).Year(„1999“).Year(„2011“)… Nimmt er dann den ersten Wert oder den letzten, gibt es Fehler?

    Sry wegen meines vorkommentares 🙂

    MfG Chris

  11. Du hast das Prinzip nicht verstanden, glaub ich 😉
    Alle Befehle werden abgearbeitet, warum sollte das anders sein?
    Fehler gibt es nur, wenn du kein Objekt zurückgibst und trotzdem eine Methode aufrufen willst.

Hinterlasse einen Kommentar!

Time limit is exhausted. Please reload the CAPTCHA.

»Informationen zum Artikel

Autor: Simon
Datum: 05.02.2010
Zeit: 17:00 Uhr
Kategorien: Wissenswertes
Gelesen: 18494x heute: 5x

Kommentare: RSS 2.0.
Diesen Artikel kommentieren oder einen Trackback senden.

»Meta