Facebook
Twitter
Google+
Kommentare
8

Von Tischen, Caching, POPO und OOP

Vor kurzem hatte ich mich ja mal ausführlich mit dem Thema Caching beschäftigt. Das Ergebnis konnte man im PHP-Magazin lesen. Dabei ging es hauptsächlich um mögliche Caching-Schichten und Header, die man setzten kann um Webseiten effizient zu persistieren. Wenn man das mal verstanden hat, dann kann man gute und schnelle Anwendungen stricken. Da dieses Thema für mich fürs erste abgeschlossen ist, möchte ich heute einen Fehler ansprechen, der mir immer wieder im Umgang mit Caches aufgefallen ist.

Nehmen wir mal an, wir haben ein Objekt, das wir Cachen wollen, beispielsweise einen Tisch. Da das bauen eines Tisches lange dauern kann, müssen wir irgendwo eine Instanz hinterlegen können, auf die wir zugreifen dürfen. Ein Tischlager wenn man so will. Jetzt kann man so vorgehen, dass man dem Stuhl beibringt wie er sich selbst Cachen kann. Also zum Beispiel eine store und load implementieren. Wenn ich dann einen bestimmten Stuhl brauche, dann kann ich prüft die load-Methode, ob er bereits im Cache ist oder ob man selbst eine neue Instanz erstellen muss. Klingt irgendwie üblich (das Active-Record-Pattern funktioniert ja so).

Warum gefällt mir das aber nicht? Ganz einfach, ich mag die objektorientierte Programmierung. Wenn ich die Klassen konstruiere, stelle ich mir immer das reale Objekt vor. Die Frage „Würde das reale Objekt von dieser Eigenschaft wissen?“ kann da sehr gut helfen. Wenden wir das also mal auf unseren Tisch an. Weiß euer Küchentisch, dass es ein Tischlager gibt? Nein. Er weiß, dass er aus Holz ist und vier Beine hat, aber ansonsten ist er eher dumm.

Wie müsste man dann dann die Implementierung gestalten. Wir hatten ja vorhin den Ansatz, dass wir dem Tisch beibringen sich abzulegen. Der Ansatz ist aber eigentlich, dass wir einen Lagerarbeiter bauen (Caching-Schicht) dem wir sagen, er soll den Tisch persistieren. Ganz wie in der realen Welt. Das wunderbare dabei ist, dass eurer Tisch auf einmal ein POPO (plain old php object) ist und nicht viel mehr kann als nötig. Testbarkeit steigt somit und auch die Wartbarkeit. Als Leitspruch kann man vielleicht auch mitnehmen „Je dümmer ein Objekt, desto besser“. Doctrine2 macht das übrigens auch so, was ich sehr elegant finde.

Was sollte man mitnehmen:

  • Immer mal wieder Fragen, ob das reale Objekt von den Eigenschaften wissen kann
  • Je dümmer ein Objekt, desto einfacher ist es warten und zu testen
  • Caching muss außerhalb von den zu cachenden Objekten und nicht in ihnen
Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

8 Comments

  1. Gebe Dir volkommen Recht: je dümmer das Objekt, desto besser!
    Diesem Leitfaden folge ich immer bei der Entwicklung. Sonst endet man irgendwann mit Gott-Klassen die alles können und schlussendlich nie im vollen Umfang gebraucht werden. Dann läuft man Gefahr mehr Speicher zu benötigen als wirklich notwendig.
    Zudem verfällt beim Caching ein Teil der komplexität in die eigentliche Logik zurück. Denn meiner Meinung nach hat das Caching nix im Objekt zu suchen.

    Reply
  2. Schließe mich da auch vollkommen an, bin daher auch sehr angetan von der Cache Implementierung des Zend Frameworks. Auch das Vorgehen meine Objekte so doofe wie möglich zu halten hat sich mittlerweile als oft hilfreich erwiesen, ein User Object was nur get/set etc. kann, lässt sich wunderbar auch mal in der Session ablegen ohne sich mit wakeups zu beschäftigen.

    Reply
  3. Die erste Regel der OOP: Verwechsel niemals Stühle mit Tischen!

    Im Ernst, warum verwenden die meisten OOP-Anhänger ständig Anthropomorphismen [1]? Weder ein Tisch noch ein Objekt kann etwas wissen. Wenn man programmiert, dann entwickelt man zielgerichtete Abstraktionen (von möglicherweise realen Objekten). D.h., es hängt vom Kontext ab, wie man abstrahiert: Werden bspw. Tische verkauft, so wären nur die Informationen über die Tische von Interesse – eine geeignete Abstraktion sollte also nur diese Informationen preisgeben. Würde ein Tisch jedoch als Ablage für andere Objekte fungieren, so hätte eine passende Abstraktion die Funktionalität, dass man Objekte auf den Tisch legen und von ihm entfernen kann.

    [1] http://www.cs.utexas.edu/users/EWD/ewd08xx/EWD854.PDF

    Reply
  4. @Nik: Ich vermute, dass der Hinweis „Immer mal wieder Fragen, ob das reale Objekt von den Eigenschaften wissen kann“ keine Funktion als Lehrbeispiel hat.
    Im Kontext von Caching ist Entkoppelung von Informationen und Funktionalität wesentlich, aber dafür muss ich das „reale Objekt“ nicht betrachten.

    Den anderen Ratschlägen stimme ich übrigens absolut zu.

    Reply
  5. Zum Artikel würde noch ergänzen, dass man um ein Objekt zu cachen immer eine eindeutige Id braucht.
    Ich denke, wenn man das Thema für Fortgeschrittene erklärt, könnte man noch deutlich weiter gehen: Es kommt nämlich immer darauf an. Eine Registry (hier: „Tischlager“) ist nicht unbedingt immer das Mittel der Wahl.

    Beispiel:

    class Tischlager implements Registry {

    protected static $instances = array();

    public function load($id) {
    if (!isset(self::$instances[$id])
    self::$instances[$id] = new Tisch($id);
    return self::$instances[$id];
    }

    public function store($tisch) {
    self::$instances[$tisch->getId()] = $tisch;
    }

    }

    Man kann auch mit dem Abstract-Builder bzw. Abstract-Factory Pattern eine Caching-Schicht bauen.

    Beispiel Builder:

    class FooBuilder extends AbstractBuilder {

    public function newFoo() { … }
    public function useCache(Cache $c) { … }
    public function buildFoo() { return … }

    }

    Eignet sich vor allem dann gut, wenn man in Schleifen größere Mengen gleichartiger Objekte laden will.

    Beispiel Factory:

    class SessionFactory implements Factory {

    public function getTable($id)
    {
    return TableSession::getInstance($id);
    }

    }

    class TableSession extends Tisch {

    // load
    public static function getInstance()
    { … }

    // store
    public function __destruct()
    { … }

    }

    Dies eignet sich besser für einzelne Objekte.

    Aber es geht auch mit dem Facade-Pattern, wenn das Facade-Objekt gleichzeitig die Rolle Factory bekommt.

    Beispiel:

    class FooFacade {

    public function __construct($config) { … }

    public function getFoo($id) {
    switch (true) {
    case $this->session->isCached($id):

    case $this->memCache->isCached($id):

    default:

    }
    // initialize
    return $foo;
    }

    }

    Das Facade-Pattern eignet sich gut, um komplexe Operationen beim Caching, zum Beispiel Cache-Kaskaden, zu verstecken. Auch ganz gut, wenn man den Zustand mehrerer lose gekoppelter Objekte synchronisieren will. Mal angenommen der Tisch hätte noch eine Gedeck mit Messer, Gabel, Löffel und Teller und man möchte die Funktionen „TischDecken“ und „TischAufräumen“ implementieren. (Das Tisch-Beispiel sucks.)

    Auch das Observer-Pattern kommt in Betracht. Damit kann man mehrere Caches als Observer registrieren und informieren, wenn eingebettete Objekte via Lazy-Loading nachgeladen werden sollen. Die Caches werden nacheinander informiert und der erste, der einen Treffer hat, lädt das Unterobjekt. Normalerweise nimmt man Observer zwar primär für Logging, aber dieses Pattern kann deutlich mehr.

    Beispiel:

    class Foo implements Observable {

    protected static $observers = array();

    public static function register(Observer $o)
    { self::$observers[] = $o; }

    public function setComplextObject($o)
    { $this->object = $o; }

    public function getComplexObject() {
    if (!isset($this->object)){
    foreach ($this->observers as $o)
    { if ($o->notify($this, ‚object‘)) { break; } }
    }
    return $this->object;
    }

    }

    Wenn man das Caching über eine Erweiterung der Klasse einbaut (also vertikal), sollte man den Konstruktor als protected markieren und eine Factory-Methode verwenden. Wenn man mag kann man zur Unterscheidung einer Klasse über Interfaces verschiedene Rollen zuordnen: IsMemCache, IsSessionCache, IsFileCache … wie gesagt: Viele Wege führen nach Rom.

    Beispiel:

    class FooSessionCache extends Foo {

    public static getInstance($id) {
    if (self::_isCached($id))
    return self::_load($id);
    else
    return parent::getInstance($id);
    }

    public function __destruct() {
    $this->_store();
    }

    }

    Eine schön einfache Variante ohne Vererbung:

    class Foo implements Cacheable {

    private static $_cache;

    protected function __construct() {}

    public static function register(CacheInterface $cache)
    { self::$_cache = $cache; }

    public static function getInstance($id)
    { return self::$_cache->load($id); }

    public function __destruct()
    { self::$_cache->store($this); }

    }

    Wenn es nicht stört, dass das Objekt grundsätzlich wissen kann, dass es einen Cache gibt, so spart dies eine Persistenzklasse. Für Testzwecke kann man dem Objekt einen DummyCache geben, der so aussieht:

    class FooCache extends Foo implements CacheInterface {

    public function load($id) { return $this; }

    public function store($id) { }

    }

    Welcher Cache verwendet wird kann man dann extern konfigurieren und das Registrieren der Caches übernimmt entweder (lazy) der Autoloader beim Laden der Klasse oder (eager) eine Bootstrap. Ob und welcher Cache verwendet wird ist für den Entwickler damit völlig transparent.

    So, das waren dann also noch mal ein paar Alternativen.

    Reply
  6. @Andre Moelle: Die eigentliche Frage ist eher, ob etwas gegenständliches wie ein Tisch als Beispiel für Caching (das Vorhalten einer Instanz-Kopie) geeignet ist. Und die kann man wohl verneinen. Bestimmte Prozesse in der Programmierung haben eben keine Entsprechung in der gegenständlichen Welt.
    Trotzdem finde ich die klassischen Auto-Klasse Beispiele für Einsteiger sinnvoller, als über ominöse Controller zu reden, von denen nicht mal Programmierer oft wissen, welche Bedeutung des Begriffs gerade gemeint ist.

    Wie auch immer, wir sind hier unter uns und das Tischbeispiel hätte man sich schenken können, weil es nicht richtig passt. Allerdings wäre dann vom Artikel nicht mehr viel übrig 🙂

    Reply
  7. Hmm… eines lässt mir keine Ruhe. Ich überlege gerade, ob Nils den „Popo“ in seinen Metatags verwendet hat. Ich glaube dann noch statt „Tisch“ das Beispiel „Bett“ eingebaut und ich wäre wirklich gespannt, was Google Adsense daraus macht.

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen