Zend Framework: Decorator Pattern

De theorie achter het Decorator Pattern is eenvoudig. Je hebt een object, je wilt bij een bepaalde functie van dat object (meestal het renderen ervan) wat eigen functionaliteit hangen, zonder dat je de het object van gedrag wilt laten veranderen. Denk hierbij aan het volgende:

#!php $widget = new Widget(); $widget->render();

Even aangenomen dat de Widget class een <object ... /> tag rendert, en je zou willen dat de widget wordt voorzien van een titel en in een div’je wordt gewrapt, dan kun je er een decorator aanhangen die daar zorg voor draagt:

$widget = new Widget();
// methode 1: Widget kan "gedecorate" worden, en zal de decorator
// aanroepen als hij gerenderd wordt.
$widget->addDecorator('MyWidgetDecorator');
// methode 2: Widget weet niks van MyWidgetDecorator, MyWidgetDecorator
// draagt zorg voor rendering en zal de render() method van de widget
// waarschijnlijk aanroepen
$widget = new MyWidgetDecorator($widget);
$widget->render();

Zend gebruikt dit design pattern in het Zend Framework in de Zend_Form package om formulieren van HTML te voorzien. ZF maakt gebruik van bovenstaande eerste methode. Dit betekent dat het element dat “gedecoreerd” wordt altijd op de hoogte moet zijn van het feit dát hij gedecoreerd wordt, terwijl dat binnen de context vaak helemaal niet interessant is.

Nu vind ik het hele principe dat je in de PHP code bepaalt wat de HTML-output wordt al eng, aangezien ikzelf (en pure front-enders met mij) dat gewoon in de templates willen bepalen. Ik ben ook een voorstander van Smarty, wat het nog eens extra bemoeilijkt, maar ook als PHP zelf als template renderer gebruikt, is het een vervelend principe.

De basis is namelijk dat je gegevens in je template hoort te hebben die je wilt gaan presenteren. Gegevens zijn bijvoorbeeld de foutmeldingen, of eventueel de controls (zoals een text-input of een button, hoewel je er over kunt discussieren of dat niet ook gewoon door je view bepaald moet worden). ZF’s method van decoraten levert hier een probleem op. Als je namelijk een formulier wilt gaan renderen, dan kun je er niet op een intuitieve, eenduidige manier achter komen wat precies de bron van de gegevens is, en hoe je daar eventueel invloed op uit kunt oefenen.

Daarom vind ik dat Zend_Form een grote refactoring nodig heeft, om gebruik te gaan maken van methode 2. Het “decoraten” van het object gaat dan niet alleen meer over renderen, maar ook over toevoegen van properties, methodes en implementatie van interfaces die het object nog niet ondersteunt. Neem daarvoor een abstract base class, die simpelweg alle bestaande functionaliteit doorsluist naar het gewrapte object:

abstract class Decorator_Abstract {
    protected $_innerObject;
 
    function __construct( $object ) {
        $this->_innerObject = $object;
    }
 
    function __get ( $name ) {
        return $this->_innerObject->{$name};
    }
 
    function __call ( $method, $args ) {
        $refl = new ReflectionMethod($this->_innerObject, $method);
        return $refl->invokeArgs($this->_innerObject, $args);
    }
 
    function __toString () {
        return $this->_innerObject->__toString();
    }
 
    function __destruct () {
        unset($this->_innerObject);
    }
 
    function getInnerObject( $deep = true ) {
        $ptr = $this;
        if($deep) {
            while($ptr instanceof self) {
                $ptr = $ptr->getInnerObject(false);
            }
        } else {
            $ptr = $this->_innerObject;
        }
        return $ptr;
    }
}

Je kunt mbv deze class heel eenvoudig objecten wrappen en aanvullen met je eigen methods, zonder dat je het oorspronkelijke object kwijtraakt.

class MyWidgetDecorator extends Decorator_Abstract {
    protected $_title = null;
 
    function __construct($widget, $title) {
        parent::__construct($widget);
        $this->_title = $title;
    }
 
    function __toString() {
        return sprintf('<div class="widget-container"><h1>%s</h1>%s</div>', $this->_title, (string) $this->_innerObject);
    }
}

Waarom is dit dan beter? Je weet als decorator altijd wat er verder nog gerenderd gaat worden, en kan daar alles van beïnvloeden wat je wilt. Stel je maakt een eenvoudige decorator waarmee je content escapet:

class MyOutputEscapingDecorator extends Decorator_Abstract {
    function __toString() {
        return htmlspecialchars((string) $this->_innerObject);
    }
 
    function getRawValue() {
        return (string) $this->_innerObject;
    }
}

Je kunt daar elk willekeurig object instoppen die zichzelf kan renderen, ook al is dat ook weer een decorator:

class MyNewline2BrDecorator extends Decorator_Abstract {
    function __toString() {
        return nl2br((string) $this->_innerObject);
    }
}
 
class Something {
    function __toString () {
        return "This contains some <b>html</b>\n\nAnd some newlines";
    }
    function isSpecialCase () {
        return true;
    }
}
 
$something = new Something();
// do whatever
 
$something = new MyNewline2BrDecorator(new MyOutputEscapingDecorator($something)));
 
if($something->isSpecialCase()) {
    // do something extraordinary
} else {
    echo $something;
}

Maar je hebt geen enkel informatieverlies, in tegenstelling tot de rendering van Zend_Form, waarbij ik bij het renderen van de label helemaal niets van andere decorators, en of die al gerenderd zijn, en wat ze dan gerenderd hebben. Stel nou dat ik ergens in een template 1 lullige uitzondering wil maken? Dan ben ik de beer en moet ik terug in de back-end code om al die decorators uit te schakelen voor dat ene gevalletje, terwijl het feitelijk gewoon een presentatie-issue is. Met methode no.2 kun je daarentegen gewoon zeggen:

if($field->name == 'specialField') {
    echo $field->getInnerObject(); // only print field, no decorators
} else {
    echo $field;
}

Dan is het ineens wél een prettige toevoeging van de form engine dat er standaard renderers zijn, zonder dat er allerlei informatie verdwijnt en het belachelijk omslachtig wordt om uitzonderingen te bouwen.

Lees de originele post op drm.tweakblogs.net

This entry was posted in Development and tagged , . Bookmark the permalink. Comments are closed, but you can leave a trackback: Trackback URL.