Decorators

This is not technically a feature request at this point, I’m mostly posting this idea for discussion/review.

I wrote a simple class that implements the decorator pattern in PHP - and here it is, with a simple example:





<?php


class CDecorator

{

  private static $registry=array();

  

  protected static function register($className, CDecorator $decorator)

  {

    $decoratorClass = get_class($decorator);

    $class = new ReflectionClass($decoratorClass);

    

    foreach ($class->getMethods() as $method)

    {

      if ($method->isProtected())

      {

        @self::$registry[$className][$method->name] = array($decorator, $method->name);

      }

    }

  }

  

  public static function invoke($object, $name, $arguments=null)

  {

    $callback = self::$registry[get_class($object)][$name];

    return call_user_func_array($callback, array_merge(array($object), $arguments));

  }

  

  public function __construct($className)

  {

    self::register($className, $this);

  }

}


class User

{

  public $firstName;

  public $lastName;

  

  public function __call($name, $arguments)

  {

    return CDecorator::invoke($this, $name, $arguments);

  }

}


new NameDecorator('User');


class NameDecorator extends CDecorator

{

  protected function getFullName($object)

  {

    return $object->firstName . ' ' . $object->lastName;

  }

}


$user = new User;

$user->firstName = 'Rasmus';

$user->lastName = 'Schultz';


echo $user->getFullName();



In this example, the User class is being decorated by the NameDecorator class with a getFullName() method.

The CDecorator class serves both as a simple manager/registry for decorations, as well as a base-class for decorators.

To apply a decorator to a class, you simply place a statement like new DecoratorClass(‘DecoratedClassName’) above or below your decorated class - in the file, outside the class declaration, that is.

This forces decorators to load immediately - lazy-loading decorators is not possible, but lazy-loading a decorated class is of course still possible, although the decorator will always load as soon as the decorated class loads.

This specific example is very simple - the idea is that a number of classes that all need to know how to concatenate first and last names into a full name. Using a decorator, these classes can share the getFullName() method implementation, without belonging to the same object hierarchy.

How is this different from CBehavior?

CBehavior maintains a list of decorators per instance - this is initialized every time an object is constructed. CDecorator instead maintains a single, static list of decorator methods, per class.

CBehavior lets you decorate a single object, dynamically, at run-time, on demand - while CDecorator lets you decorate a class, statically, at load-time.

So in terms of functionality, not much difference - both approaches enable you to add methods to an object.

The main difference is in terms of performance. While you could implement a NameBehavior using CBehavior, and get a result that is functionally equivalent to this example, the behaviors would be constructed and associated with each new object.

Of course, you could probably construct your NameBehavior once, using a singleton pattern, and assign the same NameBehavior instance each time an object is constructed - the only overhead then, as compared to a decorator, is the maintenance of the behavior-list in each instance, plus the overhead of searching them for the requested method.

Maybe there is not much to gain in terms of performance, except perhaps in cases where you have many behaviors actually just working as decorators - but those cases may be rare.

So it mostly comes down to the question, is the decorator pattern necessary or useful in Yii?

Or do behaviors already do the job?

1 Like

I just want to say, that your posts are most valuable in this forum, full of ideas, critisms, very detailed descriptions, examples etc…

Its always a fascinating read

thumbs up

What I want to achieve

Hello, for implementing dynamic attributes to models (something like eav) I wanted to write a behavior… but cause of the complexity of all that dynamic changes (adding new attributes to __get __set, also adding new entries into rules() and relations()) the behavior has some problems which caused me to look for other solutions.

Problems of behaviors:

First Overwriting __set and __get could be done with http://code.google.com/p/yii/issues/detail?id=2233

The "relations()" could be extended by getOwner()->getMetaData()->addRelation()

For rules i have no solution yet. Adding another eventhandler to is the best solution i can see.

Your Decorator solution

Your solution looks quite good and showed me the right direction. But I don’t like that I have to overwrite the magic methods in my class. I like it more to have just one point where I can add and remove my new features.

My Idea

I show first some Pseudocode. I will attach full code too.




// extending ccomponent to initialize it easy from a config-array

class CDecorator extends CComponent {

  protect $_model;

  public function attach($model)  {

     $this->_model = $model;

  }

  // forward everything with magic methods

  public function __get($name) {

     $this->_model->$name;

  }

  public function __set()..

  public function __call()..

  ...

}


class CDecoratableActiveRecord extends CActiveRecord {

  // definition of decorators like behaviors:

  public function decorators(){ return array() }

  public function getDecorated() {

     return $this->attachDecorators($this->decorators());

  }

  public function attachDecorators($ds){

     $obj = $this;

     foreach ($ds as $d)

        $obj = $obj->attachDecorator($d);

      return $obj;

  }

  public function attachDecorator($d)

  {

    $d = new Decorator();

    $d->attach($this);

    return $d;

  }

}




// usage

class User extends CDecoratableActiveRecord {

   protected function decorators() {

      return array(array('MyDecorator', 'class'=>'...'));

   }

}

actionUpdate($id) {

  $model = User::model()->findByPk($id)->getDecorated();

}



The disadvantage of my solution is, that you have to call getDecorated() after you got the CActiveRecord.

This could be solved with overwriting some methods of CActiveFinder and CActiveRecord::model() but I couldn’t dig yet deeper in the code.

my Decorator advantages/disadvantages

In my opinion the decorator compared to the behavior has many advantages.

  • Like Behaviors you could add and remove them on runtime (not implemented yet but in theorie)

  • Unlike Behaviors they can redefine everything of another class

  • They feel like a parent/child relation between decorator and model which makes it faster to understand the concept (but also faster to misunderstand the concept)

There are also some disadvantages in my solution which are quite bad yet:

  • When your model returns $this from a function and $this is used somewhere else (for instance method chaining) the decorators will be lost

  • The Decorators could also be lost when walking up the inheritence hierarchy of the model

Especially the disadvantages aren’t there in your solution but maybe they could be solved in my code too when I add some more intelligence to my magic methods which will look if the decorators got lost.

Todo for my Implementation

  • Autoadd Decorators after retrieving the model from db.

  • Add possibility to remove specific decorators.

  • Possibility to dynamically add new ones

  • Ability to specify an order which decorator comes first/last.

  • readd Decorators when they got lost (is this even possible?)

conclusion

I am happy to read opinions and suggestions about this topic. When it’s possible to solve the disadvantages in a good way this will be a realy powerful feature to simply add and remove complexity to objects

nice, useful to me :D

Interesting idea for using decorators from ruby community: github: drapergem/draper To decorate models with classes having only presentational methods. Which would otherwise end up in model, or being a helper function, or duplicate code in controller. I’m mainly passing associative arrays to the view though, and now thinking about having such decorators extend ArrayObject.